Commit 84cb2a5b authored by Daniel Gerhardt's avatar Daniel Gerhardt

Merge branch 'couchdb-mango-queries' into 'master'

Add support for CouchDB Mango queries

See merge request !72
parents 5cdd9520 12f6fdba
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