/*
* Copyright 2014 michael-simons.eu.
*
* 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 ac.simons.autolinker;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.nodes.Entities.EscapeMode;
import org.jsoup.nodes.Node;
import org.jsoup.nodes.TextNode;
import org.jsoup.parser.Parser;
/**
* @author Michael J. Simons, 2014-12-27
*/
public class AutoLinkService {
private final List<AutoLinker> autolinkers;
public AutoLinkService(List<AutoLinker> autolinkers) {
this.autolinkers = autolinkers;
}
/**
* Applies an autolinker to a given element (or a document, which is an
* element itself) and creates a new element from the nodes the autolinker
* returns.
*
* @param element The element whos text nodes should be autolinked
* @param autoLinker
* @return An optional processed element. The optional will be empty if
* {@code element} is an anchor
*/
Element applyAutoLinker(final AutoLinker autoLinker, final Element element) {
final List<Node> newChildNodes = element.childNodes().stream()
.collect(ArrayList::new,
(l, childNode) -> {
// Child node is itself an element with children
if (childNode instanceof Element) {
final Element childElement = (Element) childNode;
if (childElement.tagName().equals("a")) {
l.add(childElement);
} else {
l.add(applyAutoLinker(autoLinker, childElement));
}
} // Only TextNodes may have possible urls
else if (childNode instanceof TextNode) {
l.addAll(autoLinker.createLinks(((TextNode) childNode)));
} // Other nodes are just kept
else {
l.add(childNode);
}
},
ArrayList::addAll
);
final Element rv = new Element(element.tag(), element.baseUri(), element.attributes());
newChildNodes.forEach(rv::appendChild);
return rv;
}
/**
* @see #addLinks(java.lang.String, java.util.Optional, java.lang.Class)
* @param textWithLinkableStuff The text that contains possible urls
* @param baseUrl Base url for creating absolute urls from relative urls
* @return The text with all recognizable URLs turned into links
*/
public String addLinks(final String textWithLinkableStuff, final Optional<String> baseUrl) {
return addLinks(textWithLinkableStuff, baseUrl, String.class);
}
/**
* Looks through a text with linkable stuff and applies all configured
* {@link AutoLinker} to this text.
*
* @param <T> Type of the resulting document with embedded links
* @param textWithLinkableStuff A list that may contain urls and such
* @param baseUrl An optional base url for resolving relative urls
* @param targetClass Class of the generated document
* @return A new text with urls turned into anchor tags.
*/
public <T> T addLinks(final String textWithLinkableStuff, final Optional<String> baseUrl, Class<? extends T> targetClass) {
T rv;
if (String.class.isAssignableFrom(targetClass)) {
rv = (T) textWithLinkableStuff;
} else if (Document.class.isAssignableFrom(targetClass)) {
rv = (T) Document.createShell(baseUrl.orElse(""));
} else {
throw new RuntimeException(String.format("Invalid target class: %s", targetClass.getName()));
}
if (!(textWithLinkableStuff == null || textWithLinkableStuff.trim().isEmpty())) {
// Create a document
final Document document = addLinks(Jsoup.parseBodyFragment(textWithLinkableStuff, baseUrl.orElse("")));
// As the linkables do their work in place, use the document as return
if(Document.class.isAssignableFrom(targetClass)) {
rv = (T) document;
} else {
document
.outputSettings()
.prettyPrint(false)
.escapeMode(EscapeMode.xhtml)
.charset(StandardCharsets.UTF_8);
rv = (T) Parser.unescapeEntities(document.body().html().trim(), true);
}
}
return rv;
}
/**
* A convenience method for adding links in an existing document.
*
* @see #addLinks(java.lang.String, java.util.Optional, java.lang.Class)
* @param document Existing document, will be modified
* @return Modified document with autolinked urls
*/
public Document addLinks(final Document document) {
// Let each linkable process the document
autolinkers.forEach((autoLinker) -> {
final Element body = document.body();
body.replaceWith(applyAutoLinker(autoLinker, body));
});
return document;
}
}