Java tutorial
/******************************************************************************* * Copyright 2011 John Casey * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************/ package org.commonjava.couch.db; import static org.apache.commons.io.IOUtils.closeQuietly; import static org.apache.commons.io.IOUtils.copy; import static org.apache.http.HttpStatus.SC_CREATED; import static org.apache.http.HttpStatus.SC_NOT_FOUND; import static org.apache.http.HttpStatus.SC_OK; import static org.commonjava.couch.util.UrlUtils.buildUrl; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import javax.enterprise.event.Event; import javax.enterprise.inject.Alternative; import javax.inject.Named; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpHeaders; import org.apache.http.HttpResponse; import org.apache.http.StatusLine; import org.apache.http.client.methods.HttpDelete; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpHead; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; import org.apache.http.entity.InputStreamEntity; import org.apache.http.entity.StringEntity; import org.apache.log4j.Logger; import org.commonjava.couch.change.j2ee.ApplicationEvent; import org.commonjava.couch.change.j2ee.DatabaseEvent; import org.commonjava.couch.conf.CouchDBConfiguration; import org.commonjava.couch.db.action.BulkActionHolder; import org.commonjava.couch.db.action.CouchDocumentAction; import org.commonjava.couch.db.action.DeleteAction; import org.commonjava.couch.db.action.StoreAction; import org.commonjava.couch.db.handler.SerializedGetHandler; import org.commonjava.couch.db.model.AppDescription; import org.commonjava.couch.db.model.AttachmentDownload; import org.commonjava.couch.db.model.CouchDocRefSet; import org.commonjava.couch.db.model.CouchObjectList; import org.commonjava.couch.db.model.ViewRequest; import org.commonjava.couch.io.CouchAppReader; import org.commonjava.couch.io.CouchHttpClient; import org.commonjava.couch.io.Serializer; import org.commonjava.couch.io.json.CouchObjectListDeserializer; import org.commonjava.couch.model.Attachment; import org.commonjava.couch.model.CouchApp; import org.commonjava.couch.model.CouchDocRef; import org.commonjava.couch.model.CouchDocument; import org.commonjava.couch.model.CouchError; import org.commonjava.couch.model.DenormalizedCouchDoc; import org.commonjava.couch.util.ToString; import com.google.gson.reflect.TypeToken; @Named("dont-use-directly") @Alternative public class CouchManager { private static final Logger LOGGER = Logger.getLogger(CouchManager.class); private static final String REV = "rev"; private static final String VIEW_BASE = "_view"; private static final String APP_BASE = "_design"; private static final String BULK_DOCS = "_bulk_docs"; private static final String ALL_DOCS = "_all_docs"; private ExecutorService exec; private final CouchAppReader appReader; private final CouchDBConfiguration config; private final CouchHttpClient client; private Event<DatabaseEvent> dbEvent; private Event<ApplicationEvent> appEvent; private final Serializer serializer; public CouchManager(final CouchDBConfiguration config, final CouchHttpClient client, final Serializer serializer, final CouchAppReader appReader) { this.config = config; this.client = client; this.serializer = serializer; this.appReader = appReader; } public CouchManager(final CouchDBConfiguration config) { this.config = config; this.serializer = new Serializer(); this.appReader = new CouchAppReader(); this.client = new CouchHttpClient(config, serializer); } public CouchManager(final CouchDBConfiguration config, final CouchHttpClient client, final Serializer serializer, final CouchAppReader appReader, final Event<DatabaseEvent> dbEvent, final Event<ApplicationEvent> appEvent) { this.config = config; this.client = client; this.serializer = serializer; this.appReader = appReader; this.dbEvent = dbEvent; this.appEvent = appEvent; } public void initialize(final AppDescription description) throws CouchDBException { CouchApp app; try { app = appReader.readAppDefinition(description); } catch (final IOException e) { throw new CouchDBException("Failed to retrieve application definition: %s. Reason: %s", e, description.getClasspathAppResource(), e.getMessage()); } if (!dbExists()) { createDatabase(); } else { LOGGER.info("Database already exists: " + config.getDatabaseUrl()); } if (!appExists(description.getAppName())) { installApplication(app); } else { LOGGER.info("App: " + app.getCouchDocId() + " already exists in db: " + config.getDatabaseUrl()); } } public void store(final Collection<? extends CouchDocument> documents, final boolean skipIfExists, final boolean allOrNothing) throws CouchDBException { final Set<StoreAction> toStore = new HashSet<StoreAction>(); for (final CouchDocument doc : documents) { if (doc instanceof DenormalizedCouchDoc) { ((DenormalizedCouchDoc) doc).calculateDenormalizedFields(); } final boolean revExists = documentRevisionExists(doc); if (skipIfExists && revExists) { continue; } toStore.add(new StoreAction(doc, skipIfExists)); } modify(toStore, allOrNothing); // threadedExecute( toStore, dbUrl ); } public void delete(final Collection<? extends CouchDocument> documents, final boolean allOrNothing) throws CouchDBException { final Set<DeleteAction> toDelete = new HashSet<DeleteAction>(); for (final CouchDocument doc : documents) { if (doc instanceof DenormalizedCouchDoc) { ((DenormalizedCouchDoc) doc).calculateDenormalizedFields(); } if (!documentRevisionExists(doc)) { continue; } toDelete.add(new DeleteAction(doc)); } modify(toDelete, allOrNothing); // threadedExecute( toDelete, dbUrl ); } public void modify(final Collection<? extends CouchDocumentAction> actions, final boolean allOrNothing) throws CouchDBException { for (final CouchDocumentAction action : actions) { final CouchDocument doc = action.getDocument(); if (doc instanceof DenormalizedCouchDoc) { ((DenormalizedCouchDoc) doc).calculateDenormalizedFields(); } } final BulkActionHolder bulk = new BulkActionHolder(actions, allOrNothing); final String body = serializer.toString(bulk); String url; try { url = buildUrl(config.getDatabaseUrl(), (Map<String, String>) null, BULK_DOCS); } catch (final MalformedURLException e) { throw new CouchDBException("Failed to format bulk-update URL: %s", e, e.getMessage()); } final HttpPost request = new HttpPost(url); try { request.setEntity(new StringEntity(body, "application/json", "UTF-8")); } catch (final UnsupportedEncodingException e) { throw new CouchDBException("Failed to encode POST entity for bulk update: %s", e, e.getMessage()); } try { final HttpResponse response = client.executeHttpWithResponse(request, "Bulk update failed"); final StatusLine statusLine = response.getStatusLine(); final int code = statusLine.getStatusCode(); if (code != SC_OK && code != SC_CREATED) { String content = null; final HttpEntity entity = response.getEntity(); if (entity != null) { final ByteArrayOutputStream baos = new ByteArrayOutputStream(); InputStream in = null; try { in = entity.getContent(); copy(in, baos); } catch (final IOException e) { throw new CouchDBException("Error reading response content for error: %s\nError was: %s", e, statusLine, e.getMessage()); } finally { closeQuietly(in); } content = new String(baos.toByteArray()); } throw new CouchDBException("Bulk operation failed. Status line: %s\nContent:\n----------\n\n%s", statusLine, content); } } finally { client.cleanup(request); } // threadedExecute( new HashSet<CouchDocumentAction>( actions ), dbUrl ); } public <T> List<T> getViewListing(final ViewRequest req, final Class<T> itemType) throws CouchDBException { if (CouchDocument.class.isAssignableFrom(itemType)) { req.setParameter(ViewRequest.INCLUDE_DOCS, true); } final String url = buildViewUrl(req); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Retrieving view listing from: " + url); } final HttpGet request = new HttpGet(url); final TypeToken<CouchObjectList<T>> tt = new TypeToken<CouchObjectList<T>>() { }; final CouchObjectListDeserializer<T> deser = new CouchObjectListDeserializer<T>(tt, itemType, false); final CouchObjectList<T> listing = client.executeHttpAndReturn(request, new TypeToken<CouchObjectList<T>>() { }.getType(), new ToString("Failed to retrieve contents for view request: %s", req), deser); for (final T t : listing) { if (t instanceof DenormalizedCouchDoc) { ((DenormalizedCouchDoc) t).calculateDenormalizedFields(); } } return listing.getItems(); } public <V> V getView(final ViewRequest req, final Class<V> type) throws CouchDBException { final String url = buildViewUrl(req); final HttpGet request = new HttpGet(url); return client.executeHttpAndReturn(request, type, new ToString("Failed to retrieve contents for view request: %s", req)); } public <T extends CouchDocument> List<T> getDocuments(final Class<T> docType, final Set<CouchDocRef> refs) throws CouchDBException { final CouchDocRefSet refSet = new CouchDocRefSet(refs); return getDocuments(docType, refSet); } public <T extends CouchDocument> List<T> getDocuments(final Class<T> docType, final CouchDocRef... refs) throws CouchDBException { return getDocuments(docType, false, refs); } public <T extends CouchDocument> List<T> getDocuments(final Class<T> docType, final boolean allowMissing, final CouchDocRef... refs) throws CouchDBException { if (refs == null || refs.length < 1) { return null; } final CouchDocRefSet refSet = new CouchDocRefSet(refs); return getDocuments(docType, refSet, allowMissing); } public <T extends CouchDocument> List<T> getDocuments(final Class<T> docType, final CouchDocRefSet refSet) throws CouchDBException { return getDocuments(docType, refSet, false); } public <T extends CouchDocument> List<T> getDocuments(final Class<T> docType, final CouchDocRefSet refSet, final boolean allowMissing) throws CouchDBException { String url; try { url = buildUrl(config.getDatabaseUrl(), Collections.singletonMap(ViewRequest.INCLUDE_DOCS, "true"), ALL_DOCS); } catch (final MalformedURLException e) { throw new CouchDBException("Failed to format multi-doc URL: %s", e, e.getMessage()); } if (LOGGER.isDebugEnabled()) { LOGGER.debug("Selecting multiple documents from: " + url); } final HttpPost request = new HttpPost(url); try { final String body = serializer.toString(refSet); request.setEntity(new StringEntity(body, "application/json", "UTF-8")); } catch (final UnsupportedEncodingException e) { throw new CouchDBException("Failed to encode POST entity for multi-document selection: %s", e, e.getMessage()); } final TypeToken<CouchObjectList<T>> tt = new TypeToken<CouchObjectList<T>>() { }; final CouchObjectListDeserializer<T> deser = new CouchObjectListDeserializer<T>(tt, docType, allowMissing); final CouchObjectList<T> listing = client.executeHttpAndReturn(request, new TypeToken<CouchObjectList<T>>() { }.getType(), new ToString("Failed to retrieve documents for: %s", refSet), deser); for (final T t : listing) { if (t instanceof DenormalizedCouchDoc) { ((DenormalizedCouchDoc) t).calculateDenormalizedFields(); } } return listing.getItems(); } public <T extends CouchDocument> T getDocument(final CouchDocRef ref, final Class<T> docType) throws CouchDBException { if (!documentRevisionExists(ref)) { return null; } final String url = buildDocUrl(ref, true); final HttpGet get = new HttpGet(url); final T result = client.executeHttpAndReturn(get, new SerializedGetHandler<T>(serializer, docType), new ToString("Failed to retrieve document: %s", ref)); if (result instanceof DenormalizedCouchDoc) { ((DenormalizedCouchDoc) result).calculateDenormalizedFields(); } return result; } public boolean store(final CouchDocument doc, final boolean skipIfExists) throws CouchDBException { if (doc instanceof DenormalizedCouchDoc) { ((DenormalizedCouchDoc) doc).calculateDenormalizedFields(); } final boolean revExists = documentRevisionExists(doc); if (skipIfExists && revExists) { return false; } final HttpPost request = new HttpPost(config.getDatabaseUrl()); try { request.setHeader("Referer", config.getDatabaseUrl()); final String src = serializer.toString(doc); request.setEntity(new StringEntity(src, "application/json", "UTF-8")); client.executeHttp(request, SC_CREATED, "Failed to store document"); } catch (final UnsupportedEncodingException e) { throw new CouchDBException("Failed to store document: %s.\nReason: %s", e, doc, e.getMessage()); } return true; } public void delete(final CouchDocument doc) throws CouchDBException { if (doc instanceof DenormalizedCouchDoc) { ((DenormalizedCouchDoc) doc).calculateDenormalizedFields(); } if (!documentRevisionExists(doc)) { return; } final String url = buildDocUrl(doc, true); final HttpDelete request = new HttpDelete(url); client.executeHttp(request, SC_OK, "Failed to delete document"); } public void attach(final CouchDocument doc, final Attachment attachment) throws CouchDBException { if (!documentRevisionExists(doc)) { throw new CouchDBException("Cannot attach to a non-existent document: %s", doc.getCouchDocId()); } String url; try { url = buildUrl(config.getDatabaseUrl(), Collections.singletonMap(REV, doc.getCouchDocRev()), doc.getCouchDocId(), attachment.getName()); } catch (final MalformedURLException e) { throw new CouchDBException("Failed to format attachment URL for: %s to document: %s. Error: %s", e, attachment.getName(), doc.getCouchDocId(), e.getMessage()); } LOGGER.info("Attaching " + attachment.getName() + " to document: " + doc.getCouchDocId() + "\nURL: " + url); final HttpPut request = new HttpPut(url); request.setHeader(HttpHeaders.CONTENT_TYPE, attachment.getContentType()); try { request.setEntity(new InputStreamEntity(attachment.getData(), attachment.getContentLength())); } catch (final IOException e) { throw new CouchDBException("Failed to read attachment data: %s. Error: %s", e, attachment.getName(), e.getMessage()); } client.executeHttp(request, SC_CREATED, "Failed to attach to document"); } public void deleteAttachment(final CouchDocument doc, final String attachmentName) throws CouchDBException { doc.setCouchDocRev(null); if (!documentRevisionExists(doc)) { throw new CouchDBException("Cannot delete attachment from a non-existent document: %s", doc.getCouchDocId()); } String url; try { url = buildUrl(config.getDatabaseUrl(), Collections.singletonMap(REV, doc.getCouchDocRev()), doc.getCouchDocId(), attachmentName); } catch (final MalformedURLException e) { throw new CouchDBException("Failed to format attachment URL for: %s to document: %s. Error: %s", e, attachmentName, doc.getCouchDocId(), e.getMessage()); } final HttpDelete request = new HttpDelete(url); client.executeHttp(request, SC_OK, "Failed to delete attachment"); } public Attachment getAttachment(final CouchDocument doc, final String attachmentName) throws CouchDBException { String url; try { url = buildUrl(config.getDatabaseUrl(), doc.getCouchDocId(), attachmentName); } catch (final MalformedURLException e) { throw new CouchDBException("Failed to format attachment URL for: %s to document: %s. Error: %s", e, attachmentName, doc.getCouchDocId(), e.getMessage()); } final HttpGet request = new HttpGet(url); final HttpResponse response = client.executeHttpWithResponse(request, "Failed to retrieve attachment."); if (response.getStatusLine().getStatusCode() == SC_NOT_FOUND) { return null; } else if (response.getStatusLine().getStatusCode() != SC_OK) { throw new CouchDBException("Failed to retrieve attachment: %s from: %s. Reason: %s", attachmentName, doc.getCouchDocId(), response.getStatusLine()); } return new AttachmentDownload(attachmentName, request, response, client); } public boolean viewExists(final String appName, final String viewName) throws CouchDBException { try { return exists(buildUrl(config.getDatabaseUrl(), (Map<String, String>) null, APP_BASE, appName, VIEW_BASE, viewName)); } catch (final MalformedURLException e) { throw new CouchDBException("Cannot format view URL for: %s in: %s. Reason: %s", e, viewName, appName, e.getMessage()); } catch (final CouchDBException e) { throw new CouchDBException("Cannot verify existence of view: %s in: %s. Reason: %s", e, viewName, appName, e.getMessage()); } } public boolean appExists(final String appName) throws CouchDBException { try { return exists(buildUrl(config.getDatabaseUrl(), (Map<String, String>) null, APP_BASE, appName)); } catch (final MalformedURLException e) { throw new CouchDBException("Cannot format application URL: %s. Reason: %s", e, appName, e.getMessage()); } catch (final CouchDBException e) { throw new CouchDBException("Cannot verify existence of application: %s. Reason: %s", e, appName, e.getMessage()); } } public boolean documentRevisionExists(final CouchDocument doc) throws CouchDBException { if (doc instanceof DenormalizedCouchDoc) { ((DenormalizedCouchDoc) doc).calculateDenormalizedFields(); } final String docUrl = buildDocUrl(doc, doc.getCouchDocRev() != null); boolean exists = false; final HttpHead request = new HttpHead(docUrl); try { final HttpResponse response = client.executeHttpWithResponse(request, "Failed to ping database URL"); final StatusLine statusLine = response.getStatusLine(); if (statusLine.getStatusCode() == SC_OK) { exists = true; } else if (statusLine.getStatusCode() != SC_NOT_FOUND) { final HttpEntity entity = response.getEntity(); CouchError error; try { error = serializer.toError(entity); } catch (final IOException e) { throw new CouchDBException( "Failed to ping database URL: %s.\nReason: %s\nError: Cannot read error status: %s", e, docUrl, statusLine, e.getMessage()); } throw new CouchDBException("Failed to ping database URL: %s.\nReason: %s\nError: %s", docUrl, statusLine, error); } if (exists) { final Header etag = response.getFirstHeader("Etag"); String rev = etag.getValue(); if (rev.startsWith("\"") || rev.startsWith("'")) { rev = rev.substring(1); } if (rev.endsWith("\"") || rev.endsWith("'")) { rev = rev.substring(0, rev.length() - 1); } doc.setCouchDocRev(rev); } } finally { client.cleanup(request); } return exists; } public boolean exists(final CouchDocument doc) throws CouchDBException { if (doc instanceof DenormalizedCouchDoc) { ((DenormalizedCouchDoc) doc).calculateDenormalizedFields(); } final String docUrl = buildDocUrl(doc, false); return exists(docUrl); } public boolean exists(final String path) throws CouchDBException { boolean exists = false; String url; try { url = buildUrl(config.getDatabaseUrl(), path); } catch (final MalformedURLException e) { throw new CouchDBException("Invalid path: %s. Reason: %s", e, path, e.getMessage()); } final HttpHead request = new HttpHead(url); try { final HttpResponse response = client.executeHttpWithResponse(request, "Failed to ping database URL"); final StatusLine statusLine = response.getStatusLine(); if (statusLine.getStatusCode() == SC_OK) { exists = true; } else if (statusLine.getStatusCode() != SC_NOT_FOUND) { final HttpEntity entity = response.getEntity(); CouchError error; try { error = serializer.toError(entity); } catch (final IOException e) { throw new CouchDBException( "Failed to ping database URL: %s.\nReason: %s\nError: Cannot read error status: %s", e, url, statusLine, e.getMessage()); } throw new CouchDBException("Failed to ping database URL: %s.\nReason: %s\nError: %s", url, statusLine, error); } } finally { client.cleanup(request); } return exists; } public boolean dbExists() throws CouchDBException { return exists("/"); } public void dropDatabase() throws CouchDBException { if (!dbExists()) { return; } final HttpDelete request = new HttpDelete(config.getDatabaseUrl()); client.executeHttp(request, SC_OK, "Failed to drop database"); fireDBEvent(DatabaseEvent.Type.DROP, config.getDatabaseUrl()); } public void createDatabase() throws CouchDBException { LOGGER.info("Creating database: " + config.getDatabaseUrl()); final HttpPut request = new HttpPut(config.getDatabaseUrl()); client.executeHttp(request, SC_CREATED, "Failed to create database"); fireDBEvent(DatabaseEvent.Type.CREATE, config.getDatabaseUrl()); } public void installApplication(final CouchApp app) throws CouchDBException { final String url = buildDocUrl(app, true); LOGGER.info("Installing app at: " + url); final HttpPut request = new HttpPut(url); try { request.setHeader("Referer", config.getDatabaseUrl()); final String appJson = serializer.toString(app); request.setEntity(new StringEntity(appJson, "application/json", "UTF-8")); client.executeHttp(request, SC_CREATED, "Failed to store application document"); fireAppEvent(ApplicationEvent.Type.INSTALL, app.getDescription()); } catch (final UnsupportedEncodingException e) { throw new CouchDBException("Failed to store application document: %s.\nReason: %s", e, app, e.getMessage()); } } protected String buildViewUrl(final ViewRequest req) throws CouchDBException { try { return buildUrl(config.getDatabaseUrl(), req.getRequestParameters(), APP_BASE, req.getApplication(), VIEW_BASE, req.getView()); } catch (final MalformedURLException e) { throw new CouchDBException("Failed to format view URL for: %s.\nReason: %s", e, req, e.getMessage()); } } protected String buildDocUrl(final CouchDocument doc, final boolean includeRevision) throws CouchDBException { if (doc instanceof DenormalizedCouchDoc) { ((DenormalizedCouchDoc) doc).calculateDenormalizedFields(); } try { String url; if (includeRevision && doc.getCouchDocRev() != null) { final Map<String, String> params = Collections.singletonMap(REV, doc.getCouchDocRev()); url = buildUrl(config.getDatabaseUrl(), params, doc.getCouchDocId()); } else { url = buildUrl(config.getDatabaseUrl(), (Map<String, String>) null, doc.getCouchDocId()); } return url; } catch (final MalformedURLException e) { throw new CouchDBException("Failed to format document URL for id: %s [revision=%s].\nReason: %s", e, doc.getCouchDocId(), doc.getCouchDocRev(), e.getMessage()); } } protected synchronized void threadedExecute(final Set<? extends CouchDocumentAction> actions) throws CouchDBException { if (exec == null) { exec = Executors.newCachedThreadPool(); } final CountDownLatch latch = new CountDownLatch(actions.size()); for (final CouchDocumentAction action : actions) { action.prepareExecution(latch, this); exec.execute(action); } synchronized (latch) { while (latch.getCount() > 0) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Waiting for " + latch.getCount() + " actions to complete."); } try { latch.await(2, TimeUnit.SECONDS); } catch (final InterruptedException e) { break; } } } final List<Throwable> errors = new ArrayList<Throwable>(); for (final CouchDocumentAction action : actions) { if (action.getError() != null) { errors.add(action.getError()); } } if (!errors.isEmpty()) { throw new CouchDBException("Failed to execute %d actions.", errors.size()).withNestedErrors(errors); } } protected CouchAppReader getAppReader() { return appReader; } private void fireDBEvent(final DatabaseEvent.Type type, final String url) { if (dbEvent != null) { dbEvent.fire(new DatabaseEvent(type, url)); } } private void fireAppEvent(final ApplicationEvent.Type type, final AppDescription description) { if (appEvent != null) { appEvent.fire(new ApplicationEvent(type, description)); } } }