Page 1 of 1

Unique ID from copy / paste

Posted: Fri Nov 25, 2016 2:19 am
by cditcher
We have many elements in our schema that have unique "id" attributes. So far we have been injecting a random id when a user creates one of these elements through our framework in Author. Now we are working on preventing duplicate id's when a user copy/paste an element in Author mode. Our initial attempt was by extending AuthorDocumentFilter and overriding the insertFragment() method. This works to intercept an AuthorDocumentFragment, determine if there is an id, check the document to see if id exists, and update the id before passing to the AuthorDocumentFilterBypass object to inject the fragment with a fresh id.

This seems like a lot of overhead, as any operation that inserts content calls this method and we can safely ignore anything without id attributes. I have been scouring the api documents for a better solution. I have come across the addUniqueAttributesProccessor() method in the AuthorDocumentController class which looks promising, but I am having difficulty understanding how it is used based on documentation. Can this be used as a way to solve my problem? Can anyone provide a use case for this? There are other classes / interfaces which seem to deal with unique attributes, but again, I can't seem to decipher how they would be used.

Thanks,
Chris

Re: Unique ID from copy / paste

Posted: Fri Nov 25, 2016 2:53 am
by cditcher
Ah found the answer. Override the AuthorSchemaAwareEditingHandlerAdapter#handlePasteFragment method.

Re: Unique ID from copy / paste

Posted: Fri Nov 25, 2016 1:11 pm
by Radu
Hi,

What you found works. A better API method would have been to implement this callback:

ro.sync.ecss.extensions.api.ExtensionsBundle.getClipboardFragmentProcessor()

the default implementation for it (located in the ro.sync.ecss.extensions.commons.id.DefaultUniqueAttributesRecognizer) does something like:

Code: Select all


  /**
* @see ro.sync.ecss.extensions.api.content.ClipboardFragmentProcessor#process(ro.sync.ecss.extensions.api.content.ClipboardFragmentInformation)
*/
@Override
public void process(ClipboardFragmentInformation fragmentInformation) {
if(authorAccess == null) {
//EXM-20948 partial fix for NPE
logger.warn("NULL Author Access, should not happen", new Exception());
return;
}
GenerateIDElementsInfo currentElemsInfo = getGenerateIDElementsInfo();
if(currentElemsInfo != null && ! currentElemsInfo.isFilterIDsOnCopy()) {
//No filtering will be done.
return;
}
//Remove unique IDs when pasting in other documents or when copy/paste in the same document
boolean removeUniqueIDs = false;
if(fragmentInformation.getFragmentOriginalLocation() != null
&& ! fragmentInformation.getFragmentOriginalLocation().equals(authorAccess.getEditorAccess().getEditorLocation().toString())) {
if(preserveIDsWhenPastingBetweenResources()){
//EXM-21408 Paste from another document. Preserve the IDs
removeUniqueIDs = false;
} else {
removeUniqueIDs = true;
}
} else {
int purposeID = fragmentInformation.getPurposeID();
if(purposeID == AuthorSchemaAwareEditingHandler.CREATE_FRAGMENT_PURPOSE_COPY
|| purposeID == AuthorSchemaAwareEditingHandler.CREATE_FRAGMENT_PURPOSE_DND_COPY) {
removeUniqueIDs = true;
}
}
if(removeUniqueIDs) {
//Remove unique IDs
filterIDAttributes(fragmentInformation.getFragment().getContentNodes());
}
}

/**
* Check if we should preserve IDs when pasting between resources.
*
* @return <code>true</code> if we should preserve IDs when pasting between resources.
* By default the base method returns <code>true</code>.
*/
protected boolean preserveIDsWhenPastingBetweenResources() {
return true;
}

/**
* Filter all ID attributes from the fragment.
*
* @param contentNodes The nodes.
*/
private void filterIDAttributes(List<AuthorNode> contentNodes) {
for (int i = 0; i < contentNodes.size(); i++) {
AuthorNode node = contentNodes.get(i);
if(node.getType() == AuthorNode.NODE_TYPE_ELEMENT) {
//Remove the ID attribute.
((AuthorElement)node).removeAttribute(idAttrQname);
}
if(node instanceof AuthorParentNode) {
filterIDAttributes(((AuthorParentNode)node).getContentNodes());
}
}
}
Regards,
Radu

Re: Unique ID from copy / paste

Posted: Fri Nov 25, 2016 7:16 pm
by cditcher
Thanks Radu! This looks like what i was searching for.

