diff --git a/src/main/java/de/thm/arsnova/persistance/couchdb/InitializingCouchDbConnector.java b/src/main/java/de/thm/arsnova/persistance/couchdb/InitializingCouchDbConnector.java index 126504089a0f6252e5b0c24b22bb4c24734a718b..a5785a7e9c3877587047b71e5a9489b06c621ee4 100644 --- a/src/main/java/de/thm/arsnova/persistance/couchdb/InitializingCouchDbConnector.java +++ b/src/main/java/de/thm/arsnova/persistance/couchdb/InitializingCouchDbConnector.java @@ -1,10 +1,11 @@ package de.thm.arsnova.persistance.couchdb; import com.fasterxml.jackson.core.JsonProcessingException; +import de.thm.arsnova.entities.Content; +import de.thm.arsnova.persistance.couchdb.support.MangoCouchDbConnector; import org.ektorp.CouchDbInstance; import org.ektorp.DocumentNotFoundException; import org.ektorp.impl.ObjectMapperFactory; -import org.ektorp.impl.StdCouchDbConnector; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; @@ -24,7 +25,7 @@ import java.io.InputStreamReader; import java.util.ArrayList; import java.util.List; -public class InitializingCouchDbConnector extends StdCouchDbConnector implements InitializingBean, ResourceLoaderAware { +public class InitializingCouchDbConnector extends MangoCouchDbConnector implements InitializingBean, ResourceLoaderAware { private static final Logger logger = LoggerFactory.getLogger(InitializingCouchDbConnector.class); private final List<Bindings> docs = new ArrayList<>(); diff --git a/src/main/java/de/thm/arsnova/persistance/couchdb/support/MangoCouchDbConnector.java b/src/main/java/de/thm/arsnova/persistance/couchdb/support/MangoCouchDbConnector.java new file mode 100644 index 0000000000000000000000000000000000000000..4f4eb6197ecf57796712020bd4625e9c8be356f5 --- /dev/null +++ b/src/main/java/de/thm/arsnova/persistance/couchdb/support/MangoCouchDbConnector.java @@ -0,0 +1,217 @@ +package de.thm.arsnova.persistance.couchdb.support; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonView; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.type.TypeFactory; +import com.fasterxml.jackson.databind.util.Converter; +import de.thm.arsnova.entities.serialization.View; +import org.ektorp.CouchDbInstance; +import org.ektorp.DbAccessException; +import org.ektorp.impl.ObjectMapperFactory; +import org.ektorp.impl.StdCouchDbConnector; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * This Connector adds a query method which uses CouchDB's Mango API to retrieve data. + */ +public class MangoCouchDbConnector extends StdCouchDbConnector { + @JsonInclude(JsonInclude.Include.NON_DEFAULT) + /** + * Represents a <code>_find</code> query for CouchDB's Mango API. + * See http://docs.couchdb.org/en/stable/api/database/find.html#db-find. + */ + public class MangoQuery { + @JsonSerialize(converter = Sort.ToListConverter.class) + public class Sort { + public class ToListConverter implements Converter<Sort, List<String>> { + @Override + public List<String> convert(Sort value) { + return Arrays.asList(value.field, value.descending ? "desc" : "asc"); + } + + @Override + public JavaType getInputType(TypeFactory typeFactory) { + return typeFactory.constructType(Sort.class); + } + + @Override + public JavaType getOutputType(TypeFactory typeFactory) { + return typeFactory.constructGeneralizedType(typeFactory.constructType(List.class), String.class); + } + } + + private String field; + private boolean descending = false; + + public Sort(String field, boolean descending) { + this.field = field; + this.descending = descending; + } + + public String getField() { + return field; + } + + public void setField(String field) { + this.field = field; + } + + public boolean isDescending() { + return descending; + } + + public void setDescending(boolean descending) { + this.descending = descending; + } + } + + private Map<String, Object> selector; + private List<String> fields = new ArrayList<>(); + private List<Sort> sort = new ArrayList<>(); + private int limit = 0; + private int skip = 0; + private String indexDocument; + private String indexName; + private boolean update = true; + private boolean stable = false; + + public MangoQuery() { + this.selector = new HashMap<>(); + } + + /** + * @param selector See http://docs.couchdb.org/en/stable/api/database/find.html#selector-syntax. + */ + public MangoQuery(Map<String, Object> selector) { + this.selector = selector; + } + + @JsonView(View.Persistence.class) + public Map<String, ?> getSelector() { + return selector; + } + + /** + * @param selector See http://docs.couchdb.org/en/stable/api/database/find.html#selector-syntax. + */ + public void setSelector(Map<String, Object> selector) { + this.selector = selector; + } + + @JsonView(View.Persistence.class) + public List<String> getFields() { + return fields; + } + + public void setFields(List<String> fields) { + this.fields = fields; + } + + @JsonView(View.Persistence.class) + public List<Sort> getSort() { + return sort; + } + + public void setSort(List<Sort> sort) { + this.sort = sort; + } + + @JsonView(View.Persistence.class) + public int getLimit() { + return limit; + } + + public void setLimit(int limit) { + this.limit = limit; + } + + @JsonView(View.Persistence.class) + public int getSkip() { + return skip; + } + + public void setSkip(int skip) { + this.skip = skip; + } + + public String getIndexDocument() { + return indexDocument; + } + + public void setIndexDocument(String indexDocument) { + this.indexDocument = indexDocument; + } + + public String getIndexName() { + return indexName; + } + + public void setIndexName(String indexName) { + this.indexName = indexName; + } + + @JsonView(View.Persistence.class) + public Object getIndex() { + return indexName != null ? new String[] {indexDocument, indexName} : indexDocument; + } + + @JsonView(View.Persistence.class) + public boolean isUpdate() { + return update; + } + + public void setUpdate(boolean update) { + this.update = update; + } + + @JsonView(View.Persistence.class) + public boolean isStable() { + return stable; + } + + public void setStable(boolean stable) { + this.stable = stable; + } + } + + private static final Logger logger = LoggerFactory.getLogger(MangoCouchDbConnector.class); + + public MangoCouchDbConnector(String databaseName, CouchDbInstance dbInstance) { + super(databaseName, dbInstance); + } + + public MangoCouchDbConnector(String databaseName, CouchDbInstance dbi, ObjectMapperFactory om) { + super(databaseName, dbi, om); + } + + /** + * + * @param query The query sent to CouchDB's Mango API + * @param type Type for deserialization of retrieved entities + * @return List of retrieved entities + */ + public <T> List<T> query(final MangoQuery query, final Class<T> type) { + MangoResponseHandler<T> rh = new MangoResponseHandler<T>(type, objectMapper, true); + String queryString; + try { + queryString = objectMapper.writeValueAsString(query); + logger.debug("Querying CouchDB using Mango API: {}", queryString); + } catch (JsonProcessingException e) { + throw new DbAccessException("Could not serialize Mango query."); + } + List<T> result = restTemplate.post(dbURI.append("_find").toString(), queryString, rh); + logger.debug("Answer from CouchDB Mango query: {}", result); + + return result; + } +} diff --git a/src/main/java/de/thm/arsnova/persistance/couchdb/support/MangoQueryResultParser.java b/src/main/java/de/thm/arsnova/persistance/couchdb/support/MangoQueryResultParser.java new file mode 100644 index 0000000000000000000000000000000000000000..7367787f539098ce75769c789eb988152976bf06 --- /dev/null +++ b/src/main/java/de/thm/arsnova/persistance/couchdb/support/MangoQueryResultParser.java @@ -0,0 +1,89 @@ +package de.thm.arsnova.persistance.couchdb.support; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.ektorp.DbAccessException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +public class MangoQueryResultParser<T> { + private static final String DOCS_FIELD_NAME = "docs"; + private static final String WARNING_FIELD_NAME = "warning"; + private static final String ERROR_FIELD_NAME = "error"; + private static final String REASON_FIELD_NAME = "reason"; + + private static final Logger logger = LoggerFactory.getLogger(MangoQueryResultParser.class); + + private Class<T> type; + private ObjectMapper objectMapper; + private List<T> docs; + + public MangoQueryResultParser(Class<T> type, ObjectMapper objectMapper) { + this.type = type; + this.objectMapper = objectMapper; + } + + public void parseResult(InputStream json) throws IOException { + JsonParser jp = objectMapper.getFactory().createParser(json); + + try { + parseResult(jp); + } finally { + jp.close(); + } + } + + private void parseResult(JsonParser jp) throws IOException { + if (jp.nextToken() != JsonToken.START_OBJECT) { + throw new DbAccessException("Expected data to start with an Object"); + } + + String error = null; + String reason = null; + + // Issue #98: Can't assume order of JSON fields. + while (jp.nextValue() != JsonToken.END_OBJECT) { + String currentName = jp.getCurrentName(); + if (DOCS_FIELD_NAME.equals(currentName)) { + docs = new ArrayList<T>(); + parseDocs(jp); + } else if (WARNING_FIELD_NAME.equals(currentName)) { + logger.warn("Warning for CouchDB Mango query: {}", jp.getText()); + } else if (ERROR_FIELD_NAME.equals(currentName)) { + error = jp.getText(); + } else if (REASON_FIELD_NAME.equals(currentName)) { + reason = jp.getText(); + } + } + + if (error != null) { + String errorDesc = reason != null ? reason : error; + throw new DbAccessException("CouchDB Mango query failed: " + errorDesc); + } + } + + private void parseDocs(JsonParser jp) throws IOException { + if (jp.getCurrentToken() != JsonToken.START_ARRAY) { + throw new DbAccessException("Expected rows to start with an Array"); + } + + while (jp.nextToken() == JsonToken.START_OBJECT) { + T doc = jp.readValueAs(type); + docs.add(doc); + } + + if (jp.currentToken() != JsonToken.END_ARRAY) { + throw new DbAccessException("Cannot parse response from CouchDB. Unexpected data."); + } + } + + public List<T> getDocs() { + return docs; + } +} diff --git a/src/main/java/de/thm/arsnova/persistance/couchdb/support/MangoResponseHandler.java b/src/main/java/de/thm/arsnova/persistance/couchdb/support/MangoResponseHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..462555e7abd12682836bfcc1d223a5b68ab9970e --- /dev/null +++ b/src/main/java/de/thm/arsnova/persistance/couchdb/support/MangoResponseHandler.java @@ -0,0 +1,32 @@ +package de.thm.arsnova.persistance.couchdb.support; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.ektorp.http.HttpResponse; +import org.ektorp.http.StdResponseHandler; +import org.ektorp.util.Assert; + +import java.util.List; + +public class MangoResponseHandler<T> extends StdResponseHandler<List<T>> { + + private MangoQueryResultParser<T> parser; + + public MangoResponseHandler(Class<T> docType, ObjectMapper om) { + Assert.notNull(om, "ObjectMapper may not be null"); + Assert.notNull(docType, "docType may not be null"); + parser = new MangoQueryResultParser<T>(docType, om); + } + + public MangoResponseHandler(Class<T> docType, ObjectMapper om, + boolean ignoreNotFound) { + Assert.notNull(om, "ObjectMapper may not be null"); + Assert.notNull(docType, "docType may not be null"); + parser = new MangoQueryResultParser<T>(docType, om); + } + + @Override + public List<T> success(HttpResponse hr) throws Exception { + parser.parseResult(hr.getContent()); + return parser.getDocs(); + } +}