Commit 12f6fdba authored by Daniel Gerhardt's avatar Daniel Gerhardt

Add support for CouchDB Mango queries

Ektorp, the library used as connector to CouchDB, currently does not
support features introduced with CouchDB 2.0. These changes implement
support for Mango API's `_find` which provides a more flexible
alternative to views for data retrieval.

See http://docs.couchdb.org/en/stable/api/database/find.html.
parent 5cdd9520
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<>();
......
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;
}
}
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;
}
}
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();
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment