package org.jvalue.ods.db; import com.fasterxml.jackson.core.JsonPointer; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; import org.ektorp.CouchDbConnector; import org.ektorp.DocumentNotFoundException; import org.ektorp.DocumentOperationResult; import org.ektorp.ViewQuery; import org.ektorp.ViewResult; import org.ektorp.support.CouchDbRepositorySupport; import org.ektorp.support.DesignDocument; import org.ektorp.support.DesignDocumentFactory; import org.ektorp.support.StdDesignDocumentFactory; import org.jvalue.commons.couchdb.DbConnectorFactory; import org.jvalue.commons.utils.Assert; import org.jvalue.ods.api.data.Cursor; import org.jvalue.ods.api.data.Data; import org.jvalue.ods.api.views.DataView; import java.util.Collection; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; public final class DataRepository extends CouchDbRepositorySupport<JsonNode> { private static final String DESIGN_DOCUMENT_NAME = "Data"; private static final String DESIGN_DOCUMENT_ID = "_design/" + DESIGN_DOCUMENT_NAME; private static final DesignDocumentFactory designFactory = new StdDesignDocumentFactory(); private final CouchDbConnector connector; private final DataView domainIdView; private final DataView revAndIdByDomainIdView; @Inject DataRepository(DbConnectorFactory dbConnectorFactory, @Assisted String databaseName, @Assisted JsonPointer domainIdKey) { super(JsonNode.class, dbConnectorFactory.createConnector(databaseName, true), DESIGN_DOCUMENT_NAME); this.connector = dbConnectorFactory.createConnector(databaseName, true); initStandardDesignDocument(); domainIdView = createObjectByDomainIdView(domainIdKey); if (!containsView(domainIdView)) addView(domainIdView); revAndIdByDomainIdView = createIdAndRevByDomainIdView(domainIdKey); if (!containsView(revAndIdByDomainIdView)) addView(revAndIdByDomainIdView); DataView allView = createAllView(domainIdKey); if (!containsView(allView)) addView(allView); } public JsonNode findByDomainId(String domainId) { List<JsonNode> resultList = executeQuery(domainIdView, domainId); if (resultList.isEmpty()) throw new DocumentNotFoundException(domainId); else if (resultList.size() == 1) return resultList.get(0); else throw new IllegalStateException("found more than one element for given domain id"); } public List<JsonNode> executeQuery(DataView view, String param) { if (param == null) return queryView(view.getId()); else return queryView(view.getId(), param); } public void addView(DataView dataView) { Assert.assertNotNull(dataView); DesignDocument designDocument; boolean update = false; if (connector.contains(DESIGN_DOCUMENT_ID)) { designDocument = connector.get(DesignDocument.class, DESIGN_DOCUMENT_ID); update = true; } else { designDocument = designFactory.newDesignDocumentInstance(); designDocument.setId(DESIGN_DOCUMENT_ID); } DesignDocument.View view; if (dataView.getReduceFunction() == null) view = new DesignDocument.View(dataView.getMapFunction()); else view = new DesignDocument.View(dataView.getMapFunction(), dataView.getReduceFunction()); designDocument.addView(dataView.getId(), view); if (update) connector.update(designDocument); else connector.create(designDocument); } public void removeView(DataView view) { Assert.assertNotNull(view); if (!connector.contains(DESIGN_DOCUMENT_ID)) return; DesignDocument designDocument = connector.get(DesignDocument.class, DESIGN_DOCUMENT_ID); designDocument.removeView(view.getId()); connector.update(designDocument); } public boolean containsView(DataView dataView) { Assert.assertNotNull(dataView); if (!connector.contains(DESIGN_DOCUMENT_ID)) return false; DesignDocument designDocument = connector.get(DesignDocument.class, DESIGN_DOCUMENT_ID); return designDocument.containsView(dataView.getId()); } public Map<String, JsonNode> executeBulkGet(Collection<String> ids) { ViewQuery query = new ViewQuery() .designDocId(DESIGN_DOCUMENT_ID) .viewName(domainIdView.getId()) .includeDocs(true) .keys(ids); Map<String, JsonNode> nodes = new HashMap<>(); for (ViewResult.Row row : connector.queryView(query).getRows()) { nodes.put(row.getKey(), row.getDocAsNode()); } return nodes; } public Collection<DocumentOperationResult> executeBulkCreateAndUpdate(Collection<JsonNode> data) { return connector.executeBulk(data); } /** * Cursor based pagination of the data in this repository based on ascending sorted domain ids. * * @param startDomainId the start id of the requested page or null if the result should start * at the first entry. * @param count how many entries should be in the page. Must be > 0. */ public Data executePaginatedGet(String startDomainId, int count) { ViewQuery query = new ViewQuery() .designDocId(DESIGN_DOCUMENT_ID) .viewName(domainIdView.getId()) .includeDocs(true) .descending(false) .limit(count + 1); if (startDomainId != null) query = query.startKey(startDomainId); List<JsonNode> result = new LinkedList<>(); int resultCount = 0; String nextStartDomainId = null; boolean hasNext = false; for (ViewResult.Row row : connector.queryView(query).getRows()) { if (resultCount == count) { hasNext = true; nextStartDomainId = row.getKey(); break; } result.add(row.getDocAsNode()); ++resultCount; } Cursor cursor = new Cursor(nextStartDomainId, hasNext, resultCount); return new Data(result, cursor); } public void removeAll() { ViewQuery query = new ViewQuery() .designDocId(DESIGN_DOCUMENT_ID) .viewName(revAndIdByDomainIdView.getId()); Collection<JsonNode> deletedObjects = new LinkedList<>(); for (ViewResult.Row row : connector.queryView(query).getRows()) { ObjectNode node = (ObjectNode) row.getValueAsNode(); node.put("_deleted", true); deletedObjects.add(node); } executeBulkCreateAndUpdate(deletedObjects); } /** * Compacts the underlying CouchDb database by removing old revisions of documents. */ public void compact() { connector.compact(); } private DataView createObjectByDomainIdView(JsonPointer domainIdKey) { String viewName = "findObjectByDomainId"; String domainIdProperty = createDomainIdJavascriptProperty(domainIdKey); StringBuilder mapBuilder = new StringBuilder(); mapBuilder.append("function(doc) { if ("); mapBuilder.append(domainIdProperty); mapBuilder.append(") emit("); mapBuilder.append(domainIdProperty); mapBuilder.append(", doc) }"); return new DataView(viewName, mapBuilder.toString()); } private DataView createIdAndRevByDomainIdView(JsonPointer domainIdKey) { String viewName = "findIdAndRevByDomainId"; String domainIdProperty = createDomainIdJavascriptProperty(domainIdKey); StringBuilder mapBuilder = new StringBuilder(); mapBuilder.append("function(doc) { if ("); mapBuilder.append(domainIdProperty); mapBuilder.append(") emit("); mapBuilder.append(domainIdProperty); mapBuilder.append(", { _id : doc._id, _rev : doc._rev }) }"); return new DataView(viewName, mapBuilder.toString()); } private DataView createAllView(JsonPointer domainIdKey) { String viewName = "all"; String domainIdProperty = createDomainIdJavascriptProperty(domainIdKey); StringBuilder mapBuilder = new StringBuilder(); mapBuilder.append("function(doc) { if ("); mapBuilder.append(domainIdProperty); mapBuilder.append(" != null) emit(null,doc) }"); return new DataView(viewName, mapBuilder.toString()); } private String createDomainIdJavascriptProperty(JsonPointer domainIdKey) { StringBuilder keyBuilder = new StringBuilder(); keyBuilder.append("doc"); JsonPointer pointer = domainIdKey; while (pointer != null && !pointer.toString().isEmpty()) { if (pointer.mayMatchProperty()) { keyBuilder.append("."); keyBuilder.append(pointer.getMatchingProperty()); } else { keyBuilder.append("["); keyBuilder.append(pointer.getMatchingIndex()); keyBuilder.append("]"); } pointer = pointer.tail(); } return keyBuilder.toString(); } }