Skip to content

Commit

Permalink
New methods for searching and retrieving HEI attributes
Browse files Browse the repository at this point in the history
Fix #1
  • Loading branch information
wrygiel committed Oct 7, 2016
1 parent 27dfe65 commit 6e6eb04
Show file tree
Hide file tree
Showing 9 changed files with 449 additions and 21 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@ Release notes
Unreleased
----------

* `RegistryClient` interface (and its implementation) has been extended with
new methods for searching and retrieving HEI attributes (as requested
[here](https://github.com/erasmus-without-paper/ewp-registry-client/issues/1)).

The following methods were added (details in javadocs):

- `HeiEntry findHei(String id)`
- `HeiEntry findHei(String type, String value)`
- `Collection<HeiEntry> findHeis(ApiSearchConditions conditions)`
- `Collection<HeiEntry> getAllHeis()`

* New `HeiEntry` interface was added.

* New `setApiClassRequired(namespaceUri, localName, version)` method in
`ApiSearchConditions` class. This is just a shorthand which allows you to
call `setApiClassRequired(namespaceUri, localName)` and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,16 @@ static boolean doesVersionXMatchMinimumRequiredVersionY(String apiVersion,
*/
private final Map<String, Map<String, String>> heiIdMaps;

/**
* "heiId -> HeiEntry" index for {@link #doc}.
*
* <p>
* 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.
* </p>
*/
private final Map<String, HeiEntry> heiEntries;

/**
* "Unique API ID -> API entry elements" index for {@link #doc}.
*
Expand Down Expand Up @@ -292,6 +302,7 @@ public Iterator<String> getPrefixes(String namespaceUri) {
this.hostHeis = new HashMap<>();
this.heiIdMaps = new HashMap<>();
this.apiIndex = new HashMap<>();
this.heiEntries = new HashMap<>();

// Create indexes.

Expand Down Expand Up @@ -325,6 +336,12 @@ public Iterator<String> getPrefixes(String namespaceUri) {

mapForType.put(getCanonicalId(value), heiId);
}
for (Element heiElem : Utils.asElementList(
(NodeList) xpath.evaluate("r:institutions/r:hei", root, XPathConstants.NODESET))) {
String id = heiElem.getAttribute("id");
HeiEntry hei = new HeiEntryImpl(id, heiElem);
this.heiEntries.put(id, hei);
}
for (Element apiElem : Utils.asElementList(
(NodeList) xpath.evaluate("r:host/r:apis-implemented/*", root, XPathConstants.NODESET))) {

Expand Down Expand Up @@ -439,22 +456,7 @@ Element findApi(ApiSearchConditions conditions) {
Collection<Element> findApis(ApiSearchConditions conditions) {
// First, determine the minimum set of elements we need to look through.

List<List<Element>> lookupBase = new ArrayList<>();
if (conditions.getRequiredNamespaceUri() != null && conditions.getRequiredLocalName() != null) {

// We can make use of our namespaceUri+localName index in this case.

List<Element> 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());
}
List<List<Element>> lookupBase = this.getApiLookupBase(conditions);

// Then, iterate through all the elements and filter the ones that match.

Expand All @@ -471,6 +473,26 @@ Collection<Element> findApis(ApiSearchConditions conditions) {
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.
Expand All @@ -485,6 +507,73 @@ String findHeiId(String type, String value) {
return mapForType.get(value);
}

/**
* This implements {@link RegistryClient#findHeis(ApiSearchConditions)}, but only for this
* particular version of the catalogue document.
*/
Collection<HeiEntry> findHeis(ApiSearchConditions conditions) {

// First, determine the minimum set of elements we need to look through.

List<List<Element>> lookupBase = this.getApiLookupBase(conditions);

// Then, find all <host> elements which include the matched APIs.

Set<Element> hostElems = new HashSet<>();
for (List<Element> 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<HeiEntry> results = new HashSet<>();
for (Element hostElem : hostElems) {
Set<String> 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;
}

/**
* This implements {@link RegistryClient#getAllHeis()}, but only for this particular version of
* the catalogue document.
*/
Collection<HeiEntry> getAllHeis() {
return Collections.unmodifiableCollection(this.heiEntries.values());
}

List<List<Element>> getApiLookupBase(ApiSearchConditions conditions) {
List<List<Element>> lookupBase = new ArrayList<>();
if (conditions.getRequiredNamespaceUri() != null && conditions.getRequiredLocalName() != null) {

// We can make use of our namespaceUri+localName index in this case.

List<Element> 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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,13 +315,42 @@ public Collection<Element> findApis(ApiSearchConditions conditions) {
return this.doc.findApis(conditions);
}

@Override
public HeiEntry findHei(String id) throws UnacceptableStalenessException {
// Since expiry date can only be extended, there is no need to synchronize.
this.assertAcceptableStaleness();
return this.doc.findHei(id);
}

@Override
public HeiEntry findHei(String type, String value) throws UnacceptableStalenessException {
// Since expiry date can only be extended, there is no need to synchronize.
this.assertAcceptableStaleness();
return this.doc.findHei(type, value);
}

@Override
public String findHeiId(String type, String value) {
// Since expiry date can only be extended, there is no need to synchronize.
this.assertAcceptableStaleness();
return this.doc.findHeiId(type, value);
}

@Override
public Collection<HeiEntry> findHeis(ApiSearchConditions conditions)
throws UnacceptableStalenessException {
// Since expiry date can only be extended, there is no need to synchronize.
this.assertAcceptableStaleness();
return this.doc.findHeis(conditions);
}

@Override
public Collection<HeiEntry> getAllHeis() throws UnacceptableStalenessException {
// Since expiry date can only be extended, there is no need to synchronize.
this.assertAcceptableStaleness();
return this.doc.getAllHeis();
}

@Override
public Date getExpiryDate() {
// No need to synchronize. Simply get the expiry date of the currently held doc.
Expand Down
51 changes: 51 additions & 0 deletions src/main/java/eu/erasmuswithoutpaper/registryclient/HeiEntry.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package eu.erasmuswithoutpaper.registryclient;

import java.util.Collection;


/**
* Describes a single HEI entry, as found in the EWP Registry's catalogue.
*
* <p>
* Note, that the EWP Registry keeps only the very import attributes of each HEI (identifiers and
* name). If you are looking for more information on HEIs, then you should make use of the
* <a href='https://github.com/erasmus-without-paper/ewp-specs-api-institutions'>Institutions
* API</a>.
* </p>
*
* @since 1.2.0
*/
public interface HeiEntry {

/**
* @return SCHAC ID of this HEI.
*/
String getId();

/**
* Get the name of this HEI.
*
* @return We will try to return the name in English. If we cannot find it, we will return the
* name in any other language. If we fail this too, we will return the HEI's ID, so you
* will never get <code>null</code> here.
*/
String getName();

/**
* Get a name in the given language.
*
* @param langCode An ISO 639-1 code of the language (2 lower-case letters).
* @return String (if the name was found), or null (if it hasn't).
*/
String getName(String langCode);

/**
* Retrieve all <code>&lt;other-id&gt;</code> values of certain type.
*
* @param type type identifier, see {@link RegistryClient#findHei(String, String)} for more
* information on these types.
* @return A collection of all matched values for the given type. In no matches were found, an
* empty collection will be returned.
*/
Collection<String> getOtherIds(String type);
}
113 changes: 113 additions & 0 deletions src/main/java/eu/erasmuswithoutpaper/registryclient/HeiEntryImpl.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package eu.erasmuswithoutpaper.registryclient;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.XMLConstants;

import org.w3c.dom.Element;
import org.w3c.dom.Node;


class HeiEntryImpl implements HeiEntry {

private static class Extras {
private final String primaryName;
private final Map<String, String> allNames;
private final Map<String, List<String>> otherIds;

private Extras(HeiEntryImpl hei) {
this.allNames = new HashMap<>();
this.otherIds = new HashMap<>();
for (Node node : Utils.asNodeList(hei.elem.getChildNodes())) {
if (node.getNodeType() != Node.ELEMENT_NODE) {
continue;
}
Element elem = (Element) node;
String value = elem.getTextContent();
switch (elem.getTagName()) {
case "name":
String lang = elem.getAttributeNS(XMLConstants.XML_NS_URI, "lang");
if (value.length() > 0) {
this.allNames.put(lang, value);
}
break;

case "other-id":
String idType = elem.getAttribute("type");
List<String> lst = this.otherIds.get(idType);
if (lst == null) {
lst = new ArrayList<>();
this.otherIds.put(idType, lst);
}
lst.add(value);
break;

default:
// Ingore.
}
}
String primaryName = this.allNames.get("en");
if (primaryName == null) {
// No English name found. We'll use any name we have.
Collection<String> names = this.allNames.values();
if (names.size() > 0) {
primaryName = names.iterator().next();
} else {
// No name at all!
primaryName = hei.id;
}
}
this.primaryName = primaryName;
}
}

private final String id;
private final Element elem;

private volatile Extras extras = null;

HeiEntryImpl(String id, Element heiElem) {
this.id = id;
this.elem = heiElem;
}

@Override
public String getId() {
return this.id;
}

@Override
public String getName() {
return this.getExtras().primaryName;
}

@Override
public String getName(String langCode) {
return this.getExtras().allNames.get(langCode);
}

@Override
public Collection<String> getOtherIds(String type) {
List<String> values = this.getExtras().otherIds.get(type);
if (values == null) {
return Collections.emptyList();
}
return Collections.unmodifiableCollection(values);
}

private Extras getExtras() {
if (this.extras == null) {
synchronized (this) {
if (this.extras == null) {
this.extras = new Extras(this);
}
}
}
return this.extras;
}
}
Loading

0 comments on commit 6e6eb04

Please sign in to comment.