/*
* Copyright InsightNG (http://www.insightng.com/) 2012-2013
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* - Neither the name of InsightNG nor the names of its contributors may be used
* to endorse or promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.insightng.thirdparty.primal;
import static java.net.HttpURLConnection.HTTP_CREATED;
import static java.net.HttpURLConnection.HTTP_OK;
import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
import org.apache.commons.httpclient.URIException;
import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.httpclient.methods.ByteArrayRequestEntity;
import org.apache.commons.httpclient.methods.DeleteMethod;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.PutMethod;
import org.apache.commons.httpclient.params.HttpConnectionManagerParams;
import org.apache.commons.httpclient.util.URIUtil;
import org.openrdf.model.Model;
import org.openrdf.model.Resource;
import org.openrdf.model.URI;
import org.openrdf.model.ValueFactory;
import org.openrdf.model.impl.LinkedHashModel;
import org.openrdf.model.impl.ValueFactoryImpl;
import org.openrdf.model.vocabulary.DC;
import org.openrdf.model.vocabulary.RDF;
import org.openrdf.model.vocabulary.SKOS;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
import com.google.gson.stream.JsonReader;
import com.insightng.thirdparty.primal.exception.PrimalAuthenticationException;
import com.insightng.thirdparty.primal.exception.PrimalException;
import com.insightng.thirdparty.primal.vocabulary.PRIMAL;
import com.insightng.thirdparty.primal.vocabulary.PrimalTimePeriod;
import com.insightng.thirdparty.primal.vocabulary.ResponseKey;
/**
* PrimalClient provides full client query and manipulation access to the
* interest networks of a specified user on the Primal server. The Primal JSON
* results are processed and converted to RDF models (using the OpenRDF Sesame
* APIs).
*
* @see http://about.primal.com/documentation/
* @see http://www.openrdf.org/
* @author Jeen Broekstra
*/
public class PrimalClient {
/* public constants */
/**
* enumeration of possible Primal API versions: {@link production} and
* {@link latest}
*/
public static enum Version {
production, latest
};
/**
* Maximum timeout for establishing a connection and for keeping it open, in seconds.
*/
public static final int MAX_WAIT_TIME = 30;
/**
* The Primal data API URL (<code>https://data.primal.com/</code> ).
*/
public static final String PRIMAL_DATA_API_URL = "https://data.primal.com/";
public static final String PRIMAL_USER_API_URL = "https://api.primal.com/v1/users";
/* private fields */
private final PrimalAccount primalAccount;
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final static MultiThreadedHttpConnectionManager manager;
private final HttpClient httpClient;
private AuthScope dataApiAuthScope;
private AuthScope userApiAuthScope;
private final URL primalDataApi;
private final URL primalUserApi;
private Version version = Version.production;
private static final ValueFactory vf = ValueFactoryImpl.getInstance();
static {
// Use class-shared MultiThreadedHttpConnectionManager to allow
// concurrent access
manager = new MultiThreadedHttpConnectionManager();
// Allow 20 concurrent connections to the same host (default is 2)
final HttpConnectionManagerParams params = new HttpConnectionManagerParams();
params.setDefaultMaxConnectionsPerHost(20);
// Connection timeout is the timeout to establish a connection.
params.setConnectionTimeout((MAX_WAIT_TIME + 2) * 1000);
// Socket Timeout is the timeout for an open connection to receive data.
params.setSoTimeout((MAX_WAIT_TIME + 2) * 1000);
manager.setParams(params);
Runtime.getRuntime().addShutdownHook(new Thread("PrimalClient-shutdown") {
public void run() {
manager.closeIdleConnections(0);
manager.shutdown();
}
});
}
/* constructors */
/**
* Creates a new Primal Client with the supplied {@link PrimalAccount}
* credentials.
*/
public PrimalClient(PrimalAccount primalAccount) {
this(primalAccount, new HttpClient(manager));
}
/**
* Constructor used for unit testing.
*
* @param primalAccount
* @param httpClient
*/
PrimalClient(PrimalAccount primalAccount, HttpClient httpClient) {
this.primalAccount = primalAccount;
try {
primalDataApi = new URL(PRIMAL_DATA_API_URL);
primalUserApi = new URL(PRIMAL_USER_API_URL);
} catch (final MalformedURLException e) {
throw new RuntimeException(e);
}
this.httpClient = httpClient;
setPrimalUserAPICredentials();
}
/* public methods */
/**
* Creates a new content source.
*
* @param contentSourceName
* the name of the new content source
* @param defaultContentSource
* flag to indicate if this content source should be considered
* the default source to operate on
* @param contents
* specifies which content to include in this source, and
* optionally for each entry an instruction to only consider
* content published in that time period
* @throws PrimalException
* if the content source could not be created.
*/
public void createNewContentSource(String contentSourceName, boolean defaultContentSource,
Map<URI, PrimalTimePeriod> contents) throws PrimalException {
JsonObject newContentSource = new JsonObject();
newContentSource.add("default", new JsonPrimitive(defaultContentSource));
JsonObject contentsObject = new JsonObject();
for (Entry<URI, PrimalTimePeriod> entry : contents.entrySet()) {
JsonObject timePeriodValue = new JsonObject();
if (entry.getValue() != null) {
timePeriodValue.add("time", new JsonPrimitive(entry.getValue().toString()));
}
contentsObject.add(entry.getKey().stringValue(), timePeriodValue);
}
newContentSource.add("contents", contentsObject);
JsonObject payload = new JsonObject();
payload.add(contentSourceName, newContentSource);
PostMethod method = new PostMethod(PRIMAL_DATA_API_URL + "/@Sources");
try {
configureRequest(method);
logRequestDetails(method, PRIMAL_DATA_API_URL + "/@Sources");
ByteArrayRequestEntity requestEntity = new ByteArrayRequestEntity(payload.toString().getBytes(
"UTF-8"), "application/json");
method.setRequestEntity(requestEntity);
final int httpCode = httpClient.executeMethod(method);
logger.debug("http response code:" + httpCode);
if (HTTP_CREATED != httpCode) {
if (HTTP_UNAUTHORIZED == httpCode) {
throw new PrimalAuthenticationException();
} else {
throw new PrimalException("error executing content source creation request", httpCode);
}
}
} catch (final HttpException e) {
logger.warn("error executing content source creation request", e);
throw new PrimalException("error executing content source creation request", 0, e);
} catch (final IOException e) {
logger.warn("error executing content source creation request", e);
throw new PrimalException("error executing content source creation request", 0, e);
} finally {
method.releaseConnection();
}
}
public List<URI> getCustomContentSources() throws PrimalException {
final String requestURL = PRIMAL_DATA_API_URL + "@Sources";
final GetMethod method = new GetMethod(requestURL);
try {
configureRequest(method);
logRequestDetails(method, requestURL);
final int httpCode = httpClient.executeMethod(method);
logger.debug("response code: " + httpCode);
if (httpCode == HTTP_OK) {
final JsonReader reader = new JsonReader(new InputStreamReader(
method.getResponseBodyAsStream()));
final JsonParser jsonParser = new JsonParser();
final JsonObject responseObject = jsonParser.parse(reader).getAsJsonObject();
List<URI> sources = new ArrayList<URI>();
for (Entry<String, JsonElement> entry: responseObject.entrySet()) {
sources.add(vf.createURI(entry.getKey()));
}
return sources;
}
else if (HTTP_UNAUTHORIZED == httpCode) {
throw new PrimalAuthenticationException();
} else {
throw new PrimalException("error retrieving content sources", httpCode);
}
} catch (final HttpException e) {
logger.warn("error retrieving content sources", e);
throw new PrimalException("error retrieving content sources", 0, e);
} catch (final IOException e) {
logger.warn("error retrieving content sources", e);
throw new PrimalException("error retrieving content sources", 0, e);
} finally {
method.releaseConnection();
}
}
public Model getResult(String topic) throws PrimalException {
return getResult(topic, null);
}
/**
* Returns an RDF model describing the supplied interest network. The model
* will contain concepts that have been explicitly provided by the user, as
* well as synthesized concepts that Primal has generated and inserted into
* the interest network. The concepts within the response model will be
* validated with content taken from the website(s) defined within {source}.
*
* @param topic
* Specification of the core topic of interest for this search.
* Values should be provided in a forward slash and/or semi-colon
* delimited format, where slashes delimit a conceptual
* hierarchy, and semi-colons provide a breadth of conceptual
* detail at the same hierarchical level. For example:
* <ul>
* <li><code>tidal+research</code><br>
* Implies a simple use case of interest in the field of tidal
* research.</li>
* <li><code>baking/cookies</code><br>
* Implies interest in cookies of the edible variety rather than
* the browser data storage variety.</li>
* <li><code>shelter/caves;houses;tents</code><br>
* Implies interest in the concepts of caves, houses and tents as
* being types of shelters.</li>
* </ul>
* @param contentSource
* source of content.
* @param storage
* name of the interest network. Valid characters for interest
* network names include: A-Z, a-z, 0-9, underscores and spaces.
*
* @return a {@link Model} containing RDF statements representing a
* description of the interest network. Concepts are represented as
* instances of the class <code>skos:Concept</code> (
* {@link SKOS.CONCEPT}) having a <code>skos:prefLabel</code> (
* {@link SKOS.PREF_LABEL}) property with their human-readable
* label. Content is represented as instances of the class
* <code>primal:ContentItem</code> ({@link PRIMAL.CONTENT_ITEM}).
* @throws PrimalException
* if an error occurred in communicating with the server.
*/
public Model getResult(String topic, String contentSource) throws PrimalException {
return getResult(topic, contentSource, 0, 0, 0);
}
public Model getResult(String topic, int wait) throws PrimalException {
return getResult(topic, null, wait);
}
/**
* Returns an RDF model describing the supplied interest network. The model
* will contain concepts that have been explicitly provided by the user, as
* well as synthesized concepts that Primal has generated and inserted into
* the interest network. The concepts within the response model will be
* validated with content taken from the website(s) defined within {source}.
*
* @param topic
* Specification of the core topic of interest for this search.
* Values should be provided in a forward slash and/or semi-colon
* delimited format, where slashes delimit a conceptual
* hierarchy, and semi-colons provide a breadth of conceptual
* detail at the same hierarchical level. For example:
* <ul>
* <li><code>tidal+research</code><br>
* Implies a simple use case of interest in the field of tidal
* research.</li>
* <li><code>baking/cookies</code><br>
* Implies interest in cookies of the edible variety rather than
* the browser data storage variety.</li>
* <li><code>shelter/caves;houses;tents</code><br>
* Implies interest in the concepts of caves, houses and tents as
* being types of shelters.</li>
* </ul>
* @param contentSource
* source of content.
* @param wait
* Instruct Primal to wait for a specified period of time (in
* seconds) until a request reaches a specified state before
* returning a response. Can only be used in conjunction with a
* specific status parameter.
* @param storage
* name of the interest network. Valid characters for interest
* network names include: A-Z, a-z, 0-9, underscores and spaces.
* @param status
* Instruct Primal to return a response if and only if the
* workflow is currently in the specified state. May be null, in
* which case the workflow state will not be considered.
*
* @return a {@link Model} containing RDF statements representing a
* description of the interest network. Concepts are represented as
* instances of the class <code>skos:Concept</code> (
* {@link SKOS.CONCEPT}) having a <code>skos:prefLabel</code> (
* {@link SKOS.PREF_LABEL}) property with their human-readable
* label. Content is represented as instances of the class
* <code>primal:ContentItem</code> ({@link PRIMAL.CONTENT_ITEM}).
* @throws PrimalException
* if an error occurred in communicating with the server.
*/
public Model getResult(String topic, String contentSource, int wait) throws PrimalException {
return getResult(topic, contentSource, 0, 0, wait);
}
public Model getResult(String topic, float minScore, int maxContentCount, int wait)
throws PrimalException {
return getResult(topic, null, minScore, maxContentCount, wait);
}
/**
* Returns an RDF model describing the supplied interest network. The model
* will contain concepts that have been explicitly provided by the user, as
* well as synthesized concepts that Primal has generated and inserted into
* the interest network. The concepts within the response model will be
* validated with content taken from the website(s) defined within {source}.
*
* @param topic
* Specification of the core topic of interest for this search.
* Values should be provided in a forward slash and/or semi-colon
* delimited format, where slashes delimit a conceptual
* hierarchy, and semi-colons provide a breadth of conceptual
* detail at the same hierarchical level. For example:
* <ul>
* <li><code>tidal+research</code><br>
* Implies a simple use case of interest in the field of tidal
* research.</li>
* <li><code>baking/cookies</code><br>
* Implies interest in cookies of the edible variety rather than
* the browser data storage variety.</li>
* <li><code>shelter/caves;houses;tents</code><br>
* Implies interest in the concepts of caves, houses and tents as
* being types of shelters.</li>
* </ul>
* @param contentSource
* source of content.
* @param minScore
* modifies the results returned based on relevancy by changing
* the minimum acceptable primal:HasScore value. If set to zero,
* all results will be returned.
* @param maxContentCount
* modifies the number of results returned by changing the
* maximum number of content items to return. For example, you
* would use this parameter to return a "top ten" list of content
* items. If set to zero, all result content items will be
* returned.
* @param wait
* Instruct Primal to wait for a specified period of time (in
* seconds) until a request reaches a specified state before
* returning a response. Can only be used in conjunction with a
* specific status parameter.
* @param storage
* name of the interest network. Valid characters for interest
* network names include: A-Z, a-z, 0-9, underscores and spaces.
* @param status
* Instruct Primal to return a response if and only if the
* workflow is currently in the specified state. May be null, in
* which case the workflow state will not be considered.
*
* @return a {@link org.openrdf.model.Model} containing RDF statements
* representing a description of the interest network. Concepts are
* represented as instances of the class <code>skos:Concept</code> (
* {@link SKOS.CONCEPT}) having a <code>skos:prefLabel</code> (
* {@link SKOS.PREF_LABEL}) property with their human-readable
* label. Content is represented as instances of the class
* <code>primal:ContentItem</code> ({@link PRIMAL.CONTENT_ITEM}).
* @throws PrimalException
* if an error occurred in communicating with the server.
*/
public Model getResult(String topic, String contentSource, float minScore, int maxContentCount, int wait)
throws PrimalException {
if (wait > MAX_WAIT_TIME) {
throw new PrimalException("wait time is not allowed to exceed " + MAX_WAIT_TIME + " seconds", 0);
}
final Model model = new LinkedHashModel();
final String requestURL = createRequestURLString(topic, contentSource, minScore, maxContentCount,
wait);
final GetMethod method = new GetMethod(requestURL);
try {
configureRequest(method);
logRequestDetails(method, requestURL);
final int httpCode = httpClient.executeMethod(method);
logger.debug("response code: " + httpCode);
if (httpCode == HTTP_OK) {
final JsonReader reader = new JsonReader(new InputStreamReader(
method.getResponseBodyAsStream()));
final JsonParser jsonParser = new JsonParser();
final JsonObject responseObject = jsonParser.parse(reader).getAsJsonObject();
logger.trace("json response string: " + responseObject.toString());
final JsonObject responseInfo = responseObject
.getAsJsonObject(ResponseKey.PRIMAL_RESPONSE_INFO);
if (responseInfo != null) {
logger.debug("processing primal response info in response");
final Resource responseInfoId = vf.createURI(requestURL);
ResponseValues.processIntValue(responseInfoId, PRIMAL.CONTENT_COUNT, model,
PRIMAL.RESPONSE_INFO, responseInfo, ResponseKey.PRIMAL_CONTENT_COUNT);
ResponseValues.processIntValue(responseInfoId, PRIMAL.TOTAL_CONCEPTS_COUNT, model,
PRIMAL.RESPONSE_INFO, responseInfo, ResponseKey.PRIMAL_TOTAL_CONCEPTS_COUNT);
ResponseValues.processFloatValue(responseInfoId, PRIMAL.MIN_SEMANTIC_COVERAGE, model,
PRIMAL.RESPONSE_INFO, responseInfo, ResponseKey.PRIMAL_MIN_SEMANTIC_COVERAGE);
ResponseValues.processStringValue(responseInfoId, PRIMAL.STATUS, model,
PRIMAL.RESPONSE_INFO, responseInfo,
Version.latest == getPrimalVersion() ? ResponseKey.PRIMAL_STATUS
: ResponseKey.PRIMAL_STATUS);
ResponseValues.processIntValue(responseInfoId, PRIMAL.CONTENT_FILTERED_OUT, model,
PRIMAL.RESPONSE_INFO, responseInfo, ResponseKey.PRIMAL_CONTENT_FILTERED_OUT);
ResponseValues.processIntValue(responseInfoId, PRIMAL.CONCEPT_COUNT, model,
PRIMAL.RESPONSE_INFO, responseInfo, ResponseKey.PRIMAL_CONCEPT_COUNT);
ResponseValues.processFloatValue(responseInfoId, PRIMAL.HIGH_CONTENT_SCORE, model,
PRIMAL.RESPONSE_INFO, responseInfo, ResponseKey.PRIMAL_HIGH_CONTENT_SCORE);
ResponseValues.processFloatValue(responseInfoId, PRIMAL.TERM_COVERAGE, model,
PRIMAL.RESPONSE_INFO, responseInfo, ResponseKey.PRIMAL_TERM_COVERAGE);
if (responseInfo.has(ResponseKey.PRIMAL_RECOGNIZED_TERMS)) {
final JsonArray recTerms = responseInfo.get(ResponseKey.PRIMAL_RECOGNIZED_TERMS)
.getAsJsonArray();
for (int i = 0; i < recTerms.size(); i++) {
final String term = recTerms.get(i).getAsString();
model.add(responseInfoId, PRIMAL.RECOGNIZED_TERM, vf.createLiteral(term),
PRIMAL.RESPONSE_INFO);
}
}
ResponseValues.processStringValue(responseInfoId, PRIMAL.VIABILITY_MESSAGE, model,
PRIMAL.RESPONSE_INFO, responseInfo, ResponseKey.PRIMAL_VIABILITY_MESSAGE);
ResponseValues.processFloatValue(responseInfoId, PRIMAL.SEMANTIC_RATIO, model,
PRIMAL.RESPONSE_INFO, responseInfo, ResponseKey.PRIMAL_SEMANTIC_RATIO);
ResponseValues.processBooleanValue(responseInfoId, PRIMAL.HAS_EXPANSION, model,
PRIMAL.RESPONSE_INFO, responseInfo, ResponseKey.PRIMAL_HAS_EXPANSION);
}
logger.debug("processing SKOS concepts in response");
final JsonObject conceptScheme = responseObject
.getAsJsonObject(ResponseKey.SKOS_CONCEPT_SCHEME);
final JsonElement topConcepts = conceptScheme.get(ResponseKey.SKOS_HAS_TOP_CONCEPT);
final JsonObject collection = conceptScheme.getAsJsonObject(ResponseKey.SKOS_COLLECTION);
final Set<Entry<String, JsonElement>> members = collection.entrySet();
final Resource conceptSchemeId = vf.createBNode();
model.add(conceptSchemeId, RDF.TYPE, SKOS.CONCEPT_SCHEME, PRIMAL.CONCEPTS);
final Resource collectionId = vf.createBNode();
model.add(collectionId, RDF.TYPE, SKOS.COLLECTION, PRIMAL.CONCEPTS);
final List<URI> rootConcepts = new ArrayList<URI>();
if (topConcepts.isJsonArray()) {
URI rcURI;
final JsonArray rcArray = topConcepts.getAsJsonArray();
for (int i = 0; i < rcArray.size(); i++) {
rcURI = vf.createURI(rcArray.get(i).getAsString());
rootConcepts.add(rcURI);
}
} else {
final URI rcURI = vf.createURI(topConcepts.getAsString());
rootConcepts.add(rcURI);
}
for (final URI rootConceptId : rootConcepts) {
model.add(collectionId, SKOS.MEMBER, rootConceptId, PRIMAL.CONCEPTS);
model.add(conceptSchemeId, SKOS.HAS_TOP_CONCEPT, rootConceptId, PRIMAL.CONCEPTS);
for (final Entry<String, JsonElement> member : members) {
final String conceptId = member.getKey();
final JsonObject conceptAsJson = member.getValue().getAsJsonObject();
final JsonElement prefLabelElement = conceptAsJson.get(ResponseKey.SKOS_PREF_LABEL);
String prefLabel = null;
if (prefLabelElement != null && !prefLabelElement.isJsonNull()) {
prefLabel = prefLabelElement.getAsString();
} else {
logger.warn("no prefLabel found for {}. Skipping concept creation", conceptId);
continue;
}
final URI concept = vf.createURI(conceptId);
model.add(concept, RDF.TYPE, SKOS.CONCEPT, PRIMAL.CONCEPTS);
model.add(concept, SKOS.PREF_LABEL, vf.createLiteral(prefLabel), PRIMAL.CONCEPTS);
model.add(collectionId, SKOS.MEMBER, concept, PRIMAL.CONCEPTS);
ResponseValues.processFloatValue(concept, PRIMAL.CONCEPT_SCORE, model,
PRIMAL.CONCEPTS, conceptAsJson, ResponseKey.PRIMAL_CONCEPT_SCORE);
ResponseValues.processStringValue(concept, SKOS.ALT_LABEL, model, PRIMAL.CONCEPTS,
conceptAsJson, ResponseKey.SKOS_ALT_LABEL);
ResponseValues.processStringValue(concept, PRIMAL.SOURCE, model, PRIMAL.CONCEPTS,
conceptAsJson, ResponseKey.PRIMAL_SOURCE);
final JsonElement narrowerElem = conceptAsJson.get(ResponseKey.SKOS_NARROWER);
if (narrowerElem != null) {
final JsonArray narrower = narrowerElem.getAsJsonArray();
for (int i = 0; i < narrower.size(); i++) {
final String narrowerId = narrower.get(i).getAsString();
model.add(concept, SKOS.NARROWER, vf.createURI(narrowerId), PRIMAL.CONCEPTS);
}
}
}
}
logger.debug("processing DC part of response...");
final JsonElement dcCollection = responseObject.get(ResponseKey.DC_COLLECTION);
if (dcCollection.isJsonArray()) {
final JsonArray array = dcCollection.getAsJsonArray();
logger.debug("response contains {} content items", array.size());
for (int i = 0; i < array.size(); i++) {
final JsonObject contentItem = array.get(i).getAsJsonObject();
final String dcIdentifier = contentItem.get(ResponseKey.DC_IDENTIFIER).getAsString();
final URI contentItemId = vf.createURI(dcIdentifier);
model.add(contentItemId, RDF.TYPE, PRIMAL.CONTENT_ITEM, PRIMAL.CONTENT);
model.add(contentItemId, DC.IDENTIFIER, vf.createLiteral(dcIdentifier),
PRIMAL.CONTENT);
ResponseValues.processStringValue(contentItemId, DC.TITLE, model, PRIMAL.CONTENT,
contentItem, ResponseKey.DC_TITLE);
ResponseValues.processStringValue(contentItemId, DC.DESCRIPTION, model,
PRIMAL.CONTENT, contentItem, ResponseKey.DC_DESCRIPTION);
ResponseValues.processStringValue(contentItemId, DC.PUBLISHER, model, PRIMAL.CONTENT,
contentItem, ResponseKey.DC_PUBLISHER);
ResponseValues.processStringValue(contentItemId, DC.SOURCE, model, PRIMAL.CONTENT,
contentItem, ResponseKey.DC_SOURCE);
ResponseValues.processStringValue(contentItemId, DC.RELATION, model, PRIMAL.CONTENT,
contentItem, ResponseKey.DC_RELATION);
ResponseValues.processDateValue(contentItemId, DC.DATE, model, PRIMAL.CONTENT,
contentItem, ResponseKey.DC_DATE);
ResponseValues.processFloatValue(contentItemId, PRIMAL.CONTENT_SCORE, model,
PRIMAL.CONTENT, contentItem, ResponseKey.PRIMAL_CONTENT_SCORE);
final JsonArray subjects = contentItem.get(ResponseKey.DC_SUBJECT).getAsJsonArray();
for (int j = 0; j < subjects.size(); j++) {
final String subject = subjects.get(j).getAsString();
model.add(contentItemId, DC.SUBJECT, vf.createURI(subject), PRIMAL.CONTENT);
}
}
} else {
// TODO can this happen?
logger.warn("json element " + ResponseKey.DC_COLLECTION
+ " of unexpected type. Expected array.");
}
logger.debug("response processing complete");
} else {
logger.warn("response not ok " + httpCode);
if (HTTP_UNAUTHORIZED == httpCode) {
throw new PrimalAuthenticationException();
} else {
throw new PrimalException("error retrieving results.", httpCode);
}
}
} catch (final HttpException e) {
logger.warn("protocol error retrieving result", e);
throw new PrimalException("protocol error retrieving result", 0, e);
} catch (final IOException e) {
logger.warn("I/O error retrieving result", e);
throw new PrimalException("I/O error retrieving result", 0, e);
} finally {
method.releaseConnection();
}
return model;
}
public void addConcept(String topic, boolean expand) throws PrimalException {
addConcept(topic, null, expand);
}
/**
* Creates a topic.
*
* @param topic
* Specification of the core topic of interest for this add
* request. Values should be provided in a forward slash and/or
* semi-colon delimited format, where slashes delimit a
* conceptual hierarchy, and semi-colons provide a breadth of
* conceptual detail at the same hierarchical level. For example:
* <ul>
* <li><code>tidal+research</code><br>
* Implies a simple use case of interest in the field of tidal
* research.</li>
* <li><code>baking/cookies</code><br>
* Implies interest in cookies of the edible variety rather than
* the browser data storage variety.</li>
* <li><code>shelter/caves;houses;tents</code><br>
* Implies interest in the concepts of caves, houses and tents as
* being types of shelters.</li>
* </ul>
* @param contentSource
* source of content. If set to null, the default content
* source(s) will be used.
* @param expand
* Indicates if Primal should initiate semantic expansion on the
* submitted concept.
* @param storage
* name of the interest network. Valid characters for interest
* network names include: A-Z, a-z, 0-9, underscores and spaces.
*
* @throws PrimalException
* if an error occurred while adding the concept.
*/
public void addConcept(String topic, String contentSource, boolean expand) throws PrimalException {
final String requestURL = createRequestURLString(topic, contentSource, 0, 0, 0);
String methodName = "POST";
HttpMethod method = null;
if (expand) {
method = new PostMethod(requestURL);
} else {
method = new PutMethod(requestURL);
methodName = "PUT";
}
try {
configureRequest(method);
logRequestDetails(method, requestURL);
final int httpCode = httpClient.executeMethod(method);
logger.debug("http response code:" + httpCode);
if (HTTP_CREATED != httpCode) {
if (HTTP_UNAUTHORIZED == httpCode) {
throw new PrimalAuthenticationException();
} else {
throw new PrimalException("error executing add request", httpCode);
}
}
} catch (final HttpException e) {
logger.warn("error executing add request", e);
throw new PrimalException("error executing add request", 0, e);
} catch (final IOException e) {
logger.warn("error executing add request", e);
throw new PrimalException("error executing add request", 0, e);
} finally {
method.releaseConnection();
}
}
public void deleteConcept(String topicOfInterest) throws PrimalException {
deleteConcept(topicOfInterest, null);
}
/**
* Removes a concept from an interest network.
*
* @param topic
* Specification of the core topic of interest for this delete
* request. Values should be provided in a forward slash and/or
* semi-colon delimited format, where slashes delimit a
* conceptual hierarchy, and semi-colons provide a breadth of
* conceptual detail at the same hierarchical level. For example:
* <ul>
* <li><code>tidal+research</code><br>
* Implies a simple use case of interest in the field of tidal
* research.</li>
* <li><code>baking/cookies</code><br>
* Implies interest in cookies of the edible variety rather than
* the browser data storage variety.</li>
* <li><code>shelter/caves;houses;tents</code><br>
* Implies interest in the concepts of caves, houses and tents as
* being types of shelters.</li>
* </ul>
* @param contentSource
* source of content.
* @param storage
* name of the interest network. Valid characters for interest
* network names include: A-Z, a-z, 0-9, underscores and spaces.
*
* @throws PrimalException
* if an error occurred while removing the concept.
*/
public void deleteConcept(String topic, String contentSource) throws PrimalException {
final String requestURL = createRequestURLString(topic, contentSource, 0, 0, 0);
HttpMethod method = new DeleteMethod(requestURL);
try {
configureRequest(method);
logRequestDetails(method, requestURL);
final int httpCode = httpClient.executeMethod(method);
logger.debug("http response code:" + httpCode);
if (HTTP_OK != httpCode) {
if (HTTP_UNAUTHORIZED == httpCode) {
throw new PrimalAuthenticationException();
} else {
throw new PrimalException("error executing delete request.", httpCode);
}
}
} catch (final HttpException e) {
logger.warn("error executing delete request", e);
throw new PrimalException("error executing delete request", 0, e);
} catch (final IOException e) {
logger.warn("error executing delete request", e);
throw new PrimalException("error executing delete request", 0, e);
} finally {
method.releaseConnection();
}
}
/**
* retrieves the API version this PrimalClient currently operates on.
*
* @return the current version.
*/
public Version getPrimalVersion() {
return version;
}
/**
* set the API version this PrimalClient will operate on.
*
* @param version
* the version.
*/
public void setPrimalVersion(Version version) {
this.version = version;
}
/**
* Shut down this client, freeing up resources.
*/
public void shutdown() {
httpClient.getHttpConnectionManager().closeIdleConnections(0);
}
/**
* deletes user with the given username via the Primal User Management API
*
* @see https://about.primal.com/developers/documentation/user-management
* @param username
* the username to delete
* @throws PrimalException
* if the user could not be deleted.
*/
public void deleteUser(String username) throws PrimalException {
if (username == null) {
throw new PrimalException("username can not be null", 0);
}
String requestURL = null;
try {
requestURL = PRIMAL_USER_API_URL + "/" + URIUtil.encodePath(username, "UTF-8");
} catch (URIException e1) {
throw new RuntimeException(e1);
}
DeleteMethod method = new DeleteMethod(requestURL);
try {
configureRequest(method);
logRequestDetails(method, requestURL);
final int httpCode = httpClient.executeMethod(method);
logger.debug("http response code:" + httpCode);
if (HTTP_OK != httpCode) {
if (HTTP_UNAUTHORIZED == httpCode) {
throw new PrimalAuthenticationException();
} else {
logger.debug(method.getResponseBodyAsString());
throw new PrimalException("error executing deleteUSer request", httpCode);
}
}
} catch (final HttpException e) {
logger.warn("error executing delete request", e);
throw new PrimalException("error executing delete request", 0, e);
} catch (final IOException e) {
logger.warn("error executing delete request", e);
throw new PrimalException("error executing delete request", 0, e);
} finally {
method.releaseConnection();
}
}
/**
* Creates a new user with the given username/password via the Primal User
* Management API
*
* @see https://about.primal.com/developers/documentation/user-management
* @param username
* the username to create
* @param password
* the paswsword to assign the new username
* @return the full username as created by Primal
* @throws PrimalException
* if the user could not be created.
*/
public String createUser(String username, String password) throws PrimalException {
PostMethod method = new PostMethod(PRIMAL_USER_API_URL);
String createdUser = null;
final JsonObject payload = new JsonObject();
payload.add("name", new JsonPrimitive(username));
payload.add("password", new JsonPrimitive(password));
try {
ByteArrayRequestEntity requestEntity = new ByteArrayRequestEntity(payload.toString().getBytes(
"UTF-8"), "application/json");
method.setRequestEntity(requestEntity);
} catch (UnsupportedEncodingException e1) {
throw new RuntimeException(e1);
}
try {
configureRequest(method);
logRequestDetails(method, PRIMAL_USER_API_URL);
final int httpCode = httpClient.executeMethod(method);
logger.debug("http response code:" + httpCode);
if (HTTP_CREATED != httpCode) {
if (HTTP_UNAUTHORIZED == httpCode) {
throw new PrimalAuthenticationException();
} else {
logger.debug(method.getResponseBodyAsString());
throw new PrimalException("error executing createUser request", httpCode);
}
} else {
final JsonReader reader = new JsonReader(new InputStreamReader(
method.getResponseBodyAsStream()));
final JsonParser jsonParser = new JsonParser();
final JsonObject responseObject = jsonParser.parse(reader).getAsJsonObject();
createdUser = responseObject.get("name").getAsString();
}
} catch (final HttpException e) {
logger.warn("error executing add request", e);
throw new PrimalException("error executing add request", 0, e);
} catch (final IOException e) {
logger.warn("error executing add request", e);
throw new PrimalException("error executing add request", 0, e);
} finally {
method.releaseConnection();
}
return createdUser;
}
/**
* Changes the password for the given user via the Primal User Management
* API
*
* @see https://about.primal.com/developers/documentation/user-management
* @param username
* the username for which to change the password
* @param password
* the paswsword to assign
* @return the full username as created by Primal
* @throws PrimalException
* if the user could not be created.
*/
public void changePassword(String username, String password) throws PrimalException {
if (username == null) {
throw new PrimalException("username can not be null", 0);
}
if (password == null) {
throw new PrimalException("password can not be null", 0);
}
String requestURL = null;
try {
requestURL = PRIMAL_USER_API_URL + "/" + URIUtil.encodePath(username, "UTF-8");
} catch (URIException e2) {
throw new RuntimeException(e2);
}
PostMethod method = new PostMethod(requestURL);
final JsonObject payload = new JsonObject();
payload.add("password", new JsonPrimitive(password));
try {
ByteArrayRequestEntity requestEntity = new ByteArrayRequestEntity(payload.toString().getBytes(
"UTF-8"), "application/json");
method.setRequestEntity(requestEntity);
} catch (UnsupportedEncodingException e1) {
throw new RuntimeException(e1);
}
try {
configureRequest(method);
logRequestDetails(method, requestURL);
final int httpCode = httpClient.executeMethod(method);
logger.debug("http response code:" + httpCode);
if (HTTP_OK != httpCode) {
if (HTTP_UNAUTHORIZED == httpCode) {
throw new PrimalAuthenticationException();
} else {
logger.debug(method.getResponseBodyAsString());
throw new PrimalException("error executing change password request", httpCode);
}
}
} catch (final HttpException e) {
logger.warn("error executing change request", e);
throw new PrimalException("error executing change request", 0, e);
} catch (final IOException e) {
logger.warn("error executing change request", e);
throw new PrimalException("error executing change request", 0, e);
} finally {
method.releaseConnection();
}
}
/**
* Retrieve a list of users via the Primal User API
*
* @see https://about.primal.com/developers/documentation/user-management
*
* @return a list of usernames.
*
* @throws PrimalException
*/
public List<String> getUsers() throws PrimalException {
GetMethod method = new GetMethod(PRIMAL_USER_API_URL);
try {
configureRequest(method);
logRequestDetails(method, PRIMAL_USER_API_URL);
final int httpCode = httpClient.executeMethod(method);
if (HTTP_OK != httpCode) {
if (HTTP_UNAUTHORIZED == httpCode) {
throw new PrimalAuthenticationException();
} else {
logger.error("error retrieving users: " + method.getResponseBodyAsString());
throw new PrimalException("error retrieving users", httpCode);
}
} else {
final JsonReader reader = new JsonReader(new InputStreamReader(
method.getResponseBodyAsStream()));
final JsonParser jsonParser = new JsonParser();
final JsonObject response = jsonParser.parse(reader).getAsJsonObject();
final JsonArray users = response.getAsJsonArray("users");
List<String> result = new ArrayList<String>();
for (int i = 0; i < users.size(); i++) {
String user = users.get(i).getAsJsonObject().get("name").getAsString();
result.add(user);
}
return result;
}
} catch (IOException e) {
throw new PrimalException("error retrieving list of users", 0, e);
} finally {
method.releaseConnection();
}
}
/**
* Set the username and password for authentication with the Primal Data
* API.
*/
public void setPrimalDataAPICredentials(String username, String password) {
if (username != null && password != null) {
logger.debug("Setting username '{}' and password for server at {}.", username,
primalDataApi.toExternalForm());
dataApiAuthScope = new AuthScope(primalDataApi.getHost(), AuthScope.ANY_PORT);
httpClient.getState().setCredentials(dataApiAuthScope,
new UsernamePasswordCredentials(username, password));
httpClient.getParams().setAuthenticationPreemptive(true);
} else {
logger.warn("incomplete credentials supplied, not using authentication");
dataApiAuthScope = null;
httpClient.getState().clearCredentials();
httpClient.getParams().setAuthenticationPreemptive(false);
}
}
/* protected/private methods */
/**
* Set the username and password for authentication with the Primal User
* API.
*/
private void setPrimalUserAPICredentials() {
if (primalAccount.getUsername() != null && primalAccount.getPassword() != null) {
logger.debug("Setting username '{}' and password for server at {}.", primalAccount.getUsername(),
primalUserApi.toExternalForm());
userApiAuthScope = new AuthScope(primalUserApi.getHost(), AuthScope.ANY_PORT);
httpClient.getState()
.setCredentials(
userApiAuthScope,
new UsernamePasswordCredentials(primalAccount.getUsername(), primalAccount
.getPassword()));
httpClient.getParams().setAuthenticationPreemptive(true);
} else {
logger.warn("incomplete credentials supplied, not using authentication");
userApiAuthScope = null;
httpClient.getState().clearCredentials();
httpClient.getParams().setAuthenticationPreemptive(false);
}
}
private final void configureRequest(HttpMethod method) {
if (dataApiAuthScope != null && httpClient.getState().getCredentials(dataApiAuthScope) != null) {
method.setDoAuthentication(true);
} else {
method.setDoAuthentication(false);
}
if (primalAccount.getApplicationID() != null) {
method.setRequestHeader("Primal-App-ID", primalAccount.getApplicationID());
}
if (primalAccount.getApplicationKey() != null) {
method.setRequestHeader("Primal-App-Key", primalAccount.getApplicationKey());
}
method.setRequestHeader("Primal-Version", getPrimalVersion().toString());
}
private String createRequestURLString(String topic, String contentSource, float minScore,
int maxContentCount, int wait) {
String url = primalDataApi.toExternalForm();
try {
if (topic != null) {
url += URIUtil.encodePath(topic);
}
if (minScore > 0 || maxContentCount > 0 || contentSource != null || wait > 0) {
url += "?";
if (minScore > 0) {
url += "primal:contentScore:min=" + minScore;
url += "&";
}
if (maxContentCount > 0) {
url += "primal:contentCount:max=" + maxContentCount;
url += "&";
}
if (contentSource != null) {
url += "contentSource=" + URIUtil.encodePath(contentSource);
url += "&";
}
if (wait > 0) {
url += "timeOut=" + wait;
url += "&";
}
}
} catch (final URIException e) {
// can only happen if the default protocol charset is not supported.
// Since we use UTF-8 consistently, that
// should never happen.
throw new RuntimeException(e);
}
logger.debug("request url: " + url);
return url;
}
private void logRequestDetails(HttpMethod method, String requestURL) {
logger.debug("{} request", method.getName());
logger.debug("\trequest URL: {}", requestURL);
for (Header header : method.getRequestHeaders()) {
logger.debug("\t{}: {}", header.getName(), header.getValue());
}
}
}