Re: Unique ID from copy / paste

Posted: Wed Jan 25, 2017 6:29 pm
by Johann
Hello everybody,

I have an issue when I try to use my own CustomDefaultUniqueAttributesRecognizer.

I have created a CustomExtensionsBundle class in which I instantiate the AttributesRecognizer :

Code: Select all

  
public ClipboardFragmentProcessor getClipboardFragmentProcessor() {
return new CustomUniqueAttributesRecognizer();
}
In my framework, I put that :

Code: Select all


<field name="extensionsBundleClassName">
<String>package.CustomExtensionsBundle</String>
</field>
So the CustomUniqueAttributesRecognizer "works" in the sense that the method process is called when I perform a paste in my document. BUT, the authorAccess is always null. I know that the CustomUniqueAttributesRecognizer implements AuthorExtensionStateListener so... how to register the CustomUniqueAttributesRecognizer in the AuthorExtensionStateListeners ?

There is a section in the framework :

Code: Select all

<field name="authorExtensionStateListener">
But I cannot simply put :

Code: Select all


<field name="authorExtensionStateListener">
<String>package.CustomUniqueAttributesRecognizer</String>
</field>
because CustomUniqueAttributesRecognizer was instantiated by the CustomExtensionsBundle...

Thanks for your help,

Johann

Re: Unique ID from copy / paste

Posted: Thu Jan 26, 2017 9:32 am
by Radu
Hi Johann,

Indeed the same unique attributes recognizer needs to be connected as an extension state listener. This can be done entirely from the extensions bundle implementation.
As an example here's how the DITAExtensionsBundle (used for the DITA standard) does things:

Code: Select all

  /**
* The unique attributes recognizer
*/
private DITAUniqueAttributesRecognizer uniqueAttributesRecognizer;

/**
* @see ro.sync.ecss.extensions.api.ExtensionsBundle#createAuthorExtensionStateListener()
*/
@Override
public AuthorExtensionStateListener createAuthorExtensionStateListener() {
uniqueAttributesRecognizer = new DITAUniqueAttributesRecognizer();
return uniqueAttributesRecognizer;
}


/**
* @see ro.sync.ecss.extensions.api.ExtensionsBundle#getClipboardFragmentProcessor()
*/
@Override
public ClipboardFragmentProcessor getClipboardFragmentProcessor() {
return uniqueAttributesRecognizer;
}


/**
* @see ro.sync.ecss.extensions.api.ExtensionsBundle#getUniqueAttributesIdentifier()
*/
@Override
public UniqueAttributesRecognizer getUniqueAttributesIdentifier() {
return uniqueAttributesRecognizer;
}
Regards,
Radu

Re: Unique ID from copy / paste

Posted: Thu Jan 26, 2017 12:37 pm
by Isabelle
Hi Radu,

With Johann, we have implemented your solution but it seems that the framework doesn't call the method "createAuthorExtensionStateListener()" automatically and when we make a "paste" the method "getClipboardFragmentProcessor()" returns null.
Can you explain us how the method "createAuthorExtensionStateListener()" should be call.
Thanks.

Regards,
Isabelle

Re: Unique ID from copy / paste

Posted: Thu Jan 26, 2017 12:41 pm
by Radu
Hi Isabelle,

Could you double check, edit the framework configuration and in the Extensions tab make sure that you did not add an implementation of AuthorExtensionStateListener as a separate extension?
Because if you add it as a separate extension it will take precedence and the alternative createAuthorExtensionStateListener() method from the ExtensionsBundle implementation will not get called anymore.

Regards,
Radu

Re: Unique ID from copy / paste

Posted: Thu Jan 26, 2017 1:49 pm
by Isabelle
Radu,

You were right, we already had an implementation of AuthorExtensionStateListener in the framework.
It was for a StylesFilter.
So I delete this implementation and I have override the method "createAuthorStylesFilter()" in our extensions bundle implementation.
But this method is never called and our custom StylesFilter doesn't work anymore while our custom DefaultUniqueAttributesRecognizer works perfectly now.
What should I add to make both work again ?
Thanks.

Regards,
Isabelle

Re: Unique ID from copy / paste

Posted: Thu Jan 26, 2017 2:32 pm
by Radu
Hi Isabelle,

Possibly it's the same problem, in the Extensions tab you need to remove the reference to the custom styles filter implementation in order for the styles filter implementation used in the extensions bundle to start to be used.

Regards,
Radu

Re: Unique ID from copy / paste

