All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.globalmentor.swing.text.xml.XMLLinkController Maven / Gradle / Ivy

The newest version!
/*
 * Copyright © 1996-2009 GlobalMentor, Inc. 
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.globalmentor.swing.text.xml;

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.net.*;

import javax.swing.*;
import javax.swing.event.*;
import javax.swing.text.*;

import com.globalmentor.java.*;
import com.globalmentor.log.Log;
import com.globalmentor.net.*;
import com.globalmentor.oebps.spec.OEB;
import com.globalmentor.swing.event.XMLLinkEvent;
import com.globalmentor.w3c.spec.XLink;

import static com.globalmentor.w3c.spec.HTML.*;
import static com.globalmentor.xml.xhtml.XHTML.*;

/**
 * Class to watch the associated component and fire hyperlink events on it when appropriate. This class interprets links according to the XLink specification,
 * but allows other link controllers, which may recognize links based on other criteria, to be registered for other namespaces.
 * 

* If an XML element is encountered in a particular namespace and a link controller has been registered for that namespace, the registered link controller will * be used. Otherwise, this link controller will be used to detect links. *

*

* To add linking functionality for other namespaces, a child class should override {@link #isLinkElement(Element)} and {@link #getLinkElementHRef(Element)}. * @author Garret Wilson * @see #isLinkElement(Element) * @see #getLinkElementHRef(Element) */ public class XMLLinkController extends MouseAdapter implements MouseMotionListener, Serializable { /** Default constructor. */ public XMLLinkController() { } /** * The element the cursor is currently over; used to know when to generate enter and exit hyperlink events. */ private Element currentElement = null; /** * The URI the element currently represents; used to know when to generate enter and exit hyperlink events. */ private URI currentURI = null; /** This is used by viewToModel to avoid allocing a new array each time. */ private Position.Bias[] bias = new Position.Bias[1]; /** * Called when a mouse click occurs. If the component is read-only, then the clicked event is used to generate a hyperlink event if the clicked area is a * link. * @param mouseEvent The mouse event. * @see MouseListener#mouseClicked */ public void mouseClicked(MouseEvent mouseEvent) { final JEditorPane editorPane = (JEditorPane)mouseEvent.getSource(); //get the source of the event if(!editorPane.isEditable() && !mouseEvent.isPopupTrigger()) { //if the editor pane is read-only, and this isn't the popup trigger final Point point = new Point(mouseEvent.getX(), mouseEvent.getY()); //create a point from the mouse click coordinates final int pos = editorPane.viewToModel(point); //get the position of the mouse click //TODO del Debug.notify("Mouse clicked on position: "+pos); //TODO fix if(pos >= 0) { //if we found a valid position in the document activateLink(pos, editorPane, mouseEvent.getX(), mouseEvent.getY()); //try activate a link at that position } } } /** * Called when the mouse is dragged. This version ignores mouse dragging. * @param mouseEvent The mouse event. */ public void mouseDragged(MouseEvent mouseEvent) { } /** * Called when the mouse is moved. * @param mouseEvent The mouse event. */ public void mouseMoved(final MouseEvent mouseEvent) { //TODO del Log.trace("Inside XMLEditorKit.LinkController.mouseMoved()"); //TODO del final JEditorPane editorPane = (JEditorPane)mouseEvent.getSource(); //get the source of the event final XMLEditorKit editorKit = (XMLEditorKit)editorPane.getEditorKit(); //get the editor kit for the editor pane boolean adjustCursor = true; //we'll assume we'll adjust the cursor unless we're still over the same element Cursor newCursor = editorKit.getDefaultCursor(); //get the default cursor; if we determine we're over a link, we'll change this to the link cursor if(!editorPane.isEditable()) { //if the editor pane isn't editable (it's being used for browsing) final Point point = new Point(mouseEvent.getX(), mouseEvent.getY()); //create a new point object with the position of the mouse //TODO testing final int pos = editorPane.viewToModel(point); //get the position in the model of the mouse movement /*TODO fix; see why this functions slightly differently than editorPane.viewToModel() int pos=editorPane.getUI().viewToModel(editorPane, point, bias); //get the position in the model of the mouse click if(bias[0]==Position.Bias.Backward && pos>0) pos--; */ if(pos >= 0) { //if we found a valid position in the document final Document document = editorPane.getDocument(); //get a reference to the model's document if(document instanceof com.globalmentor.swing.text.xml.XMLDocument) { //if this is an XML document XMLDocument xmlDocument = (com.globalmentor.swing.text.xml.XMLDocument)document; //cast the document to an XMLDocument Element element = xmlDocument.getCharacterElement(pos); //get the element for this position if(currentElement != element) { //if we've moved to a different element Log.trace("We've moved to a different element; currentURI: ", currentURI); //TODO del currentElement = element; //show that we have a new current element while(element != null) { //we'll keep looking up the chain for a link element until we find one or run out of elements final XMLLinkController linkController = getLinkController(element); //get a link controller for this element if(linkController.isLinkElement(element)) { //if this is a link element //TODO del when works final String href=getLinkElementHRef(element); //get the href for the element we're over (we don't know if this is a link element, so this may return null) try { final URI uri = linkController.getLinkElementURI(xmlDocument, element); //get the URI for the element we're over (we don't know if this is a link element, so this may return null) Log.trace("URI: ", uri); //TODO del //TODO del when works if(url!=currentURL) //if we're over a different link if(!Objects.equals(uri, currentURI)) { //if we're over a different link (comparing the URIs using the URI.equals() method, if possible) //TODO del when works final AttributeSet attributeSet=element.getAttributes(); //get the attributes of this element TODO what if this is null? fireEntryExitEvents(editorPane, xmlDocument, currentURI, uri, element); //fire the appropriate events for exiting and entering a link currentURI = uri; //update which link we're over if(uri != null) //if we're now over a link newCursor = editorKit.getLinkCursor(xmlDocument, uri); //we'll show the appropriate link cursor } else { //if we're still over the same link adjustCursor = false; //don't adjust the cursor } } catch(URISyntaxException uriSyntaxException) { //if the URI could not be formed Log.warn(uriSyntaxException); //continue normally TODO should this be something just under a warning, since this is a user-caused error? } break; //stop looking for links up the hierarchy; we just found one } else { //if this isn't a link element element = element.getParentElement(); //check the parent element } } if(element == null) { //if we were unable to find a link element fireEntryExitEvents(editorPane, xmlDocument, currentURI, null, null); //fire the appropriate events for exiting and entering a link currentURI = null; //show that we don't currently have a URI } } else //if we're not over a different element adjustCursor = false; //don't adjust the cursor } } //if we should adjust the cursor and we're really changing cursors if(adjustCursor && editorPane.getCursor() != newCursor) editorPane.setCursor(newCursor); //update the cursor } } /** * Calls linkActivated on the associated JEditorPane if the given position represents a link. This method walks its way up the * element hierarchy to find any enclosing link element, if the element at the given position does not represent a link. If this was the result of a mouse * click, x and y will give the location of the mouse, otherwise they will be (<0). * @param pos The position in the model. * @param editorPane Tthe editor pane that contains the document. * @param x The horizontal position of the mouse, if this is the result of a mouse click, or (<0). * @param y The vertical position of the mouse, if this is the result of a mouse click, or (<0). * @return true if a link was successfully found and activated, resulting in a hyperlink event being fired. */ protected boolean activateLink(final int pos, final JEditorPane editorPane, final int x, final int y) { //TODO fix super.activateLink(pos, editorPane, x, y); //TODO testing //TODO del Log.trace("Inside OEBEditorKit.activateLink()"); final Document document = editorPane.getDocument(); //get the document in the editor pane if(document instanceof XMLDocument) { //if this is an XML document //TODO del Log.trace("OEBEditorKit.activeLink() is an OEBDocument"); XMLDocument xmlDocument = (XMLDocument)document; //cast the document to an XML document Element element = xmlDocument.getCharacterElement(pos); //get the element this position represents //TODO del when works final String elementName=(String)attributeSet.getAttribute(StyleConstants.NameAttribute); //get the name of this element //TODO del Log.trace("OEBEditorKit.activeLink() is on element: "+(elementName!=null ? elementName : "null")); //TODO del Log.trace("element: "+element.toString()); //TODO testing while(element != null) { //we'll keep looking up the chain for a link element until we find one or run out of elements final XMLLinkController linkController = getLinkController(element); //get a link controller for this element if(linkController.isLinkElement(element)) { //if the link controller says this is a link element //TODO del when not needed final AttributeSet attributeSet=element.getAttributes(); //get the attributes of this element TODO make sure we really need this //TODO fix protected String getLinkElementHRef(final Element element) //TODO del when not needed final String href=getLinkElementHRef(element); //get the href value //TODO del when not needed final URL url=getLinkElementURL(xmlDocument, element); //get the link URL //TODO del Debug.notify("OEBEditorKit.activeLink() found href: "+href); //ask the link controller to create a hyperlink event final HyperlinkEvent linkEvent = linkController.createHyperlinkEvent(editorPane, xmlDocument, /*TODO del if not needed url, */element); //TODO fix for usemap if(linkEvent != null) { //if a hyperlink event was successfully created TODO doesn't this always succeed? /*TODO del //because it's likely the target won't be this same element and may not even be a link currentElement=null; //show that we're no longer on the old element currentURL=null; //show that we're no longer using the old URL fireEntryExitEvents(editorPane, xmlDocument, currentURL, null, element); //fire the appropriate events for exiting the link final XMLEditorKit editorKit=(XMLEditorKit)editorPane.getEditorKit(); //get the editor kit for the editor pane //adjust the cursor back to the default if(editorPane.getCursor()!=editorKit.getDefaultCursor()) //if the editor pane's cursor isn't the editor kit's default editorPane.setCursor(editorKit.getDefaultCursor()); //set the cursor back to the default */ editorPane.fireHyperlinkUpdate(linkEvent); //let the editor pane fire the hyperlink event return true; //show that we activated a link } break; //stop looking for links up the hierarchy; we just found one } else //if this isn't a link element element = element.getParentElement(); //check the parent element } } return false; //show that we were not successful in finding a link and activating it } /** * Determines whether the specified element represents a link. This version makes its determination based upon the rules of XLink. Other versions may override * this function to do their own determinations. * @param element The element in question. * @return true if the specified element represents a link. */ protected boolean isLinkElement(final Element element) { final AttributeSet attributeSet = element.getAttributes(); //get the attributes of this element final String xlinkType = XMLStyles.getXMLAttributeValue(attributeSet, XLink.XLINK_NAMESPACE_URI.toString(), XLink.ATTRIBUTE_TYPE); //get the xlink:type of this element (if there is one) if(xlinkType != null) //if this is an XLink element return true; //show that this is a link element //TODO for now, until we support default attributes in the DTD, accept a link that only has an xlink:href //TODO do we even need this anymore? del this and make xlink:type mandatory like the specification says //TODO del else if XMLStyleUtilities.getXMLAttributeValue(attributeSet, XLinkConstants.XLINK_NAMESPACE_URI.toString(), XLinkConstants.ATTRIBUTE_HREF); //if there is an xlink:href //TODO del else if(attributeSet.getAttribute(XLinkConstants.XLINK_HREF)!=null) //TODO del return true; //TODO del when default attributes are supported in the DTD else //if this is another element return false; //show that this element doesn't have the xlink:type attribute } /** * Gets the link href of the specified element if the specified element represents a link. This version finds the href based upon the rules of XLink. Other * versions may override this function to return values from other attributes. * @param element The element which may be a link element. * @return The href of the link, or null if the element does not represent a link or if the element's href is not present. */ protected String getLinkElementHRef(final Element element) { final AttributeSet attributeSet = element.getAttributes(); //get the attributes of this element final String xlinkType = XMLStyles.getXMLAttributeValue(attributeSet, XLink.XLINK_NAMESPACE_URI.toString(), XLink.ATTRIBUTE_TYPE); //get the xlink:type of this element (if there is one) //TODO bring back when the DTD supports default attributes if(xlinkType!=null) //if this is an XLink element //TODO use getDefinedAttribute(), so as not to search up the hierarchy return XMLStyles.getXMLAttributeValue(attributeSet, XLink.XLINK_NAMESPACE_URI.toString(), XLink.ATTRIBUTE_HREF); //return the xlink:href of this element (if there is one) //TODO bring back when the DTD supports default attributes return null; //show that this isn't an XLink element, because it didn't have an "xlink:type" attribute } /** * Gets the full URI specified element if the specified element represents a link. The href is first calculated and then a URI is created from the base URI * specified by the element or one of its ancestors. If a base URI cannot be found in the element hierarchy, the document is asked for that value. * @param xmlDocument The XML document which contains the element. * @param element The element which may be a link element. * @return The URI of the link, or null if the element does not represent a link or if the element's href is not present. * @throws URISyntaxException Thrown if the element's href and/or the base URI do not allow a valid URI to be constructed. * @see #getLinkElementHRef(Element) * @see XMLDocument#getBaseURI() * @see XMLStyles#getBaseURI(AttributeSet) */ protected URI getLinkElementURI(final XMLDocument xmlDocument, final Element element) throws URISyntaxException { final String href = getLinkElementHRef(element); //get the href of the element if(href != null) { //if there is an href URI baseURI = XMLStyles.getBaseURI(element.getAttributes()); //get the base URI of the document if(baseURI == null) //if we couldn't found a base URI in the attributes baseURI = xmlDocument.getBaseURI(); //get the base URI from the document //TODO make sure this works for fragments return URIs.createURI(baseURI, href); //convert the href into a full URI, correctly processing URI fragments beginning with "#" } return null; //show that we could not find an href for the element } /** * Calls linkActivated on the associated JEditorPane if the given position represents a link. *

* This is implemented to forward to the method with the same name, but with the following args both == -1. * * @param pos the position * @param html the editor pane */ /*TODO fix protected void activateLink(int pos, JEditorPane editor) { activateLink(pos, editor, -1, -1); } */ /** * Calls linkActivated on the associated JEditorPane if the given position represents a link. If this was the result of a mouse click, x and * y will give the location of the mouse, otherwise they will be < 0. * * @param pos the position * @param html the editor pane */ /*TODO del when not needed protected void activateLink(int pos, JEditorPane editorPane, int x, int y) { //TODO del Log.trace("Inside XMLEditorKit.activateLink()"); return; //TODO fix for XLink } */ /*TODO fix final Document document=editorPane.getDocument(); //get the document in the editor pane if(document instanceof XMLDocument) { //if this is an XML document XMLDocument xmlDocument=(XMLDocument)document; //cast the document to an XML document Element element=XMLDocument.getCharacterElement(pos); //get the element this position represents AttributeSet attributeSet=element.getAttributes(); //get the attributes of this element AttributeSet anchor = (AttributeSet) a.getAttribute(HTML.Tag.A); String href = (anchor != null) ? (String) anchor.getAttribute(HTML.Attribute.HREF) : null; HyperlinkEvent linkEvent = null; if (href != null) { linkEvent = createHyperlinkEvent(html, hdoc, href, anchor); } else if (x >= 0 && y >= 0) { // Check for usemap. Object useMap = a.getAttribute(HTML.Attribute.USEMAP); if (useMap != null && (useMap instanceof String)) { Map m = hdoc.getMap((String)useMap); if (m != null) { Rectangle bounds; try { bounds = html.modelToView(pos); Rectangle rBounds = html.modelToView(pos + 1); if (bounds != null && rBounds != null) { bounds.union(rBounds); } } catch (BadLocationException ble) { bounds = null; } if (bounds != null) { AttributeSet area = m.getArea (x - bounds.x, y - bounds.y, bounds.width, bounds.height); if (area != null) { href = (String)area.getAttribute (HTML.Attribute.HREF); if (href != null) { linkEvent = createHyperlinkEvent(html, hdoc, href, anchor); } } } } } } if (linkEvent != null) { html.fireHyperlinkUpdate(linkEvent); } } */ /** * Creates and returns a new instance of a hyperlink event. * @param editorPane The editor pane for which the event belongs and will be fired. * @param xmlDocument The document in which the link lies. //TODO fix @param eventType //TODO del if not needed @param href The hypertext reference. * @param element The link element. * @return The new hyperlink event. */ protected HyperlinkEvent createHyperlinkEvent(final JEditorPane editorPane, final XMLDocument xmlDocument, /*TODO fix final EventType eventType,*//*TODO del if not needed String href, */ final Element element) //TODO fix so that fireEntryExitEvents() can call this { //TODO del Log.trace("Inside XMLEditorKit.createHyperlinkEvent() with href of: "+href); //TODO del URI uri; //we'll try get a full URI from the href String description; //we'll store a description of the URI here, or the message we get if we fail to form a URI try { uri = getLinkElementURI(xmlDocument, element); //get the link URI from the element description = uri.toString(); //use the URI as the description //TODO fix; this can throw a null pointer exception } catch(URISyntaxException uriSyntaxException) { //if there are any problems creating a URI uri = null; //don't use the URI description = getLinkElementHRef(element); //store the href that resulted in the invalid URI } //TODO change this later to be an XLinkEvent //create a hyperlink event with the specified URI and href final HyperlinkEvent linkEvent = new XMLLinkEvent(editorPane, HyperlinkEvent.EventType.ACTIVATED, uri, description); return linkEvent; //return the link we created } /** * Fires the appropriate events for entering and exiting a link. If there is record of being over an event before, an exit event is fired. If * href is not null, an entry event will be fired. * @param editorPane The editor pane for which the event belongs and will be fired. * @param xmlDocument The document in which the links lie. * @param currentURI The URI the element currently represents; used to know when to generate exit hyperlink events. * @param newURI The new link URI, or null if no entry event should be fired. * @param element The link element. */ protected void fireEntryExitEvents(final JEditorPane editorPane, final XMLDocument xmlDocument, final URI currentURI, final URI newURI, final Element element) { //TODO del Log.trace("XMLEditorKit.fireEntryExitEvents() with href of: "+href); //TODO del if(currentURI != null) { //if we were over a link before //create a hyperlink event to represent exiting a hyperlink TODO eventually use the common hyperlink event factory final HyperlinkEvent exitEvent = new XMLLinkEvent(editorPane, HyperlinkEvent.EventType.EXITED, currentURI, currentURI.toString()); //TODO del Log.trace("ready to fire exit event: "+exitEvent); //TODO del editorPane.fireHyperlinkUpdate(exitEvent); //fire the exit event } if(newURI != null) { //if we're over a new link //TODO fix final HyperlinkEvent linkEvent=new HyperlinkEvent(editorPane, HyperlinkEvent.EventType.ACTIVATED, uri, description); //create a hyperlink event representing entering a hyperlink TODO eventually use the common hyperlink event factory final HyperlinkEvent enteredEvent = new XMLLinkEvent(editorPane, HyperlinkEvent.EventType.ENTERED, newURI, newURI.toString()); editorPane.fireHyperlinkUpdate(enteredEvent); //fire the enter event } } /** * Retrieves a link controller for the given element by looking up any registered link controllers for the element's namespace. * @param element The element for which a link controller should be returned. * @return A link controller for the given element. */ protected XMLLinkController getLinkController(final Element element) { XMLLinkController linkController = null; //we'll determine a link controller (which may be this class itself, if nothing else is registered) final AttributeSet attributeSet = element.getAttributes(); //get the element's attribute set if(attributeSet != null) { //if we have an attribute set //final a registered XML link controller to which to delegate if we can /*TODO fix final */String elementNamespaceURI = XMLStyles.getXMLElementNamespaceURI(attributeSet); //get the namespace of this element, if it has one //TODO del Log.trace("Looking for view factory for namespace: ", elementNamespaceURI); //TODO del if(elementNamespaceURI == null) { //if this element has no namespace TODO this code is duplicated from XMLViewFactory---combine somehow final ContentType mediaType = XMLStyles.getMediaType(attributeSet); //see if this element's document has a media type defined if(mediaType != null) { //if there is a media type defined for this element's document //TODO probably do all this differently later, like registering a view factory with a media type or something or, better yet, registering a namespace with a media type if(ContentType.TEXT_PRIMARY_TYPE.equals(mediaType.getPrimaryType()) && OEB.X_OEB1_DOCUMENT_SUBTYPE.equals(mediaType.getSubType())) elementNamespaceURI = OEB.OEB1_DOCUMENT_NAMESPACE_URI.toString(); //TODO testing else if(isHTML(mediaType)) elementNamespaceURI = XHTML_NAMESPACE_URI.toString(); //TODO testing } } //TODO del Log.trace("Decided namespace is really: ", elementNamespaceURI); //TODO del linkController = getLinkController(elementNamespaceURI); //see if a link controller has been registered for this namespace (which may be null) } if(linkController == null) //if we couldn't find a link controller linkController = this; //use ourselves as a link controller return linkController; //return whatever link controller we found } /** * Retreives a link controller for the given namespace. This version defaults to returning this link controller. A child class can override this method to * return a link controller specifically for a given namespace. * @param namespaceURI The namespace URI of the element for which a link controller should be returned, or null if the element has no namespace. * @return A view factory for the given namespace. */ protected XMLLinkController getLinkController(final String namespaceURI) { return this; //default to just using this link controller } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy