Skip to content
This repository has been archived by the owner on Jul 25, 2018. It is now read-only.

feat(couchdb): support for linked documents in couchdb #596

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,16 @@ public class ComponentRepository extends SummaryAwareRepository<Component> {
" }" +
" }";

private static final String WITH_LINKED_RELEASES =
"function(doc) {" +
" if (doc.type == 'component') {"
+ " emit(doc._id, null);" +
" for(var i in doc.releaseIds) {" +
" emit(doc._id, { _id : doc.releaseIds[i] } );" +
" }" +
" }" +
"}";

public ComponentRepository(DatabaseConnector db, ReleaseRepository releaseRepository, VendorRepository vendorRepository) {
super(Component.class, db, new ComponentSummary(releaseRepository, vendorRepository));

Expand Down Expand Up @@ -184,4 +194,20 @@ public int getTotalComponentsCount(){
ViewResult result = db.queryView(query);
return result.getTotalRows();
}

@View(name = "withLinkedReleases", map = WITH_LINKED_RELEASES)
public List<Component> getWithReleases() {
ViewQuery viewQuery = createQuery("withLinkedReleases").includeDocs(true);
return getConnector().queryWithLinkedDocuments(viewQuery, getType(), "component");
}

@View(name = "withLinkedReleases", map = WITH_LINKED_RELEASES)
public Component getWithReleases(String id) {
ViewQuery viewQuery = createQuery("withLinkedReleases").includeDocs(true).key(id);
List<Component> components = getConnector().queryWithLinkedDocuments(viewQuery, getType(), "component");
if (components.isEmpty()) {
return null;
}
return components.get(0);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package org.eclipse.sw360.datahandler.db;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.eclipse.sw360.datahandler.TestUtils;
import org.eclipse.sw360.datahandler.common.DatabaseSettings;
import org.eclipse.sw360.datahandler.couchdb.DatabaseConnector;
import org.eclipse.sw360.datahandler.couchdb.DatabaseMixIn;
import org.eclipse.sw360.datahandler.couchdb.MapperFactory;
import org.eclipse.sw360.datahandler.couchdb.annotation.LinkedDocuments;
import org.eclipse.sw360.datahandler.thrift.components.Component;
import org.eclipse.sw360.datahandler.thrift.components.Release;
import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

import java.util.List;
import java.util.Set;

import static org.eclipse.sw360.datahandler.TestUtils.assertTestString;

public class LinkedDocumentLoadingTest {

private static final String dbName = DatabaseSettings.COUCH_DB_DATABASE;

private ComponentRepository componentRepository;
private ReleaseRepository releaseRepository;
private VendorRepository vendorRepository;

@Before
public void setUp() throws Exception {
assertTestString(dbName);
// assertTestString(attachmentsDbName);

TestUtils.createDatabase(DatabaseSettings.getConfiguredHttpClient(), dbName);

// Prepare the database
DatabaseConnector databaseConnector = new DatabaseConnector(DatabaseSettings.getConfiguredHttpClient(), dbName,
new TestMapperFactory());

vendorRepository = new VendorRepository(databaseConnector);
releaseRepository = new ReleaseRepository(databaseConnector, vendorRepository);
componentRepository = new ComponentRepository(databaseConnector, releaseRepository, vendorRepository);
}

@After
public void tearDown() throws Exception {
// Delete the database
TestUtils.deleteDatabase(DatabaseSettings.getConfiguredHttpClient(), dbName);
}

@Test
public void testReleaseResolvingForOneComponentWithoutRelease() {
Component component = new Component("Component 1").setId("c1");
componentRepository.add(component);

// check that releases are not resolved by default
component = componentRepository.get("c1");
Assert.assertNotNull(component);
Assert.assertNull(component.getReleaseIds());
Assert.assertNull(component.getReleases());

// check that releases are resolved with special method
component = componentRepository.getWithReleases("c1");
Assert.assertNotNull(component);
Assert.assertNull(component.getReleaseIds());
Assert.assertNull(component.getReleases());
}

@Test
public void testReleaseResolvingForOneComponent() {
Release release1 = new Release("r1", "1.0", "c1").setId("r1");
releaseRepository.add(release1);

Release release2 = new Release("r2", "2.0", "c1").setId("r2");
releaseRepository.add(release2);

Component component = new Component("Component 1").setId("c1");
component.addToReleaseIds("r1");
component.addToReleaseIds("r2");
componentRepository.add(component);

// check that releases are not resolved by default
component = componentRepository.get("c1");
Assert.assertNotNull(component);
Assert.assertThat(component.getReleaseIds(), Matchers.containsInAnyOrder("r1", "r2"));
Assert.assertNull(component.getReleases());

// check that releases are resolved with special method
component = componentRepository.getWithReleases("c1");
Assert.assertNotNull(component);
Assert.assertThat(component.getReleaseIds(), Matchers.containsInAnyOrder("r1", "r2"));
Assert.assertThat(component.getReleases(), Matchers.containsInAnyOrder(release1, release2));
}

@Test
public void testReleaseResolvingForMoreComponents() {
Release release1 = new Release("r1", "1.0", "c1").setId("r1");
releaseRepository.add(release1);

Release release2 = new Release("r2", "2.0", "c1").setId("r2");
releaseRepository.add(release2);

Release release3 = new Release("r3", "2.0", "c3").setId("r3");
releaseRepository.add(release3);

Component component1 = new Component("Component 1").setId("c1");
component1.addToReleaseIds("r1");
component1.addToReleaseIds("r2");
componentRepository.add(component1);

Component component2 = new Component("Component 2").setId("c2");
component2.addToReleaseIds("r2");
component2.addToReleaseIds("r3");
componentRepository.add(component2);

// check that releases are resolved with special method
List<Component> components = componentRepository.getWithReleases();
for (Component component : components) {
if (component.getId().equals("c1")) {
Assert.assertThat(component.getReleaseIds(), Matchers.containsInAnyOrder("r1", "r2"));
Assert.assertThat(component.getReleases(), Matchers.containsInAnyOrder(release1, release2));
}
if (component.getId().equals("c2")) {
Assert.assertThat(component.getReleaseIds(), Matchers.containsInAnyOrder("r2", "r3"));
Assert.assertThat(component.getReleases(), Matchers.containsInAnyOrder(release2, release3));
}
}
}

private static class TestMapperFactory extends MapperFactory {
@Override
public ObjectMapper createObjectMapper() {
ObjectMapper objectMapper = super.createObjectMapper();
objectMapper.addMixInAnnotations(Component.class, ComponentMixin.class);
return objectMapper;
}
}

private abstract static class ComponentMixin extends DatabaseMixIn {
@LinkedDocuments(targetField = "releases")
private Set<String> releaseIds;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
*
* <pre>
* try {
* x.stream.filter(element -> wrap(() -> element.test())).collect(Collectors.toList());
* x.stream.filter(element -> wrapException(() -> element.test())).collect(Collectors.toList());
* } catch (WrappedException e) {
* // e.getCause() contains original exception
* }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@
package org.eclipse.sw360.datahandler.couchdb;

import com.google.common.collect.ImmutableSet;
import org.eclipse.sw360.datahandler.thrift.ThriftUtils;
import org.apache.log4j.Logger;
import org.eclipse.sw360.datahandler.couchdb.annotation.LinkedDocument;
import org.eclipse.sw360.datahandler.couchdb.annotation.LinkedDocuments;
import org.eclipse.sw360.datahandler.thrift.ThriftUtils;
import org.ektorp.*;
import org.ektorp.http.HttpClient;
import org.ektorp.impl.StdCouchDbConnector;
import org.ektorp.util.Assert;
import org.ektorp.util.Documents;

import java.net.MalformedURLException;
Expand Down Expand Up @@ -68,6 +71,10 @@ public DatabaseConnector(HttpClient httpClient, String dbName, MapperFactory map
this(dbName, new DatabaseInstance(httpClient), mapperFactory);
}

public DatabaseConnector(Supplier<HttpClient> httpClient, String dbName, MapperFactory mapperFactory) throws MalformedURLException {
this(httpClient.get(), dbName, mapperFactory);
}

private DatabaseConnector(String dbName, DatabaseInstance instance, MapperFactory mapperFactory) throws MalformedURLException {
super(dbName, instance, mapperFactory);
this.instance = instance;
Expand Down Expand Up @@ -253,4 +260,42 @@ public <T> List<DocumentOperationResult> deleteIds(Collection<String> ids, Class
final List<T> deletionCandidates = get(type, ids);
return deleteBulk(deletionCandidates);
}

/**
* Loads a query that is prepared to load linked documents as well. In order to
* use this method, fields must be annotated with {@link LinkedDocument} or
* {@link LinkedDocuments} and the view must be prepared with a map function
* like:
*
* <pre>
* function(doc) {
* if(doc.type == 'project') {
* emit(doc._id, null);
* for(var i in doc.releaseIds) {
* if(doc.releaseIds[i]._id) {
* emit(doc._id, { _id: doc.releaseIds[i]._id });
* }
* }
* }
* }
* </pre>
*
* @param viewQuery
* the query to execute
*
* @param type
* type of the main class that should be loaded
* @param typeName
* name of main document type. This document must contain a field
* named "type"
*
* @return list of loaded documents
*/
public <T> List<T> queryWithLinkedDocuments(ViewQuery viewQuery, Class<T> type, String typeName) {
Assert.notNull(viewQuery, "query may not be null");
viewQuery.dbPath(dbURI.toString());

LinkedDocumentViewResponseHandler<T> responseHandler = new LinkedDocumentViewResponseHandler<T>(type, typeName, objectMapper);
return executeQuery(viewQuery, responseHandler);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
// Always put _id and _rev upfront. Not required, but serialized objects then look nicer.
@JsonIgnoreProperties({"optionals", "_attachments"})
@SuppressWarnings("unused")
public class DatabaseMixIn {
public abstract class DatabaseMixIn {

@JsonProperty("issetBitfield")
private byte __isset_bitfield = 0;
Expand All @@ -37,23 +37,14 @@ public class DatabaseMixIn {
*/

@JsonProperty(KEY_ID)
public String getId() {
return null;
}
public abstract String getId();

@JsonProperty(KEY_ID)
public void setId(String id) {
// No implementation necessary
}
public abstract void setId(String id);

@JsonProperty(KEY_REV)
public String getRevision() {
return null;
}
public abstract String getRevision();

@JsonProperty(KEY_REV)
public void setRevision(String revision) {
// No implementation necessary
}

public abstract void setRevision(String revision);
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ protected DatabaseConnector getConnector(){
return connector;
}

protected Class<T> getType() {
return type;
}

public static Set<String> getIds(ViewResult rows) {
HashSet<String> ids = new HashSet<>();

Expand Down
Loading