Posted: Thu Feb 16, 2017 11:47 am
by Johann
Hello everyone,

we have still issues to implement exactly what we need.

I recap, in the same framework, we need an implementation of the StylesFilter class and an implementation of the DefaultUniqueAttributesRecognizer class. Both of these classes have to implement the AuthorExtensionStateListener (already the case for the DefaultUniqueAttributesRecognizer) because we need to use the authorAccess.

So, we have created en implementation of ExtensionsBundle and we put this in our framework file :

Code: Select all

<field name="extensionsBundleClassName">
<String>package.MyExtensionsBundle</String>
</field>

In this class, we have this :

Code: Select all

@Override
public AuthorExtensionStateListener createAuthorExtensionStateListener() {
uniqueAttributesRecognizer = new myDefaultUniqueAttributesRecognizer();
return uniqueAttributesRecognizer;
}

/**
* @see ro.sync.ecss.extensions.api.ExtensionsBundle#createAuthorStylesFilter()
*/
@Override
public MyStylesFilter createAuthorStylesFilter() {
if(stylesFilter == null)
{
stylesFilter = new myStylesFilter();
}

// Workaround to initialize authorAccess in StylesFilter.
// At the moment only one StateListener can be created.
if(uniqueAttributesRecognizer != null && uniqueAttributesRecognizer.getAuthorAccess() != null){
stylesFilter.activated(uniqueAttributesRecognizer.getAuthorAccess());
}

return stylesFilter;
}

public ClipboardFragmentProcessor getClipboardFragmentProcessor() {
return uniqueAttributesRecognizer;
}

/**
* @see ro.sync.ecss.extensions.api.ExtensionsBundle#getUniqueAttributesIdentifier()
*/
@Override
public MyDefaultUniqueAttributesRecognizer getUniqueAttributesIdentifier() {
return uniqueAttributesRecognizer;
}

With this code, when the document is opened, two instances of MyExtensionsBundle are created. The first one creates the StylesFilter, the second one creates the AuthorExtensionStateListener so it's impossible to pass the authorAccess to the StylesFilter. How can we reach what we need : both StylesFilter and DefaultUniqueAttributesRecognizer accessing authorAccess ?

Thank you for your help,

Johann

Re: Unique ID from copy / paste

Posted: Thu Feb 16, 2017 12:37 pm
by Radu
Hi Johann,

This is more of a question about how to delegate the same event to two objects. You need a third object to act as a proxy.
Here's one possible solution:

Code: Select all

public class MyExtensionsBundle extends ExtensionsBundle implements AuthorExtensionStateListener {

/**
* @see ro.sync.ecss.extensions.api.ExtensionsBundle#createAuthorExtensionStateListener()
*/
@Override
public AuthorExtensionStateListener createAuthorExtensionStateListener() {
return this;
}

private StylesFilter myStylesFilter = null;
private UniqueAttributesRecognizer attributesRecognizer = null;
private AuthorAccess authorAccess;

/**
* @see ro.sync.ecss.extensions.api.ExtensionsBundle#createAuthorStylesFilter()
*/
@Override
public StylesFilter createAuthorStylesFilter() {
if(myStylesFilter == null){
myStylesFilter = new StylesFilterTest();
if(authorAccess != null){
myStylesFilter.activated(authorAccess);
}
}
return myStylesFilter;
}

/**
* @see ro.sync.ecss.extensions.api.ExtensionsBundle#getUniqueAttributesIdentifier()
*/
@Override
public UniqueAttributesRecognizer getUniqueAttributesIdentifier() {
if(attributesRecognizer == null){
attributesRecognizer = new UniqueAttributesRecognizerTest();
if(attributesRecognizer != null){
attributesRecognizer.activated(authorAccess);
}
}
return attributesRecognizer;
}

/**
* @see ro.sync.ecss.extensions.api.AuthorExtensionStateListener#activated(ro.sync.ecss.extensions.api.AuthorAccess)
*/
@Override
public void activated(AuthorAccess authorAccess) {
this.authorAccess = authorAccess;
if(myStylesFilter != null){
myStylesFilter.activated(authorAccess);
}
if(attributesRecognizer != null){
attributesRecognizer.activated(authorAccess);
}
}

/**
* @see ro.sync.ecss.extensions.api.AuthorExtensionStateListener#deactivated(ro.sync.ecss.extensions.api.AuthorAccess)
*/
@Override
public void deactivated(AuthorAccess authorAccess) {
if(myStylesFilter != null){
myStylesFilter.deactivated(authorAccess);
}
if(attributesRecognizer != null){
attributesRecognizer.deactivated(authorAccess);
}
}

/**
* @see ro.sync.ecss.extensions.api.Extension#getDescription()
*/
@Override
public String getDescription() {
return "bla";
}

/**
* @see ro.sync.ecss.extensions.api.ExtensionsBundle#getDocumentTypeID()
*/
@Override
public String getDocumentTypeID() {
return "bla";
}
}
There is also this API class ro.sync.ecss.extensions.api.AuthorExtensionStateListenerDelegator which implements AuthorExtensionStateListener and to which you can add additional AuthorExtensionStateListener listeners. It's basically the same idea.

