/* * Copyright Siemens AG, 2014-2015. Part of the SW360 Portal Project. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ 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.ektorp.*; import org.ektorp.http.HttpClient; import org.ektorp.impl.StdCouchDbConnector; import org.ektorp.util.Documents; import java.net.MalformedURLException; import java.util.*; import java.util.function.Function; import java.util.function.Supplier; /** * Database Connector to a CouchDB database * * @author cedric.bodet@tngtech.com */ public class DatabaseConnector extends StdCouchDbConnector { private static final Logger log = Logger.getLogger(DatabaseConnector.class); private final String dbName; private final DatabaseInstance instance; private String adminRole = "_admin"; /** * Create a connection to the database * * @param httpClient HttpClient with authentication of CouchDB server * @param dbName name of the database on the CouchDB server */ public DatabaseConnector(HttpClient httpClient, String dbName) throws MalformedURLException { this(httpClient, dbName, new MapperFactory()); } /** * Create a connection to the database * * @param httpClient Supplier<HttpClient> with authentication of CouchDB server * @param dbName name of the database on the CouchDB server */ public DatabaseConnector(Supplier<HttpClient> httpClient, String dbName) throws MalformedURLException { this(httpClient.get(), dbName, new MapperFactory()); } /** * Create a connection to the database * * @param httpClient HttpClient with authentication of CouchDB server * @param dbName name of the database on the CouchDB server * @param mapperFactory Specific mapper factory to use for serialization */ public DatabaseConnector(HttpClient httpClient, String dbName, MapperFactory mapperFactory) throws MalformedURLException { this(dbName, new DatabaseInstance(httpClient), mapperFactory); } private DatabaseConnector(String dbName, DatabaseInstance instance, MapperFactory mapperFactory) throws MalformedURLException { super(dbName, instance, mapperFactory); this.instance = instance; this.dbName = dbName; // Create the database if it does not exists yet instance.createDatabase(dbName); restrictAccessToAdmins(); } public Optional<Status> restrictAccessToAdmins() { boolean hasChanged = false; Function<SecurityGroup,SecurityGroup> addAdminRole = securityGroup -> { List<String> newGroupRoles = securityGroup.getRoles(); newGroupRoles.add(adminRole); return new SecurityGroup(securityGroup.getNames(), newGroupRoles); }; Security security = Optional.ofNullable(getSecurity()) .orElse(new Security()); SecurityGroup adminGroup = Optional.ofNullable(security.getAdmins()) .orElse(new SecurityGroup()); SecurityGroup memberGroup = Optional.ofNullable(security.getMembers()) .orElse(new SecurityGroup()); if(!adminGroup.getRoles().contains(adminRole)){ adminGroup = addAdminRole.apply(adminGroup); hasChanged = true; } if(!memberGroup.getRoles().contains(adminRole)){ memberGroup = addAdminRole.apply(memberGroup); hasChanged = true; } if(hasChanged){ return Optional.of(updateSecurity(new Security(adminGroup,memberGroup))); } return Optional.empty(); } /** * Creates the Object as a document in the database. If the id is not set it will be generated by the database. */ public <T> boolean add(T document) { try { super.create(document); return true; } catch (UpdateConflictException e) { log.warn("Update conflict exception while adding object!", e); return false; } catch (IllegalArgumentException e) { log.warn("Illegal argument exception while adding document", e); return false; } } /** * Get an object of class type from the database and deserialize it. */ public <T> T get(Class<T> type, String id) { try { return super.get(type, id); } catch (DocumentNotFoundException e) { log.info("Document not found for ID: " + id); return null; } catch (DbAccessException e) { log.error("Document ID " + id + " could not be successfully converted to " + type.getName(), e); return null; } } /** * Get a list of documents from their IDs. All documents should be of the same type. */ public <T> List<T> get(Class<T> type, Collection<String> ids) { if (ids == null) return Collections.emptyList(); // Copy to set in order to avoid duplicates Set<String> idSet = ImmutableSet.copyOf(ids); ViewQuery q = new ViewQuery() .allDocs() .includeDocs(true) .keys(idSet); return queryView(q, type); } @Override public void update(Object document) { if (document != null) { try { final Class documentClass = document.getClass(); if (ThriftUtils.isMapped(documentClass)) { DocumentWrapper wrapper = getDocumentWrapper(document, documentClass); if (wrapper != null) { super.update(wrapper); } } else { super.update(document); } } catch (UpdateConflictException | IllegalArgumentException e) { log.error("Document cannot be updated " + document, e); } } } @SuppressWarnings("unchecked") private DocumentWrapper getDocumentWrapper(Object document, Class documentClass) { final Class<? extends DocumentWrapper> wrapperClass = ThriftUtils.getWrapperClass(documentClass); final String documentId = Documents.getId(document); DocumentWrapper wrapper = get(wrapperClass, documentId); if (wrapper == null || !wrapper.getClass().equals(wrapperClass)) { log.error("document " + documentId + " cannot be wrapped"); return null; } if (!wrapper.getId().equals(documentId)) { log.error("round trip from database is not identity for id " + documentId); return null; } if (!wrapper.getRevision().equals(Documents.getRevision(document))) { log.error("concurrent access to document " + documentId); return null; } wrapper.updateNonMetadata(document); return wrapper; } /** * Returns true if the database contains a document with the given ID. */ public boolean contains(String id) { return (id != null) && super.contains(id); } /** * Delete the document with the given id. Returns true if the delete was successful. */ public boolean deleteById(String id) { if (super.contains(id)) { String rev = super.getCurrentRevision(id); super.delete(id, rev); return true; } return false; } public String getDbName() { return dbName; } public DatabaseInstance getInstance() { return instance; } /** * Deletes all objects in the supplied collection. * * @param deletionCandidates , the objects that will be deleted * @return The list will only contain entries for documents that has any kind of error code returned from CouchDB. * i.e. the list will be empty if everything was completed successfully. */ protected List<DocumentOperationResult> deleteBulk(Collection<?> deletionCandidates) { List<Object> operations = new ArrayList<>(); for (Object candidate : deletionCandidates) { operations.add(BulkDeleteDocument.of(candidate)); } return executeBulk(operations); } public <T> List<DocumentOperationResult> deleteIds(Collection<String> ids, Class<T> type) { final List<T> deletionCandidates = get(type, ids); return deleteBulk(deletionCandidates); } }