/************************************************************************* * * * This file is part of the 20n/act project. * * 20n/act enables DNA prediction for synthetic biology/bioengineering. * * Copyright (C) 2017 20n Labs, Inc. * * * * Please direct all queries to act@20n.com. * * * * This program is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see <http://www.gnu.org/licenses/>. * * * *************************************************************************/ package com.twentyn.patentExtractor; import com.fasterxml.jackson.annotation.JsonProperty; import org.apache.commons.io.input.ReaderInputStream; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.jsoup.Jsoup; import org.jsoup.nodes.TextNode; import org.jsoup.select.NodeTraversor; import org.jsoup.select.NodeVisitor; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.TransformerException; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpression; import javax.xml.xpath.XPathExpressionException; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.StringReader; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.regex.Pattern; import java.util.zip.GZIPInputStream; /** * This class represents parts of a USPTO patent document that are relevant to 20's use cases. It can extract * information from the USPTO's XML documents and convert it to a POJO that can then be serialized as JSON. Use this * as the basis for any processing of patent text. */ public class PatentDocument { public static final Logger LOGGER = LogManager.getLogger(PatentDocument.class); // See http://www.uspto.gov/learning-and-resources/xml-resources. public static final String DTD2014 = "v4.5 2014-04-03"; public static final String DTD2013 = "v4.4 2013-05-16"; public static final String DTD2012 = "v4.3 2012-12-04"; public static final String DTD2006 = "v4.2 2006-08-23"; public static final String DTD2005 = "v4.1 2005-08-25"; public static final String DTD2004 = "v40 2004-12-02"; public static final String DTD2014_APP = "v4.4 2014-04-03"; public static final String DTD2012_APP = "v4.3 2012-12-04"; public static final String DTD2006_APP = "v4.2 2006-08-23"; public static final String DTD2005_APP = "v4.1 2005-08-25"; public static final String DTD2004_APP = "v4.0 2004-12-02"; public static final String PATH_DTD_VERSION = "/us-patent-grant/@dtd-version"; public static final String PATH_DTD_VERSION_APP = "/us-patent-application/@dtd-version"; public static final String[] PATHS_TEXT = { "//description", "//invention-title", "//abstract", }; public static final String PATH_CLAIMS = "//claims"; public static final String PATH_KEY_FILE_ID = "fileId", PATH_KEY_TITLE = "title", PATH_KEY_DATE = "date", PATH_KEY_MAIN_CLASSIFICATION = "classification", PATH_KEY_FURTHER_CLASSIFICATIONS = "further_classifications", PATH_KEY_SEARCHED_CLASSIFICATIONS = "referenced_classifications"; // TODO: is there a type-safe way of building an object from XPath with a map of functions? public static final HashMap<String, String> PATHS_2013 = new HashMap<String, String>() {{ put(PATH_KEY_FILE_ID, "/us-patent-grant/@file"); put(PATH_KEY_TITLE, "/us-patent-grant/us-bibliographic-data-grant/invention-title"); put(PATH_KEY_DATE, "/us-patent-grant/@date-publ"); put(PATH_KEY_MAIN_CLASSIFICATION, "/us-patent-grant/us-bibliographic-data-grant/classification-national/main-classification/text()"); put(PATH_KEY_FURTHER_CLASSIFICATIONS, "/us-patent-grant/us-bibliographic-data-grant/classification-national/further-classification"); put(PATH_KEY_SEARCHED_CLASSIFICATIONS, "/us-patent-grant/us-bibliographic-data-grant/us-field-of-classification-search/classification-national[./country/text()='US']/main-classification"); }}; public static final HashMap<String, String> PATHS_2004 = new HashMap<String, String>() {{ put(PATH_KEY_FILE_ID, "/us-patent-grant/@file"); put(PATH_KEY_TITLE, "/us-patent-grant/us-bibliographic-data-grant/invention-title"); put(PATH_KEY_DATE, "/us-patent-grant/@date-publ"); put(PATH_KEY_MAIN_CLASSIFICATION, "/us-patent-grant/us-bibliographic-data-grant/classification-national/main-classification/text()"); put(PATH_KEY_FURTHER_CLASSIFICATIONS, "/us-patent-grant/us-bibliographic-data-grant/classification-national/further-classification"); put(PATH_KEY_SEARCHED_CLASSIFICATIONS, "/us-patent-grant/us-bibliographic-data-grant/field-of-search/classification-national[./country/text()='US']/main-classification"); }}; public static final HashMap<String, String> PATHS_2014_APP = new HashMap<String, String>() {{ put(PATH_KEY_FILE_ID, "/us-patent-application/@file"); put(PATH_KEY_TITLE, "/us-patent-application/us-bibliographic-data-application/invention-title"); put(PATH_KEY_DATE, "/us-patent-application/@date-publ"); put(PATH_KEY_MAIN_CLASSIFICATION, "/us-patent-application/us-bibliographic-data-application/classification-national/main-classification/text()"); put(PATH_KEY_FURTHER_CLASSIFICATIONS, "/us-patent-application/us-bibliographic-data-application/classification-national/further-classification"); put(PATH_KEY_SEARCHED_CLASSIFICATIONS, // Note: doesn't exist, but left for ease of use. "/us-patent-application/us-bibliographic-data-application/us-field-of-classification-search/classification-national[./country/text()='US']/main-classification"); }}; public static final HashMap<String, HashMap<String, String>> VERSION_MAP = new HashMap<String, HashMap<String, String>>() {{ put(DTD2014, PATHS_2013); // All the 2013 paths work with the 2014 DTD. put(DTD2013, PATHS_2013); put(DTD2012, PATHS_2013); // All the 2013 paths work with the 2012 DTD. put(DTD2006, PATHS_2013); // All the 2013 paths work with the 2006 DTD. put(DTD2005, PATHS_2013); // All the 2013 paths work with the 2005 DTD. put(DTD2004, PATHS_2004); put(DTD2014_APP, PATHS_2014_APP); put(DTD2012_APP, PATHS_2014_APP); // All the 2014 app paths work with the 2012 app DTD. put(DTD2006_APP, PATHS_2014_APP); // All the 2014 app paths work with the 2006 app DTD. put(DTD2005_APP, PATHS_2014_APP); // All the 2014 app paths work with the 2005 app DTD, though the classifications might be different. put(DTD2004_APP, PATHS_2014_APP); // All the 2014 app paths work with the 2005 app DTD assuming searched classifications are always empty. }}; private static final Pattern GZIP_PATTERN = Pattern.compile("\\.gz$"); public static class HtmlVisitor implements NodeVisitor { // Based on https://github.com/jhy/jsoup/blob/master/src/main/java/org/jsoup/examples/HtmlToPlainText.java private static final HashSet<String> SEGMENTING_NODES = new HashSet<String>() {{ addAll(Arrays.asList( "p", "h1", "h2", "h3", "h4", "h5", "h6", "dt", "dd", "tr", "li", "body", "div", // HTML entities "row", "claim" // patent-specific entities )); }}; private static final Pattern SPACE_PATTERN = Pattern.compile("^\\s+$"); private StringBuilder segmentBuilder = new StringBuilder(); private List<String> textSegments = new LinkedList<>(); @Override public void head(org.jsoup.nodes.Node node, int i) { // This borrows a page from HtmlToPlainText's book. if (node instanceof TextNode) { String text = ((TextNode) node).text(); if (text != null && text.length() > 0) { segmentBuilder.append(((TextNode) node).text()); } } } @Override public void tail(org.jsoup.nodes.Node node, int i) { String nodeName = node.nodeName(); if (nodeName.equals("a")) { // Same as Jsoup's HtmlToPlainText. segmentBuilder.append(String.format(" <%s>", node.absUrl("href"))); } else if (SEGMENTING_NODES.contains(nodeName) && segmentBuilder.length() > 0) { String segmentText = segmentBuilder.toString(); // Ignore blank lines, as we'll be tagging each line separately. if (!SPACE_PATTERN.matcher(segmentText).matches()) { this.textSegments.add(segmentText); } // TODO: is it better to drop the old one than clear the existing? segmentBuilder.setLength(0); } } public List<String> getTextContent() { return this.textSegments; } } private static List<String> extractTextFromHTML(DocumentBuilder docBuilder, NodeList textNodes) throws ParserConfigurationException, TransformerConfigurationException, TransformerException, XPathExpressionException { List<String> allTextList = new ArrayList<>(0); if (textNodes != null) { for (int i = 0; i < textNodes.getLength(); i++) { Node n = textNodes.item(i); /* This extremely around-the-horn approach to handling text content is due to the mix of HTML and * XML in the patent body. We use Jsoup to parse the HTML entities we find in the body, and use * its extremely convenient NodeVisitor API to recursively traverse the document and extract the * text content in reasonable chunks. */ Document contentsDoc = Util.nodeToDocument(docBuilder, "body", n); String docText = Util.documentToString(contentsDoc); // With help from http://stackoverflow.com/questions/832620/stripping-html-tags-in-java org.jsoup.nodes.Document htmlDoc = Jsoup.parse(docText); HtmlVisitor visitor = new HtmlVisitor(); NodeTraversor traversor = new NodeTraversor(visitor); traversor.traverse(htmlDoc); List<String> textSegments = visitor.getTextContent(); allTextList.addAll(textSegments); } } return allTextList; } /** * Extracts the text content from text fields in a patent XML document. * * @param docBuilder A document builder to use when constructing intermediate XML/HTML documents in the extraction * process. * @param paths A list of XPath paths from which to exactract text. * @param xpath An XPath instance to use when running XPath queries. * @param doc The XML document from which to extract text. * @return A list of strings representing the textual content of the document. These could be sentences, * paragraphs, or larger text units, but should represent some sort of structure in the document's text. * @throws ParserConfigurationException * @throws TransformerConfigurationException * @throws TransformerException * @throws XPathExpressionException */ private static List<String> getRelevantDocumentText(DocumentBuilder docBuilder, String[] paths, XPath xpath, Document doc) throws ParserConfigurationException, TransformerConfigurationException, TransformerException, XPathExpressionException { List<String> allTextList = new ArrayList<>(0); for (String path : paths) { XPathExpression exp = xpath.compile(path); NodeList textNodes = (NodeList) exp.evaluate(doc, XPathConstants.NODESET); allTextList.addAll(extractTextFromHTML(docBuilder, textNodes)); } return allTextList; } /** * Converts an XML file into a patent document object, extracting relevant fields from the patent XML. * * @param inputPath A path to the file to be read. * @return A patent object if the XML can be read, or null otherwise. * @throws IOException Thrown on file I/O errors. * @throws ParserConfigurationException Thrown when the XML parser cannot be configured correctly. * @throws SAXException Thrown on XML parser errors. * @throws XPathExpressionException Thrown when XPath fails to handle queries against the specified document. */ // TODO: logging? // TODO: are @nullable and @non-null annotations still a thing? // TODO: prolly belongs in a factory. public static PatentDocument patentDocumentFromXMLFile(File inputPath) throws IOException, ParserConfigurationException, SAXException, TransformerConfigurationException, TransformerException, XPathExpressionException { InputStream iStream = null; iStream = new BufferedInputStream(new FileInputStream(inputPath)); if (GZIP_PATTERN.matcher(inputPath.getName()).find()) { iStream = new GZIPInputStream(iStream); } return patentDocumentFromXMLStream(iStream); } /** * Converts a string of XML into a patent document object, extracting relevant fields from the patent XML. * * @param text The XML string to parse and extract. * @return A patent object if the XML can be read, or null otherwise. * @throws IOException * @throws ParserConfigurationException * @throws SAXException * @throws TransformerConfigurationException * @throws TransformerException * @throws XPathExpressionException */ public static PatentDocument patentDocumentFromXMLString(String text) throws IOException, ParserConfigurationException, SAXException, TransformerConfigurationException, TransformerException, XPathExpressionException { StringReader stringReader = new StringReader(text); return patentDocumentFromXMLStream(new ReaderInputStream(stringReader)); } public static PatentDocument patentDocumentFromXMLStream(InputStream iStream) throws IOException, ParserConfigurationException, SAXException, TransformerConfigurationException, TransformerException, XPathExpressionException { // Create XPath objects for validating that this document is actually a patent. XPath xpath = Util.getXPathFactory().newXPath(); XPathExpression versionXPath = xpath.compile(PATH_DTD_VERSION); XPathExpression versionXPathApp = xpath.compile(PATH_DTD_VERSION_APP); DocumentBuilderFactory docFactory = Util.mkDocBuilderFactory(); DocumentBuilder docBuilder = docFactory.newDocumentBuilder(); Document doc = docBuilder.parse(iStream); Util.DocumentType docType = Util.identifyDocType(doc); if (docType != Util.DocumentType.PATENT && docType != Util.DocumentType.APPLICATION) { LOGGER.warn("Found unexpected document type: " + docType); return null; } boolean isApplication = docType == Util.DocumentType.APPLICATION; // Yes this is in fact the way suggested by the XPath API. String version; if (!isApplication) { version = (String) versionXPath.evaluate(doc, XPathConstants.STRING); } else { version = (String) versionXPathApp.evaluate(doc, XPathConstants.STRING); } if (version == null || !VERSION_MAP.containsKey(version)) { LOGGER.warn(String.format("Unrecognized patent DTD version: %s", version)); return null; } HashMap<String, String> paths = VERSION_MAP.get(version); /* Create XPath objects for extracting the fields of interest based on the version information. * TODO: extract these into some sharable, thread-safe place, maybe via dependency injection. */ XPathExpression idXPath = xpath.compile(paths.get(PATH_KEY_FILE_ID)); XPathExpression dateXPath = xpath.compile(paths.get(PATH_KEY_DATE)); XPathExpression titleXPath = xpath.compile(paths.get(PATH_KEY_TITLE)); XPathExpression classificationXPath = xpath.compile(paths.get(PATH_KEY_MAIN_CLASSIFICATION)); XPathExpression furtherClassificationsXPath = xpath.compile(paths.get(PATH_KEY_FURTHER_CLASSIFICATIONS)); XPathExpression searchedClassificationsXPath = xpath.compile(paths.get(PATH_KEY_SEARCHED_CLASSIFICATIONS)); String fileId = (String) idXPath.evaluate(doc, XPathConstants.STRING); String date = (String) dateXPath.evaluate(doc, XPathConstants.STRING); NodeList titleNodes = (NodeList) titleXPath.evaluate(doc, XPathConstants.NODESET); String title = StringUtils.join(" ", extractTextFromHTML(docBuilder, titleNodes)); String classification = (String) classificationXPath.evaluate(doc, XPathConstants.STRING); NodeList furtherClassificationNodes = (NodeList) furtherClassificationsXPath.evaluate(doc, XPathConstants.NODESET); ArrayList<String> furtherClassifications = null; if (furtherClassificationNodes != null) { furtherClassifications = new ArrayList<>(furtherClassificationNodes.getLength()); for (int i = 0; i < furtherClassificationNodes.getLength(); i++) { Node n = furtherClassificationNodes.item(i); String txt = n.getTextContent(); if (txt != null) { furtherClassifications.add(i, txt); } } } else { furtherClassifications = new ArrayList<>(0); } NodeList otherClassificationNodes = (NodeList) searchedClassificationsXPath.evaluate(doc, XPathConstants.NODESET); ArrayList<String> otherClassifications = null; if (otherClassificationNodes != null) { otherClassifications = new ArrayList<>(otherClassificationNodes.getLength()); for (int i = 0; i < otherClassificationNodes.getLength(); i++) { Node n = otherClassificationNodes.item(i); String txt = n.getTextContent(); if (txt != null) { otherClassifications.add(i, txt); } } } else { otherClassifications = new ArrayList<>(0); } // Extract text content for salient document paths. List<String> allTextList = getRelevantDocumentText(docBuilder, PATHS_TEXT, xpath, doc); List<String> claimsTextList = getRelevantDocumentText(docBuilder, new String[]{PATH_CLAIMS}, xpath, doc); return new PatentDocument(fileId, date, title, classification, furtherClassifications, otherClassifications, allTextList, claimsTextList, isApplication); } @JsonProperty("file_id") protected String fileId; @JsonProperty("grant_date") protected String grantDate; @JsonProperty("title") protected String title; @JsonProperty("primary_classification") protected String mainClassification; @JsonProperty("further_classification") protected List<String> furtherClassifications; @JsonProperty("searched_classifications") protected List<String> searchedClassifications; @JsonProperty("text_content") protected List<String> textContent; @JsonProperty("claims") protected List<String> claimsText; @JsonProperty("isApplication") protected Boolean isApplication; // TODO: this could probably use a builder if it gets more complicated. protected PatentDocument(String fileId, String grantDate, String title, String mainClassification, List<String> furtherClassifications, List<String> searchedClassifications, List<String> textContent, List<String> claimsText, Boolean isApplication) { this.fileId = fileId; this.grantDate = grantDate; this.title = title; this.mainClassification = mainClassification; this.furtherClassifications = furtherClassifications; this.searchedClassifications = searchedClassifications; this.textContent = textContent; this.claimsText = claimsText; this.isApplication = isApplication; } public String getFileId() { return fileId; } public String getGrantDate() { return grantDate; } public String getTitle() { return title; } public String getMainClassification() { return mainClassification; } public List<String> getFurtherClassifications() { return furtherClassifications; } public List<String> getSearchedClassifications() { return searchedClassifications; } public List<String> getTextContent() { return textContent; } public List<String> getClaimsText() { return claimsText; } public Boolean getIsApplication() { return isApplication; } }