The link target reference finder represents the support for finding references from links which indicate specific elements inside an XML document. This support will only be available if a schema is associated with the document type.
If you do not define a custom link target reference finder, the
DefaultElementLocatorProvider
implementation will be used by default. The interface which should be
implemented for a custom link target reference finder is
ro.sync.ecss.extensions.api.link.ElementLocatorProvider
.
As an alternative, the
ro.sync.ecss.extensions.commons.DefaultElementLocatorProvider
implementation can also be extended.
The used ElementLocatorProvider
will be
queried for an ElementLocator
when a link
location must be determined (when user clicks on a link). Then, to
find the corresponding (linked) element, the obtained
ElementLocator
will be queried for each
element from the document.
The DefaultElementLocatorProvider
implementation offers support for the most common types of
links:
links based on ID attribute values
XPointer element() scheme
The method getElementLocator
determines
what ElementLocator
should be used. In the
default implementation it checks if the link is an XPointer
element() scheme otherwise it assumes it is an ID. A non-null
IDTypeVerifier
will always be
provided if a schema is associated with the document type.
The link
string argument is the "anchor" part of
the of the URL which is composed from the value of the link
property specified for the link element in the CSS.
public ElementLocator getElementLocator(IDTypeVerifier idVerifier, String link) { ElementLocator elementLocator = null; try { if(link.startsWith("element(")){ // xpointer element() scheme elementLocator = new XPointerElementLocator(idVerifier, link); } else { // Locate link element by ID elementLocator = new IDElementLocator(idVerifier, link); } } catch (ElementLocatorException e) { logger.warn("Exception when create element locator for link: " + link + ". Cause: " + e, e); } return elementLocator; }
The XPointerElementLocator
is an
implementation of the abstract class
ro.sync.ecss.extensions.api.link.ElementLocator
for links that have one of the following XPointer element()
scheme patterns:
elementID
)locate the element with the specified id
/1/2/3
)A child sequence appearing alone identifies an element by means of stepwise navigation, which is directed by a sequence of integers separated by slashes (/); each integer n locates the nth child element of the previously located element.
elementID/3/4
)A child sequence appearing after a
NCName
identifies an element by
means of stepwise navigation, starting from the
element located by the given name.
The constructor separates the id/integers which are delimited by slashes(/) into a sequence of identifiers (an XPointer path). It will also check that the link has one of the supported patterns of the XPointer element() scheme.
public XPointerElementLocator(IDTypeVerifier idVerifier, String link) throws ElementLocatorException { super(link); this.idVerifier = idVerifier; link = link.substring("element(".length(), link.length() - 1); StringTokenizer stringTokenizer = new StringTokenizer(link, "/", false); xpointerPath = new String[stringTokenizer.countTokens()]; int i = 0; while (stringTokenizer.hasMoreTokens()) { xpointerPath[i] = stringTokenizer.nextToken(); boolean invalidFormat = false; // Empty xpointer component is not supported if(xpointerPath[i].length() == 0){ invalidFormat = true; } if(i > 0){ try { Integer.parseInt(xpointerPath[i]); } catch (NumberFormatException e) { invalidFormat = true; } } if(invalidFormat){ throw new ElementLocatorException( "Only the element() scheme is supported when locating XPointer links." + "Supported formats: element(elementID), element(/1/2/3), element(elemID/2/3/4)."); } i++; } if(Character.isDigit(xpointerPath[0].charAt(0))){ // This is the case when xpointer have the following pattern /1/5/7 xpointerPathDepth = xpointerPath.length; } else { // This is the case when xpointer starts with an element ID xpointerPathDepth = -1; startWithElementID = true; } }
The method startElement
will be
invoked at the beginning of every element in the XML
document(even when the element is empty). The arguments it
takes are
uri
the namespace URI, or the empty string if the element has no namespace URI or if namespace processing is disabled
localName
the local name of the element
qName
the qualified name of the element
atts
the attributes attached to the element. If there are no attributes, it will be empty.
The method returns true
if the processed
element is found to be the one indicated by the link.
The XPointerElementLocator
implementation of the startElement
will update the depth of the current element and keep the
index of the element in its parent. If the
xpointerPath
starts with an element ID
then the current element ID is verified to match the
specified ID. If this is the case the depth of the XPointer
is updated taking account of the depth of the current
element.
If the XPointer path depth is the same as the current element depth then the kept indices of the current element path are compared to the indices in the XPointer path. If all of them match then the element has been found.
public boolean startElement(String uri, String localName, String name, Attr[] atts) { boolean linkLocated = false; // Increase current element document depth startElementDepth ++; if (endElementDepth != startElementDepth) { // The current element is the first child of the parent currentElementIndexStack.push(new Integer(1)); } else { // Another element in the parent element currentElementIndexStack.push(new Integer(lastIndexInParent + 1)); } if (startWithElementID) { // This the case when xpointer path starts with an element ID. String xpointerElement = xpointerPath[0]; for (int i = 0; i < atts.length; i++) { if(xpointerElement.equals(atts[i].getValue())){ if(idVerifier.hasIDType( localName, uri, atts[i].getQName(), atts[i].getNamespace())){ xpointerPathDepth = startElementDepth + xpointerPath.length - 1; break; } } } } if (xpointerPathDepth == startElementDepth){ // check if xpointer path matches with the current element path linkLocated = true; try { int xpointerIdx = xpointerPath.length - 1; int stackIdx = currentElementIndexStack.size() - 1; int stopIdx = startWithElementID ? 1 : 0; while (xpointerIdx >= stopIdx && stackIdx >= 0) { int xpointerIndex = Integer.parseInt(xpointerPath[xpointerIdx]); int currentElementIndex = ((Integer)currentElementIndexStack.get(stackIdx)).intValue(); if(xpointerIndex != currentElementIndex) { linkLocated = false; break; } xpointerIdx--; stackIdx--; } } catch (NumberFormatException e) { logger.warn(e,e); } } return linkLocated; }
The method endElement
will be
invoked at the end of every element in the XML document
(even when the element is empty).
The XPointerElementLocator
implementation of the endElement
updates the depth of the current element path and the index
of the element in its parent.
public void endElement(String uri, String localName, String name) { endElementDepth = startElementDepth; startElementDepth --; lastIndexInParent = ((Integer)currentElementIndexStack.pop()).intValue(); }
The IDElementLocator
is an
implementation of the abstract class
ro.sync.ecss.extensions.api.link.ElementLocator
for links that use an id.
The constructor only assigns field values and the method
endElement
is empty for this
implementation.
The method startElement
checks
each of the element's attribute values and when one matches
the link, it considers the element found if one of the
following conditions is satisfied:
the qualified name of the attribute is
xml:id
the attribute is of type ID
The type of the attribute is checked with the help of the
method
IDTypeVerifier.hasIDType
.
public boolean startElement(String uri, String localName, String name, Attr[] atts) { boolean elementFound = false; for (int i = 0; i < atts.length; i++) { if (link.equals(atts[i].getValue())) { if("xml:id".equals(atts[i].getQName())) { // xml:id attribute elementFound = true; } else { // check if attribute has ID type String attrLocalName = ExtensionUtil.getLocalName(atts[i].getQName()); String attrUri = atts[i].getNamespace(); if (idVerifier.hasIDType(localName, uri, attrLocalName, attrUri)) { elementFound = true; } } } } return elementFound; }