Regards,
Radu

Re: Unique ID from copy / paste

Posted: Thu Feb 16, 2017 1:40 pm
by Johann
Hello Radu,

I just tried your first solution but I get the same issue because two different instances of ExtensionsBundle are created. The first one calls createAuthorExtensionStateListener and the second one calls createAuthorStylesFilter... So, the first ExtensionsBundle is not aware of the stylesFilter end the second one is not aware of the stateListener.

I am going to see your second solution.

Johann

Re: Unique ID from copy / paste

Posted: Thu Feb 16, 2017 5:05 pm
by Radu
Hi Johann,

I tested and you are right, somehow the styles filter is created very early and there is some kind of transitory time in which it is created with an extensions bundle, then another extensions bundle is created and used later on. I need to look more into this.
Can you tell me more about your use case, why do you need the AuthorAccess?
One possibility on the styles filter filter callback would be to get access to the current page something like this:

Code: Select all

        WSEditor currented = PluginWorkspaceProvider.getPluginWorkspace().getCurrentEditorAccess(PluginWorkspace.MAIN_EDITING_AREA);
if(currented != null){
WSEditorPage currentPage = currented.getCurrentPage();
if(currentPage instanceof WSAuthorEditorPage){
WSAuthorEditorPage authorPage = (WSAuthorEditorPage) currentPage;
if(authorPage != null){
//This is the equivalent of AuthorAccess
}
}
}
The authorPage may be null during an initial number of calls but afterwards it should return the proper API.

Regards,
Radu

Re: Unique ID from copy / paste

Posted: Fri Feb 17, 2017 11:59 am
by Johann
Hi Radu,

I'm glad you got the same behaviour.

Among other things, the authorAccess in the filter method enables me to refresh some AuthorNodes.

I tried your solution and it seems to do the job.


Thank you !

Johann

Re: Unique ID from copy / paste

Posted: Fri Feb 17, 2017 12:56 pm
by Radu
Hi Johann,

For Oxygen 19 I will try to add an improvement so that if your StylesFilter implementation also implements AuthorExtensionStateListener, our call will automatically call the activated method on your implementation with the proper authoraccess object before the styles filter implementation starts getting used.
Among other things, the authorAccess in the filter method enables me to refresh some AuthorNodes.
That's precisely why I have always been skeptical about adding a dependency betwee the StylesFilter and a very powerful API which can do all sorts of things.
The StylesFilter is a CSS-like layer used to style content, it should not have any other logic, for example to refresh nodes. The AuthorAccess API has lots of methods, it can modify content, it can save the current file, but this is API not suitable to be used from an interface (StylesFilter) which is a CSS-like layer.
I'm not sure how you decide to refresh nodes (maybe you could give me more details about that) but you can also add a document listener to listen for modification events and call the refresh nodes API when appropriate.

Regards,
Radu

Re: Unique ID from copy / paste

Posted: Fri Feb 17, 2017 4:03 pm
by Johann
I understand what you mean.

We essentially use the refresh method because we set pseudoClasses to some authorNodes. Without this refresh, the pseudo class does not seem to be well applied.

Johann

Re: Unique ID from copy / paste

Posted: Fri Feb 17, 2017 4:07 pm
by Radu
Hi Johann,

A pseudo class should be set on an AuthorElement from the live edited XML document only using the AuthorDocumentController API:

ro.sync.ecss.extensions.api.AuthorDocumentController.setPseudoClass(String, AuthorElement)

The API takes care of notifying the UI of the pseudo class changes.
You should normally use ro.sync.ecss.extensions.api.AuthorElementBaseInterface.setPseudoClass(String) only when the element is not part of the edited content, for example if you have created an author document fragment with AuthorDocumentController.createDocumentFragment(int, int) and you want to process the fragment before inserting it back in the document.

Regards,
Radu