eu.erasmuswithoutpaper.registryclient.CatalogueDocument Maven / Gradle / Ivy
Show all versions of ewp-registry-client Show documentation
package eu.erasmuswithoutpaper.registryclient;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import javax.xml.namespace.NamespaceContext;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import eu.erasmuswithoutpaper.registryclient.CatalogueFetcher.Http200RegistryResponse;
import eu.erasmuswithoutpaper.registryclient.RegistryClient.InvalidApiEntryElement;
import eu.erasmuswithoutpaper.registryclient.RegistryClient.RegistryClientException;
import eu.erasmuswithoutpaper.registryclient.RegistryClient.StaleApiEntryElement;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Attr;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.TypeInfo;
import org.w3c.dom.UserDataHandler;
import org.xml.sax.SAXException;
/**
* Thread-safe (and mostly immutable) internal representation of the catalogue document.
*/
@SuppressFBWarnings("SIC_INNER_SHOULD_BE_STATIC_ANON")
class CatalogueDocument {
private static final Logger logger = LoggerFactory.getLogger(CatalogueDocument.class);
/**
* The underlying catalogue document.
*
*
* This field is final, but its data is still mutable (and thus, not thread-safe). Thus, clones
* need to be created whenever the elements of this document are exposed outside.
*
*/
private final Document doc;
/**
* This is the ETag we got along the retrieved catalogue document.
*/
private final String etag;
/**
* "SHA-256 -> heiIds" index for {@link #doc}.
*
*
* Client certificate's SHA-256 hex fingerprint is mapped to the set of all HEI IDs covered by
* this certificate (can be empty).
*
*
*
* This field is final, but its data is still mutable (and thus, not thread-safe). Unmodifiable
* views need to be used before its values are exposed outside.
*
*/
private final Map> certHeis;
private final Map> cliKeyHeis;
/**
* "Host Element -> heiIds" index for {@link #doc}.
*
*
* For each <host>
element in the catalogue, a set of HEI IDs covered by this
* host (can be empty).
*
*
*
* This field is final, but its data is still mutable (and thus, not thread-safe). Unmodifiable
* views need to be used before its values are exposed outside.
*
*/
private final Map> hostHeis;
/**
* "Host Element -> rsa-server-key fingerprints" index for {@link #doc}.
*
*
* For each <host>
element in the catalogue, a set of rsa-server-key
* fingerprints covering this host (can be empty).
*
*
*
* This field is final, but its data is still mutable (and thus, not thread-safe). Unmodifiable
* views need to be used before its values are exposed outside.
*
*/
private final Map> hostServerKeys;
/**
* "HEI other-id type -> other-id value -> heiId" index for {@link #doc}.
*
*
* We keep a separate map for each type
attribute of <other-id>
* elements present in the {@link #doc}. The keys of each such map contain all values present for
* the type, and the value contains a single HEI ID mapped for this value (if there are many HEI
* IDs mapped for this value (which should not happen in general) then a random one is stored
* here).
*
*
*
* This field is final, but its data is still mutable (and thus, not thread-safe). Unmodifiable
* views need to be used before its values are exposed outside.
*
*/
private final Map> heiIdMaps;
/**
* "heiId -> HeiEntry" index for {@link #doc}.
*
*
* This field is final, but its data is still mutable (and thus, not thread-safe). Unmodifiable
* views need to be used before its values are exposed outside.
*
*/
private final Map heiEntries;
/**
* "Unique API ID -> API entry elements" index for {@link #doc}.
*
*
* Unique API ID is constructed from both namespaceUri and localName of the API entry element (see
* {@link #getApiIndexKey(String, String)}). Each such ID is mapped to a list of all DOM
* {@link Element}s found under <apis-implemented>
element in the {@link #doc}.
*
*
*
* This field is final, but its data is still mutable (and thus, not thread-safe). Unmodifiable
* views need to be used before its values are exposed outside.
*
*/
private final Map> apiIndex;
/**
* "SHA-256 -> RSA public key" index for {@link #doc}.
*
*
* This map holds RSA public keys parsed from catalogue's binaries.
*
*/
private final HashMap keyBodies;
/**
* Indicates the time after which this copy of the catalogue should be considered stale. It is
* okay to serve stale copies for a while, but the client should schedule an "is it still
* up-to-date?" check.
*
*
* This field is mutable. It can be modified via {@link #extendExpiryDate(Date)}.
*
*/
private volatile Date expires;
/**
* Parse the response received from the Registry Service and create a new
* {@link CatalogueDocument} based on it.
*
* @param registryResponse The {@link Http200RegistryResponse} response received from the Registry
* Service.
* @throws CatalogueParserException if registryResponse did not contain a valid catalogue.
*/
CatalogueDocument(Http200RegistryResponse registryResponse) throws CatalogueParserException {
DocumentBuilder docBuilder = Utils.newSecureDocumentBuilder();
this.expires = registryResponse.getExpires();
if (this.expires == null) {
// It seems that the Registry didn't supply the "Expires" header.
// (In general, this shouldn't happen.)
logger.warn("Missing 'Expires' header in catalogue response. Will assume 5 minutes.");
this.expires = new Date((new Date().getTime()) + 1000 * 60 * 5);
}
this.etag = registryResponse.getETag();
// Parse it.
try {
this.doc = docBuilder.parse(new ByteArrayInputStream(registryResponse.getContent()));
} catch (SAXException e) {
throw new CatalogueParserException("Problem parsing the catalogue response", e);
} catch (IOException e) {
throw new RuntimeException(e);
}
// Run a basic validation. (Just a sanity check. No detailed validation is necessary.)
Element root = this.doc.getDocumentElement();
if (root.getNamespaceURI() == null
|| (!root.getNamespaceURI().equals(RegistryClient.REGISTRY_CATALOGUE_V1_NAMESPACE_URI))) {
throw new CatalogueParserException("Catalogue namespace URI mismatch.");
}
if (!root.getLocalName().equals("catalogue")) {
throw new CatalogueParserException("Catalogue localName mismatch.");
}
// Prepare dependencies for traversal.
XPathFactory xpathfactory = XPathFactory.newInstance();
XPath xpath = xpathfactory.newXPath();
xpath.setNamespaceContext(new NamespaceContext() {
@Override
public String getNamespaceURI(String prefix) {
if ("r".equals(prefix)) {
return RegistryClient.REGISTRY_CATALOGUE_V1_NAMESPACE_URI;
}
throw new IllegalArgumentException(prefix);
}
@Override
public String getPrefix(String namespaceUri) {
throw new UnsupportedOperationException();
}
@Override
public Iterator getPrefixes(String namespaceUri) {
throw new UnsupportedOperationException();
}
});
this.certHeis = new HashMap<>();
this.cliKeyHeis = new HashMap<>();
this.hostHeis = new HashMap<>();
this.hostServerKeys = new HashMap<>();
this.heiIdMaps = new HashMap<>();
this.apiIndex = new HashMap<>();
this.heiEntries = new HashMap<>();
this.keyBodies = new HashMap<>();
// Create indexes.
try {
List extends Element> elements = Utils.asElementList((NodeList) xpath.evaluate(
"r:host", root, XPathConstants.NODESET));
for (Element hostElem : elements) {
List extends Node> children = Utils.asNodeList(hostElem.getChildNodes());
Node institutionsCovered = null;
Node clientCredentials = null;
Node serverCredentials = null;
for (Node child : children) {
if ("institutions-covered".equals(child.getLocalName())) {
institutionsCovered = child;
} else if ("client-credentials-in-use".equals(child.getLocalName())) {
clientCredentials = child;
} else if ("server-credentials-in-use".equals(child.getLocalName())) {
serverCredentials = child;
}
}
Set coveredHeis = new HashSet<>();
if (institutionsCovered != null) {
List extends Node> heiNodes = Utils.asNodeList(institutionsCovered.getChildNodes());
for (Node heiIdNode : heiNodes) {
if ("hei-id".equals(heiIdNode.getLocalName())) {
coveredHeis.add(heiIdNode.getTextContent());
}
}
}
Set heis = new HashSet<>();
this.hostHeis.put(hostElem, heis);
heis.addAll(coveredHeis);
Set keys = new HashSet<>();
this.hostServerKeys.put(hostElem, keys);
if (clientCredentials != null) {
List extends Node> credentialNodes =
Utils.asNodeList(clientCredentials.getChildNodes());
for (Node credential : credentialNodes) {
if ("certificate".equals(credential.getLocalName())) {
String fingerprint =
credential.getAttributes().getNamedItem("sha-256").getTextContent();
Set coveredCertHeis;
if (this.certHeis.containsKey(fingerprint)) {
coveredCertHeis = this.certHeis.get(fingerprint);
} else {
coveredCertHeis = new HashSet<>();
this.certHeis.put(fingerprint, coveredCertHeis);
}
coveredCertHeis.addAll(coveredHeis);
} else if ("rsa-public-key".equals(credential.getLocalName())) {
String fingerprint =
credential.getAttributes().getNamedItem("sha-256").getTextContent();
Set coveredKeyHeis;
if (this.cliKeyHeis.containsKey(fingerprint)) {
coveredKeyHeis = this.cliKeyHeis.get(fingerprint);
} else {
coveredKeyHeis = new HashSet<>();
this.cliKeyHeis.put(fingerprint, coveredKeyHeis);
}
coveredKeyHeis.addAll(coveredHeis);
}
}
}
if (serverCredentials != null) {
List extends Node> credentialNodes =
Utils.asNodeList(serverCredentials.getChildNodes());
for (Node credential : credentialNodes) {
if ("rsa-public-key".equals(credential.getLocalName())) {
String fingerprint =
credential.getAttributes().getNamedItem("sha-256").getTextContent();
keys.add(fingerprint);
}
}
}
}
List extends Element> otherIdElems = Utils.asElementList((NodeList) xpath
.evaluate("r:institutions/r:hei/r:other-id", root, XPathConstants.NODESET));
for (Element otherIdElem : otherIdElems) {
String type = otherIdElem.getAttribute("type");
String value = otherIdElem.getTextContent();
String heiId = ((Element) otherIdElem.getParentNode()).getAttribute("id");
Map mapForType = this.heiIdMaps.get(type);
if (mapForType == null) {
mapForType = new HashMap<>();
this.heiIdMaps.put(type, mapForType);
}
mapForType.put(getCanonicalId(value), heiId);
}
List extends Element> heiElems = Utils.asElementList(
(NodeList) xpath.evaluate("r:institutions/r:hei", root, XPathConstants.NODESET));
for (Element heiElem : heiElems) {
String id = heiElem.getAttribute("id");
HeiEntry hei = new HeiEntryImpl(id, heiElem);
this.heiEntries.put(id, hei);
}
List extends Element> apiElems = Utils.asElementList(
(NodeList) xpath.evaluate("r:host/r:apis-implemented/*", root, XPathConstants.NODESET));
for (Element apiElem : apiElems) {
// newApiIndex's keys uniquely identify API's namespaceURI and localName.
String key = getApiIndexKey(apiElem.getNamespaceURI(), apiElem.getLocalName());
List entries = this.apiIndex.get(key);
if (entries == null) {
entries = new ArrayList<>();
this.apiIndex.put(key, entries);
}
// entries - the list of all API entry elements for this key.
entries.add(apiElem);
}
KeyFactory rsaKeyFactory;
try {
rsaKeyFactory = KeyFactory.getInstance("RSA");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
List extends Element> keyElems = Utils.asElementList(
(NodeList) xpath.evaluate("r:binaries/r:rsa-public-key", root, XPathConstants.NODESET));
for (Element keyElem : keyElems) {
String fingerprint = keyElem.getAttribute("sha-256");
byte[] data = Base64.getMimeDecoder().decode(keyElem.getTextContent());
X509EncodedKeySpec spec = new X509EncodedKeySpec(data);
RSAPublicKey value;
try {
value = (RSAPublicKey) rsaKeyFactory.generatePublic(spec);
this.keyBodies.put(fingerprint, value);
} catch (InvalidKeySpecException | ClassCastException e) {
if (logger.isWarnEnabled()) {
logger.warn("Could not load object " + fingerprint + " as RSAPublicKey: " + e);
}
}
}
} catch (XPathExpressionException e) {
throw new RuntimeException(e);
}
}
private static String getApiIndexKey(String namespaceUri, String localName) {
return "{" + namespaceUri + "}" + localName;
}
/**
* Convert <other-id>
value to its canonical form.
*/
private static String getCanonicalId(String value) {
return value.trim().toLowerCase(Locale.ENGLISH);
}
/**
* Check if first version string matches the "minimum required" version string in the second
* argument.
*
*
* Both strings MUST be in thr "X.Y.Z" format, where X, Y and Z are non-negative integers (a
* subset of semantic versioning strings). If this requirement is not met, this method will not
* attempt to compare the strings, and it will simply return false
.
*
*
*
* ("1.6.0", "1.10.0") == false
,
* ("1.10.0", "1.6.0") == true
,
* ("1.6.0", "1.6.0") == true
,
* ("1.10", "1.6.0") == false
(because the first one is invalid),
* ("1.10.0", "1.6.0x") == false
(because the second one is invalid).
*
*
* @param apiVersion string of 3 ordinal numbers separated by dots.
* @param minRequiredVersion string of 3 ordinal numbers separated by dots.
* @return true if both arguments are valid version strings, and the first one is equal or
* greater than the second one.
*/
static boolean doesVersionXMatchMinimumRequiredVersionY(String apiVersion,
String minRequiredVersion) {
String[] s1 = apiVersion.split("\\.");
String[] s2 = minRequiredVersion.split("\\.");
if (s1.length != 3 || s2.length != 3) {
return false;
}
int[] i1 = new int[3];
int[] i2 = new int[3];
try {
for (int i = 0; i < 3; i++) {
i1[i] = Integer.parseInt(s1[i]);
i2[i] = Integer.parseInt(s2[i]);
}
} catch (NumberFormatException e) {
return false;
}
for (int i = 0; i < 3; i++) {
if (i1[i] > i2[i]) {
return true;
} else if (i1[i] < i2[i]) {
return false;
}
}
return true;
}
public boolean isApiCoveredByServerKey(Element apiElement, RSAPublicKey serverKey)
throws InvalidApiEntryElement {
return this.extractFingerprintsForApiElement(apiElement)
.contains(Utils.extractFingerprint(serverKey));
}
@Override
public String toString() {
return "CatalogueDocument[ETag=" + this.getETag() + ", Expires=" + this.getExpiryDate() + "]";
}
private boolean doesElementMatchConditions(Element elem, ApiSearchConditions conds) {
if (conds.getRequiredNamespaceUri() != null
&& (!conds.getRequiredNamespaceUri().equals(elem.getNamespaceURI()))) {
return false;
}
if (conds.getRequiredLocalName() != null
&& (!conds.getRequiredLocalName().equals(elem.getLocalName()))) {
return false;
}
if (conds.getRequiredMinVersion() != null) {
String attrVer = elem.getAttribute("version");
if (attrVer.isEmpty()) {
return false;
}
if (!doesVersionXMatchMinimumRequiredVersionY(attrVer, conds.getRequiredMinVersion())) {
return false;
}
}
if (conds.getRequiredHei() != null) {
Element hostElem = (Element) elem.getParentNode().getParentNode();
Set heis = this.hostHeis.get(hostElem);
if (heis == null) {
return false;
}
return heis.contains(conds.getRequiredHei());
}
return true;
}
private Set extractFingerprintsForApiElement(Element apiElement) {
// Extract the meta object, which we store in the ApiEntryElement wrapper.
if (!(apiElement instanceof ApiEntryElement)) {
throw new InvalidApiEntryElement();
}
InternalApiEntryAttachment meta = ((ApiEntryElement) apiElement).internalApiEntryAttachment;
// Verify if the meta object is not yet stale. We want to force the clients
// to NOT cache these apiElements.
if (meta.isStale()) {
logger.warn("Stale apiElements in use. Possible memory leaks. See: "
+ "https://github.com/erasmus-without-paper/ewp-registry-client/issues/8");
throw new StaleApiEntryElement();
}
// Use the CatalogueDocument from the meta object, instead of "this".
// This will fix the issue of clients getting wrong results, but may cause
// more memory leaks, if clients cache apiElements somewhere.
if (!meta.catalogueDocument.hostServerKeys.containsKey(meta.host)) {
// The host of this API doesn't have *any* server keys.
return new HashSet<>();
}
return meta.catalogueDocument.hostServerKeys.get(meta.host);
}
/**
* Extend the expiry date of the document.
*
*
* Expiry date can be moved into the future once it is confirmed that this version of the document
* (as identified by its {@link #getETag()}) is still up-to-date.
*
*
* @param newExpiryDate The new expiry date. It needs to be after the previously used one,
* otherwise it won't be changed.
*/
synchronized void extendExpiryDate(Date newExpiryDate) {
if (newExpiryDate.after(this.expires)) {
this.expires = new Date(newExpiryDate.getTime());
}
}
/**
* This implements {@link RegistryClient#findApi(ApiSearchConditions)}, but only for this
* particular version of the catalogue document.
*/
Element findApi(ApiSearchConditions conditions) {
Element bestChoice = null;
for (Element entry : this.findApis(conditions)) {
if (bestChoice == null) {
bestChoice = entry;
} else if (bestChoice.getAttribute("version").length() == 0) {
bestChoice = entry;
} else {
String currentBest = bestChoice.getAttribute("version");
String newCandidate = entry.getAttribute("version");
if (doesVersionXMatchMinimumRequiredVersionY(newCandidate, currentBest)) {
bestChoice = entry;
}
}
}
return bestChoice;
}
/**
* This implements {@link RegistryClient#findApis(ApiSearchConditions)}, but only for this
* particular version of the catalogue document.
*/
Collection findApis(ApiSearchConditions conditions) {
// First, determine the minimum set of elements we need to look through.
List> lookupBase = this.getApiLookupBase(conditions);
// Then, iterate through all the elements and filter the ones that match.
List results = new ArrayList<>();
for (List lst : lookupBase) {
for (Element elem : lst) {
if (this.doesElementMatchConditions(elem, conditions)) {
// Create a copy of the element, so that it's thread-safe.
Element clone = (Element) elem.cloneNode(true);
ApiEntryElement apiEntryElement = new ApiEntryElement(clone,
new InternalApiEntryAttachment(this, (Element) elem.getParentNode().getParentNode()));
results.add(apiEntryElement);
}
}
}
return results;
}
/**
* This implements {@link RegistryClient#findHei(String)}, but only for this particular version of
* the catalogue document.
*/
HeiEntry findHei(String id) {
return this.heiEntries.get(id);
}
/**
* This implements {@link RegistryClient#findHei(String, String)}, but only for this particular
* version of the catalogue document.
*/
HeiEntry findHei(String type, String value) {
String heiId = this.findHeiId(type, value);
if (heiId == null) {
return null;
}
return this.findHei(heiId);
}
/**
* This implements {@link RegistryClient#findHeiId(String, String)}, but only for this particular
* version of the catalogue document.
*/
String findHeiId(String type, String value) {
value = getCanonicalId(value);
Map mapForType = this.heiIdMaps.get(type);
if (mapForType == null) {
return null;
}
// It's thread-safe (Strings are immutable).
return mapForType.get(value);
}
/**
* This implements {@link RegistryClient#findHeis(ApiSearchConditions)}, but only for this
* particular version of the catalogue document.
*/
Collection findHeis(ApiSearchConditions conditions) {
// First, determine the minimum set of elements we need to look through.
List> lookupBase = this.getApiLookupBase(conditions);
// Then, find all elements which include the matched APIs.
Set hostElems = new HashSet<>();
for (List lst : lookupBase) {
for (Element apiElem : lst) {
if (this.doesElementMatchConditions(apiElem, conditions)) {
Element hostElem = (Element) apiElem.getParentNode().getParentNode();
hostElems.add(hostElem);
}
}
}
// Finally, collect the unique HEI entries covered by these hosts.
Set results = new HashSet<>();
for (Element hostElem : hostElems) {
Set heiIds = this.hostHeis.get(hostElem);
for (String heiId : heiIds) {
HeiEntry hei = this.heiEntries.get(heiId);
if (hei == null) {
// Should not happen, but just in case.
continue;
}
results.add(hei);
}
}
return results;
}
RSAPublicKey findRsaPublicKey(String fingerprint) {
return this.keyBodies.get(fingerprint);
}
/**
* This implements {@link RegistryClient#getAllHeis()}, but only for this particular version of
* the catalogue document.
*/
Collection getAllHeis() {
return Collections.unmodifiableCollection(this.heiEntries.values());
}
List> getApiLookupBase(ApiSearchConditions conditions) {
List> lookupBase = new ArrayList<>();
if (conditions.getRequiredNamespaceUri() != null && conditions.getRequiredLocalName() != null) {
// We can make use of our namespaceUri+localName index in this case.
List match = this.apiIndex.get(
getApiIndexKey(conditions.getRequiredNamespaceUri(), conditions.getRequiredLocalName()));
if (match != null) {
lookupBase.add(match);
}
} else {
// We do not have such an index. We'll need to browse through all entries.
lookupBase.addAll(this.apiIndex.values());
}
return lookupBase;
}
/**
* @return ETag of this document.
*/
String getETag() {
return this.etag;
}
/**
* @return expiry date of this document (it can change in time, see
* {@link #extendExpiryDate(Date)}).
*/
Date getExpiryDate() {
return new Date(this.expires.getTime());
}
/**
* This implements {@link RegistryClient#getHeisCoveredByCertificate(Certificate)}, but only for
* this particular version of the catalogue document.
*/
Collection getHeisCoveredByCertificate(Certificate clientCert) {
String fingerprint = Utils.extractFingerprint(clientCert);
Set heis = this.certHeis.get(fingerprint);
if (heis == null) {
heis = new HashSet<>();
}
return Collections.unmodifiableSet(heis);
}
Collection getHeisCoveredByClientKey(RSAPublicKey clientKey) {
String fingerprint = Utils.extractFingerprint(clientKey);
Set heis = this.cliKeyHeis.get(fingerprint);
if (heis == null) {
heis = new HashSet<>();
}
return Collections.unmodifiableSet(heis);
}
RSAPublicKey getServerKeyCoveringApi(Element apiElement) {
for (String fingerprint : this.extractFingerprintsForApiElement(apiElement)) {
RSAPublicKey key = this.findRsaPublicKey(fingerprint);
if (key != null) {
return key;
} else {
logger.warn("Catalogue contains a reference to a non-existent key {}"
+ ". We will ignore this reference, but this shouldn't happen and should be "
+ "investigated.", fingerprint);
}
}
return null;
}
Collection getServerKeysCoveringApi(Element apiElement) {
List result = new ArrayList<>();
for (String fingerprint : this.extractFingerprintsForApiElement(apiElement)) {
RSAPublicKey key = this.findRsaPublicKey(fingerprint);
if (key != null) {
result.add(key);
} else {
logger.warn("Catalogue contains a reference to a non-existent key {}"
+ ". We will ignore this reference, but this shouldn't happen and should be "
+ "investigated.", fingerprint);
}
}
return result;
}
/**
* This implements {@link RegistryClient#isCertificateKnown(Certificate)}, but only for this
* particular version of the catalogue document.
*/
boolean isCertificateKnown(Certificate clientCert) {
String fingerprint = Utils.extractFingerprint(clientCert);
return this.certHeis.containsKey(fingerprint);
}
boolean isClientKeyKnown(RSAPublicKey clientKey) {
String fingerprint = Utils.extractFingerprint(clientKey);
return this.cliKeyHeis.containsKey(fingerprint);
}
/**
* Instances of this class get attached to the Elements returned by
* {@link CatalogueDocument#findApis(ApiSearchConditions)} and
* {@link CatalogueDocument#findApi(ApiSearchConditions)} methods.
*/
private static class InternalApiEntryAttachment {
/**
* The parent {@link CatalogueDocument}, from which this API entry originated from.
*/
private final CatalogueDocument catalogueDocument;
/**
* The parent <host>
element which this API entry has been cloned from.
*/
private final Element host;
/**
* The time when this object was created, in the Date.getTime format (number of milliseconds
* since January 1, 1970, 00:00:00 GMT).
*/
private final long created;
private InternalApiEntryAttachment(CatalogueDocument catalogueDocument, Element host) {
this.catalogueDocument = catalogueDocument;
this.host = host;
this.created = new Date().getTime();
}
public boolean isStale() {
long now = new Date().getTime();
long diff = now - this.created;
return diff > 60000; // one minute
}
}
static class CatalogueParserException extends RegistryClientException {
private static final long serialVersionUID = 8536812850006770409L;
CatalogueParserException(String message) {
super(message);
}
CatalogueParserException(String message, Exception cause) {
super(message, cause);
}
}
private static class ApiEntryElement implements Element {
private final Element element;
private final InternalApiEntryAttachment internalApiEntryAttachment;
public ApiEntryElement(Element element, InternalApiEntryAttachment internalApiEntryAttachment) {
this.element = element;
this.internalApiEntryAttachment = internalApiEntryAttachment;
}
@Override
public String getTagName() {
return element.getTagName();
}
@Override
public String getAttribute(String name) {
return element.getAttribute(name);
}
@Override
public void setAttribute(String name, String value) throws DOMException {
element.setAttribute(name, value);
}
@Override
public void removeAttribute(String name) throws DOMException {
element.removeAttribute(name);
}
@Override
public Attr getAttributeNode(String name) {
return element.getAttributeNode(name);
}
@Override
public Attr setAttributeNode(Attr newAttr) throws DOMException {
return element.setAttributeNode(newAttr);
}
@Override
public Attr removeAttributeNode(Attr oldAttr) throws DOMException {
return element.removeAttributeNode(oldAttr);
}
@Override
public NodeList getElementsByTagName(String name) {
return element.getElementsByTagName(name);
}
@Override
public String getAttributeNS(String namespaceUri, String localName) throws DOMException {
return element.getAttributeNS(namespaceUri, localName);
}
@Override
public void setAttributeNS(String namespaceUri, String qualifiedName, String value)
throws DOMException {
element.setAttributeNS(namespaceUri, qualifiedName, value);
}
@Override
public void removeAttributeNS(String namespaceUri, String localName) throws DOMException {
element.removeAttributeNS(namespaceUri, localName);
}
@Override
public Attr getAttributeNodeNS(String namespaceUri, String localName) throws DOMException {
return element.getAttributeNodeNS(namespaceUri, localName);
}
@Override
public Attr setAttributeNodeNS(Attr newAttr) throws DOMException {
return element.setAttributeNodeNS(newAttr);
}
@Override
public NodeList getElementsByTagNameNS(String namespaceUri, String localName)
throws DOMException {
return element.getElementsByTagNameNS(namespaceUri, localName);
}
@Override
public boolean hasAttribute(String name) {
return element.hasAttribute(name);
}
@Override
public boolean hasAttributeNS(String namespaceUri, String localName) throws DOMException {
return element.hasAttributeNS(namespaceUri, localName);
}
@Override
public TypeInfo getSchemaTypeInfo() {
return element.getSchemaTypeInfo();
}
@Override
public void setIdAttribute(String name, boolean isId) throws DOMException {
element.setIdAttribute(name, isId);
}
@Override
public void setIdAttributeNS(String namespaceUri, String localName, boolean isId)
throws DOMException {
element.setIdAttributeNS(namespaceUri, localName, isId);
}
@Override
public void setIdAttributeNode(Attr idAttr, boolean isId) throws DOMException {
element.setIdAttributeNode(idAttr, isId);
}
@Override
public String getNodeName() {
return element.getNodeName();
}
@Override
public String getNodeValue() throws DOMException {
return element.getNodeValue();
}
@Override
public void setNodeValue(String nodeValue) throws DOMException {
element.setNodeValue(nodeValue);
}
@Override
public short getNodeType() {
return element.getNodeType();
}
@Override
public Node getParentNode() {
return element.getParentNode();
}
@Override
public NodeList getChildNodes() {
synchronized (element.getOwnerDocument()) {
return element.getChildNodes();
}
}
@Override
public Node getFirstChild() {
return element.getFirstChild();
}
@Override
public Node getLastChild() {
return element.getLastChild();
}
@Override
public Node getPreviousSibling() {
return element.getPreviousSibling();
}
@Override
public Node getNextSibling() {
return element.getNextSibling();
}
@Override
public NamedNodeMap getAttributes() {
return element.getAttributes();
}
@Override
public Document getOwnerDocument() {
return element.getOwnerDocument();
}
@Override
public Node insertBefore(Node newChild, Node refChild) throws DOMException {
return element.insertBefore(newChild, refChild);
}
@Override
public Node replaceChild(Node newChild, Node oldChild) throws DOMException {
return element.replaceChild(newChild, oldChild);
}
@Override
public Node removeChild(Node oldChild) throws DOMException {
return element.removeChild(oldChild);
}
@Override
public Node appendChild(Node newChild) throws DOMException {
return element.appendChild(newChild);
}
@Override
public boolean hasChildNodes() {
return element.hasChildNodes();
}
@Override
public Node cloneNode(boolean deep) {
return element.cloneNode(deep);
}
@Override
public void normalize() {
element.normalize();
}
@Override
public boolean isSupported(String feature, String version) {
return element.isSupported(feature, version);
}
@Override
public String getNamespaceURI() {
return element.getNamespaceURI();
}
@Override
public String getPrefix() {
return element.getPrefix();
}
@Override
public void setPrefix(String prefix) throws DOMException {
element.setPrefix(prefix);
}
@Override
public String getLocalName() {
return element.getLocalName();
}
@Override
public boolean hasAttributes() {
return element.hasAttributes();
}
@Override
public String getBaseURI() {
return element.getBaseURI();
}
@Override
public short compareDocumentPosition(Node other) throws DOMException {
return element.compareDocumentPosition(other);
}
@Override
public String getTextContent() throws DOMException {
return element.getTextContent();
}
@Override
public void setTextContent(String textContent) throws DOMException {
element.setTextContent(textContent);
}
@Override
public boolean isSameNode(Node other) {
return element.isSameNode(other);
}
@Override
public String lookupPrefix(String namespaceUri) {
return element.lookupPrefix(namespaceUri);
}
@Override
public boolean isDefaultNamespace(String namespaceUri) {
return element.isDefaultNamespace(namespaceUri);
}
@Override
public String lookupNamespaceURI(String prefix) {
return element.lookupNamespaceURI(prefix);
}
@Override
public boolean isEqualNode(Node arg) {
return element.isEqualNode(arg);
}
@Override
public Object getFeature(String feature, String version) {
return element.getFeature(feature, version);
}
@Override
public Object setUserData(String key, Object data, UserDataHandler handler) {
return element.setUserData(key, data, handler);
}
@Override
public Object getUserData(String key) {
return element.getUserData(key);
}
}
}