package nota.oxygen.common.links; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.InputStream; import java.net.URI; import java.net.URISyntaxException; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.swing.text.BadLocationException; import nota.oxygen.common.BaseAuthorOperation; import nota.oxygen.common.Utils; import org.apache.commons.validator.routines.EmailValidator; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.dom.ls.DOMImplementationLS; import org.w3c.dom.ls.LSInput; import org.w3c.dom.ls.LSParser; import ro.sync.ecss.extensions.api.ArgumentDescriptor; import ro.sync.ecss.extensions.api.ArgumentsMap; import ro.sync.ecss.extensions.api.AuthorDocumentController; import ro.sync.ecss.extensions.api.AuthorOperationException; import ro.sync.ecss.extensions.api.content.TextContentIterator; import ro.sync.ecss.extensions.api.content.TextContext; import ro.sync.ecss.extensions.api.node.AuthorElement; import ro.sync.ecss.extensions.api.node.AuthorNode; /** * Operation to find and markup hyperlinks, including external http and https links, internal # links and e-mail links * @author OHA * */ public class MarkupLinksOperation extends BaseAuthorOperation { private int startIndex = 0; private int getStartOffset() throws AuthorOperationException { AuthorNode start = getStartNode(); if (start==null) throw new AuthorOperationException("Found not find suitable start node"); return start.getStartOffset(); } protected AuthorNode getStartNode() throws AuthorOperationException { AuthorNode res = getAuthorAccess().getDocumentController().getAuthorDocumentNode(); AuthorNode[] candidates = getAuthorAccess().getDocumentController().findNodesByXPath(startNodeXPath, false, false, false); if (candidates.length>0) return candidates[0]; return res; } @Override public String getDescription() { return "Find and markup hyperlinks"; } private void markupLink(int startIndex, int endIndex) throws AuthorOperationException { getAuthorAccess().getEditorAccess().select(startIndex, endIndex); String text = getAuthorAccess().getEditorAccess().getSelectedText(); URI link = expandLink(text); Element linkElem = getLinkElement(); linkElem.setAttribute(refAttributeName, link.toString()); if (!externalAttributeName.equals("")) { if (isInternalLink(link)) { linkElem.setAttribute(externalAttributeName, "false"); } else { linkElem.setAttribute(externalAttributeName, "true"); } } getAuthorAccess().getDocumentController().surroundInFragment(Utils.serialize(linkElem), startIndex, endIndex); } @Override protected void doOperation() throws AuthorOperationException { int foundCount = 0; startIndex = getAuthorAccess().getEditorAccess().getCaretOffset(); if (startIndex<getStartOffset()) startIndex = getStartOffset(); int[] res = findNextOccurence(); while (res.length==2) { foundCount++; getAuthorAccess().getEditorAccess().select(res[0], res[1]); int ans = showYesNoCancelMessage(getDescription(), "Do you wish to mark up link '"+getAuthorAccess().getEditorAccess().getSelectedText()+"'?", 1); if (ans==1) { markupLink(res[0], res[1]); } else if (ans==-1) { break; } res = findNextOccurence(); } if (foundCount==0) showMessage(getDescription(), "Found no links to mark up"); } String[] domainList; private String[] getDomainList() throws AuthorOperationException { if (domainList == null) { Element root = getDomainListDocument().getDocumentElement(); if (root==null) { domainList = new String[0]; } else { NodeList domains = root.getElementsByTagName("domain"); domainList = new String[domains.getLength()]; for (int i=0; i<domainList.length; i++) { Node n = domains.item(i); Element d = (Element)n; Node suffix = d.getElementsByTagName("suffix").item(0); if (suffix==null) { domainList[i] = ""; } else { domainList[i] = suffix.getTextContent().toLowerCase(); } } } } return domainList; } private URI expandLink(URI link) throws AuthorOperationException { URI res; res = expandExternalLink(link); if (res!=null) return res; res = expandInternalLink(link); if (res!=null) return res; return expandMailLink(link); } private URI expandExternalLink(URI link) throws AuthorOperationException { String[] domains = getDomainList(); if (link.getScheme()==null) { try { link = new URI("http://"+link.toString()); } catch (URISyntaxException e) { return null; } if (!link.isAbsolute()) return null; if (link.getHost()==null) return null; if (link.getUserInfo()!=null) return null; for (int i=0; i<domains.length; i++) { if (link.getHost().toLowerCase().endsWith(domains[i])) return link; } return null; } else if (link.getScheme().equals("http") || link.getScheme().equals("https")) { return link; } return null; } private boolean isInternalLink(URI link) throws AuthorOperationException { return expandInternalLink(link)!=null; } private URI expandInternalLink(URI link) throws AuthorOperationException { if (link.getScheme()!=null) return null; if (link.getSchemeSpecificPart()!=null) return null; if (link.getFragment()==null) return null; return link; } private URI expandMailLink(URI link) throws AuthorOperationException { if (link.getScheme()==null) { try { link = new URI("mailto:"+link.toString()); } catch (URISyntaxException e) { return null; } } if (link.getScheme().equals("mailto")) { if (link.getFragment()!=null) return null; if (link.getSchemeSpecificPart()==null) return null; if (EmailValidator.getInstance(false).isValid(link.getSchemeSpecificPart())) return link; } return null; } private boolean isLink(String linkCandidate) throws AuthorOperationException { return expandLink(linkCandidate)!=null; } private URI expandLink(String linkCandidate) throws AuthorOperationException { if (linkCandidate==null) return null; URI link; try { link = new URI(linkCandidate); } catch (URISyntaxException e) { return null; } return expandLink(link); } private int[] findNextOccurence() throws AuthorOperationException { AuthorDocumentController docCtrl = getAuthorAccess().getDocumentController(); TextContentIterator itr = docCtrl.getTextContentIterator(startIndex, docCtrl.getAuthorDocumentNode().getEndOffset()); while (itr.hasNext()) { TextContext nextContext = itr.next(); try { AuthorElement elem = getElementAtOffset(nextContext.getTextStartOffset()); if (elem.getLocalName().equals(linkLocalName)) continue; } catch (BadLocationException e) { throw new AuthorOperationException(e.getMessage(), e); } String next = nextContext.getText().toString(); if (nextContext.getTextStartOffset()<startIndex) { next = next.substring(startIndex-nextContext.getTextStartOffset()); } Pattern pat = Pattern.compile("\\b\\w[\\w.;/?:@=&$-_+!*'(),]+\\w\\b"); Matcher mat = pat.matcher(next); while (mat.find()) { if (isLink(mat.group())) { startIndex = nextContext.getTextStartOffset()+mat.end(); return new int[] { nextContext.getTextStartOffset()+mat.start(), startIndex}; } } } return new int[] {}; } private Document domainListDocument; protected void loadDomainListDocument() throws AuthorOperationException { InputStream is; try { is = new FileInputStream(domainListFile); } catch (FileNotFoundException e) { throw new AuthorOperationException("Could not find domain list file "+domainListFile); } DOMImplementationLS impl = Utils.getDOMImplementationLS(); LSParser builder = impl.createLSParser(DOMImplementationLS.MODE_SYNCHRONOUS, null); LSInput input = impl.createLSInput(); input.setByteStream(is); input.setEncoding("UTF-8"); domainListDocument = builder.parse(input); } protected Document getDomainListDocument() throws AuthorOperationException { if (domainListDocument==null) {//Lazy loading of settings document loadDomainListDocument(); } return domainListDocument; } private Element getLinkElement() throws AuthorOperationException{ return Utils.deserializeElement(linkFragment); } @Override protected void parseArguments(ArgumentsMap args) throws IllegalArgumentException { linkFragment = (String)args.getArgumentValue(ARG_LINK_FRAGMENT); try { Element linkElem = Utils.deserializeElement(linkFragment); linkLocalName = linkElem.getLocalName(); } catch (AuthorOperationException e) { linkLocalName = ""; } refAttributeName = (String)args.getArgumentValue(ARG_REF_ATTRIBUTE_NAME); externalAttributeName = (String)args.getArgumentValue(ARG_EXTERNAL_ATTRIBUTE_NAME); if (externalAttributeName==null) externalAttributeName = ""; externalAttributeName = externalAttributeName.trim(); domainListFile = (String)args.getArgumentValue(ARG_DOMAIN_LIST_FILE); startNodeXPath = (String)args.getArgumentValue(ARG_START_NODE_XPATH); } private static String ARG_LINK_FRAGMENT = "link fragment"; private static String ARG_REF_ATTRIBUTE_NAME = "reference attribute name"; private static String ARG_EXTERNAL_ATTRIBUTE_NAME = "external attribute name - leave empty if not available"; private static String ARG_DOMAIN_LIST_FILE = "domain list file"; private static String ARG_START_NODE_XPATH = "start node xpath"; private String linkFragment; private String linkLocalName; private String refAttributeName; private String externalAttributeName; private String domainListFile; private String startNodeXPath; @Override public ArgumentDescriptor[] getArguments() { return new ArgumentDescriptor[] { new ArgumentDescriptor(ARG_LINK_FRAGMENT, ArgumentDescriptor.TYPE_FRAGMENT, "link xml fragment"), new ArgumentDescriptor(ARG_REF_ATTRIBUTE_NAME, ArgumentDescriptor.TYPE_STRING, "name of reference attribute (href)"), new ArgumentDescriptor(ARG_EXTERNAL_ATTRIBUTE_NAME, ArgumentDescriptor.TYPE_STRING, "name of external attribute (extenal)"), new ArgumentDescriptor(ARG_DOMAIN_LIST_FILE, ArgumentDescriptor.TYPE_STRING, "name of external attribute (external)"), new ArgumentDescriptor(ARG_START_NODE_XPATH, ArgumentDescriptor.TYPE_XPATH_EXPRESSION, "xpath statement specifying the node at which to start the search for links") }; } }