Package ru.dreamteam.couch

Source Code of ru.dreamteam.couch.Db

package ru.dreamteam.couch;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.logging.Logger;

import org.apache.commons.lang3.StringUtils;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
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.client.utils.URIBuilder;
import org.apache.http.entity.BasicHttpEntity;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;

import static org.apache.http.HttpStatus.*;
import ru.dreamteam.couch.changes.ChangesQuery;
import ru.dreamteam.couch.query.Query;
import ru.dreamteam.couch.util.JSONUtils;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;

/**
* Base class to work with documents in a database.<br/>
* To create a view in the database you need:
* <ul>
* <li> Create directory structure {@code views/[DBNAME]} in the root of classpath
* <li> Add {@code [VIEWNAME].json} file
* <li> Place your functions in this file
* </ul>
* See examples in tests.
* Views will be updated from json files each time the constructor of {@code Db} class executes.
*
* <br/>Date: 15.02.13
* @author DPokidov
*/
public class Db {
    @SuppressWarnings("unused")
    private final Logger log = Logger.getLogger(Db.class.getName());

    private String dbName;
    private Couch dbInstance;
    private ObjectMapper mapper;
    private String dbInfo;

    Db(Couch dbInstance, String dbName) {
        this.mapper = JSONUtils.createMapper();
        this.dbInstance = dbInstance;
        this.dbName = dbName;
        this.dbInfo = String.format("Host: %s, port: %d, db: %s", dbInstance.getHost(), dbInstance.getPort(), dbName);
        new DbManager(dbName, dbInstance, mapper).updateViews();
    }

    /**
     * Saves a document to the database.
     * Also, it will be setting a new modifyTime for {@code obj}
     * @param obj a document to save
     */
    public <T extends CouchEntity> void save(final T obj) {
        final List<Attachment> attachments = prepareObjForSave(obj);

        dbInstance.execute(new HttpCall<T>() {
            @Override
            public HttpRequest getRequest() throws URISyntaxException, IOException {
                byte[] content = mapper.writeValueAsBytes(obj);
                HttpPut put = new HttpPut(buildUri("/" + dbName + "/" + obj.getId()));
                prepareCommandBody(put, content);
                return put;
            }

            @Override
            public T doWithResponse(HttpResponse response) throws IOException {
                processSaveResponse(Arrays.asList(obj), response);
                saveObjAttachments(obj, attachments);
                return obj;
            }
           
        }, SC_OK, SC_CREATED);
    }

    private static class BulkWrapper {
        @JsonProperty("docs")
        private List<?> docs;

        public void setDocs(List<?> docs) {
            this.docs = docs;
        }
    }
    /**
     * Create or update objects in Couchdb using bulk POST request.
     * @param objs documents to save
     */
    public <T extends CouchEntity> void bulkSave(final List<T> objs) {
        final List<List<Attachment>> attachments = new ArrayList<>();
        for (CouchEntity e : objs) {
            attachments.add(prepareObjForSave(e));
        }

        dbInstance.execute(new HttpCall<Void>() {
            @Override
            public HttpRequest getRequest() throws URISyntaxException,
                    IOException {
                BulkWrapper wrapper = new BulkWrapper();
                wrapper.setDocs(objs);
               
                byte[] content = toBytes(wrapper);
                HttpPost command = new HttpPost(buildUri("/" + dbName + "/_bulk_docs"));
                prepareCommandBody(command, content);
                return command;
            }

            @Override
            public Void doWithResponse(HttpResponse response)
                    throws IOException {
                processSaveResponse(objs, response);

                for (int i = 0; i < objs.size(); i++) {
                    saveObjAttachments(objs.get(i), attachments.get(i));
                }
                return null;
            }
        }, SC_OK, SC_CREATED);
    }

    private <T extends CouchEntity> void processSaveResponse(List<T> objs, HttpResponse response) throws IOException {
        String responseText = "";
        responseText = EntityUtils.toString(response.getEntity());
        JsonNode node = mapper.readTree(responseText);
        if (node.isArray()) {
            updateObjsRev(objs, node);
        } else {
            updateObjRev(objs.get(0), node);
        }
    }

    private <T extends CouchEntity> void updateObjsRev(List<T> objs, JsonNode response) {
        for (int i = 0; i < objs.size(); i++) {
            CouchEntity obj = objs.get(i);
            updateObjRev(obj, response.get(i));
        }
    }

    private <T extends CouchEntity> void updateObjRev(T obj, JsonNode response) {
        String revision = response.get("rev").asText();
        obj.setRevision(revision);
    }

    private <T extends CouchEntity> void saveObjAttachments(T obj, List<Attachment> attachments) {
        if (attachments == null) {
            return;
        }
        for (Attachment a : attachments) {
            saveAttach(obj, a);
            obj.setRevision(getLastRevision(obj.getId()));
        }
    }

    private <T extends CouchEntity> List<Attachment> prepareObjForSave(T obj) {
        if (obj.getId() == null || obj.getId().isEmpty()) {
            obj.setId(UUID.randomUUID().toString());
        }
        List<Attachment> attachments = null;
        if (!StringUtils.isEmpty(obj.getId()) && obj.getAttachments() != null) {
            attachments = new ArrayList<>();
            for (String fileName : obj.getAttachments()) {
                attachments.add(getAttach(obj.getId(), fileName));
            }
        }
        obj.setModifyTime(new Date().getTime());
       
        return attachments;
    }

    private void prepareCommandBody(HttpEntityEnclosingRequestBase request, byte[] content) {
        BasicHttpEntity entity = new BasicHttpEntity();
        entity.setContentType("application/json");
        entity.setContent(new ByteArrayInputStream(content));
        entity.setContentLength(content.length);
        request.setEntity(entity);
    }

    /**
     * Gets a document with a specific id.
     * This method loads only information about attachments without data.
     * It's a shorthand method for {@link #getById(id, null, clazz)}
     * @param id id of the document
     * @param clazz class that representing this document
     * @return document or {@code null} if document with specific {@code id} does not exist
     */
    public <T extends CouchEntity> T getById(final String id, final Class<T> clazz) {
        return getById(id, null, clazz);
    }

    /**
     * Gets a document with a specific id and revision.
     * This method loads only information about attachments without data.
     * @param id id of the document
     * @param clazz class that representing this document
     * @return document or {@code null} if document with specific {@code id} does not exist
     */
    public <T extends CouchEntity> T getById(final String id, final String rev, final Class<T> clazz) {
        return dbInstance.execute(new HttpCall<T>() {
            @Override
            public HttpRequest getRequest() throws URISyntaxException, IOException {
                String path = "/" + dbName + "/" + id;
                URI uri = null;
                if (StringUtils.isNotBlank(rev)) {
                    uri = buildUri(path, new BasicNameValuePair(CouchConstants.REV, rev));
                } else {
                    uri = buildUri(path);
                }
                return new HttpGet(uri);
            }

            @Override
            public T doWithResponse(HttpResponse response) throws IOException {
                if (response.getStatusLine().getStatusCode() == 404) {
                    return null;
                }
                return mapper.readValue(response.getEntity().getContent(), clazz);
            }
        }, SC_OK, SC_NOT_FOUND);
    }

    /**
     * Return information about all revisions for document
     * @param id document id
     * @return list of revisions
     */
    public List<RevisionInfo> getRevisions(final String id) {
        return dbInstance.execute(new HttpCall<List<RevisionInfo>>() {
            @Override
            public HttpRequest getRequest() throws URISyntaxException,
                    IOException {
                return new HttpGet(buildUri("/" + dbName + "/" + id, new BasicNameValuePair(CouchConstants.REVS_INFO, "true")));
            }

            @Override
            public List<RevisionInfo> doWithResponse(HttpResponse response)
                    throws IOException {
                String responseBody = EntityUtils.toString(response.getEntity(), "UTF-8");
                JsonNode node = mapper.readTree(responseBody);
                JsonNode revsJson = node.get("_revs_info");
                Iterator<JsonNode> revsJsonIt = revsJson.elements();
                List<RevisionInfo> result = new ArrayList<>();
                while (revsJsonIt.hasNext()) {
                    RevisionInfo revisionInfo = mapper.readValue(revsJsonIt.next().toString(), RevisionInfo.class);
                    result.add(revisionInfo);
                }
                return result;
            }
        }, SC_OK);
    }

    private class KeysWrapper {
        @JsonProperty("keys")
        private Set<String> keys;

        public KeysWrapper(Set<String> keys) {
            super();
            this.keys = keys;
        }
    }

    /**
     * Get documents by multiple keys using one request
     * @param clazz class of entity to get
     * @param ids list of ids
     * @return list of entities by ids
     */
    public <T extends CouchEntity> List<T> getByIds(final Class<T> clazz, final String... ids) {
        return dbInstance.execute(new HttpCall<List<T>>() {
            @Override
            public HttpRequest getRequest() throws URISyntaxException,
                    IOException {
                HttpPost command = new HttpPost(buildUri("/" + dbName + "/_all_docs", new BasicNameValuePair(CouchConstants.INCLUDE_DOCS, "true")));
                //Using LinkedHashSet to store ordering of ids
                prepareCommandBody(command, toBytes(new KeysWrapper(new LinkedHashSet<>(Arrays.asList(ids)))));
                return command;
            }

            @Override
            public List<T> doWithResponse(HttpResponse response) throws IOException {
                PagingResult<T> result = new PagingResult<>();
                JsonNode node = mapper.readTree(response.getEntity().getContent());
                if (node.get("total_rows") != null) {
                    result.setTotalRows(node.get("total_rows").asInt());
                }
                ArrayNode rowsNode = (ArrayNode) node.get("rows");
                for (JsonNode aRowsNode : rowsNode) {
                    if (aRowsNode.has("doc")) {
                        result.add(mapper.<T>readValue(aRowsNode.get("doc").toString(), clazz));
                    }
                }
                return result;
            }
        });
    }
   
    /**
     * Returns actual revision of a document
     * @param id id of the document
     * @return Actual revision of the document
     */
    public String getLastRevision(final String id) {
        return dbInstance.execute(new HttpCall<String>() {
            @Override
            public HttpRequest getRequest() throws URISyntaxException,
                    IOException {
                return new HttpHead(buildUri("/" + dbName + "/" + id));
            }

            @Override
            public String doWithResponse(HttpResponse response)
                    throws IOException {
                String etag = response.getHeaders("ETag")[0].getValue();
                return etag.substring(1, etag.length() - 1); //Remove quotes
            }
        });
    }

    /**
     * Saves or creates an attachment in a document {@code parent}
     * @param parent parent document for the attachment
     * @param attachs attachment object to store.
     * @return updated revision of entity
     */
    public String saveAttach(final CouchEntity parent, final Attachment attach) {
        return dbInstance.execute(new HttpCall<String>() {
            @Override
            public HttpRequest getRequest() throws URISyntaxException,
                    IOException {
                HttpPut command = new HttpPut(buildUri("/" + dbName + "/" + parent.getId() + "/" + attach.getId(),
                        new BasicNameValuePair(CouchConstants.REV, parent.getRevision())));
                BasicHttpEntity entity = new BasicHttpEntity();
                entity.setContentLength(attach.getData().length);
                entity.setContent(new ByteArrayInputStream(attach.getData()));
                entity.setContentType(attach.getContentType());
                command.setEntity(entity);
                return command;
            }

            @Override
            public String doWithResponse(HttpResponse response)
                    throws IOException {
                return getLastRevision(parent.getId());
            }
        }, SC_CREATED);
    }

    /**
     * Deletes an attachment from a document
     * @param parent document which store the attachment
     * @param attachId attachmentId to delete
     */
    public void deleteAttach(final CouchEntity parent, final String attachId) {
        dbInstance.execute(new HttpCall<Void>() {
            @Override
            public HttpRequest getRequest() throws URISyntaxException,
                    IOException {
                return new HttpDelete(buildUri("/" + dbName + "/" + parent.getId() + "/" + attachId,
                        new BasicNameValuePair(CouchConstants.REV, parent.getRevision())));
            }

            @Override
            public Void doWithResponse(HttpResponse response) throws IOException { return null; }
        });
    }

    /**
     * Deletes a document from a database <br>
     * This is a shorthand method for {@code delete(id, false)}
     * @param id id of the object
     * @return deleted document revision
     */
    public String delete(String id) {
        return delete(id, false);
    }

    /**
     * Delete an object from a database.<br>
     * Can be use with batch mode.
     * @param id id of an object
     * @param batch if {@code true} then couchdb will make in-memory delete only without immediately flush to disk
     * @return deleted document revision or {@code null} if it's not present (when perform batch delete, for example)
     */
    public String delete(final String id, final boolean batch) {
        return dbInstance.execute(new HttpCall<String>() {
            @Override
            public HttpRequest getRequest() throws URISyntaxException,
                    IOException {
                List<BasicNameValuePair> queryParams = new ArrayList<>();
                queryParams.add(new BasicNameValuePair(CouchConstants.REV, getLastRevision(id)));
                if (batch) {
                    queryParams.add(new BasicNameValuePair(CouchConstants.BATCH, "ok"));
                }
                URIBuilder builder = new URIBuilder(
                        buildUri("/" + dbName + "/" + id, queryParams.toArray(new BasicNameValuePair[queryParams.size()]))
                );
                URI uri = builder.build();
                return new HttpDelete(uri);
            }

            @Override
            public String doWithResponse(HttpResponse response)
                    throws IOException {
                Header[] etagHeader = response.getHeaders("ETag");
                if (etagHeader != null && etagHeader.length == 1) {
                    String etag = etagHeader[0].getValue();
                    return etag.substring(1, etag.length() - 1); // remove quotes
                }
                return null;
            }
        }, SC_OK, SC_ACCEPTED);
    }

    /**
     * Retrieves an attachment from a document.
     * @param entityId document id.
     * @param fileName filename of the attachment.
     * @return
     */
    public Attachment getAttach(final String entityId, final String fileName) {
        return dbInstance.execute(new HttpCall<Attachment>() {
            @Override
            public HttpRequest getRequest() throws URISyntaxException, IOException {
                return new HttpGet(buildUri("/" + dbName + "/" + entityId + "/" + fileName));
            }

            @Override
            public Attachment doWithResponse(HttpResponse response) throws IOException {
                Attachment attachment = new Attachment();
                HttpEntity entity = response.getEntity();
                attachment.setId(fileName);
                attachment.setContentType(entity.getContentType().getValue());
                attachment.setData(EntityUtils.toByteArray(entity));
                return attachment;
            }
        });
    }

    /**
     * Purge revs of specified document.
     * If no revisions specified then purge all revisions of document.
     * @param id document id
     * @param revs list of revisions to purge
     */
    public void purge(String id, String... revs) {
        final Map<String, List<String>> purgeEntity = new HashMap<>();
        if (revs == null || revs.length == 0) {
            List<RevisionInfo> revsList = getRevisions(id);
            List<String> revsStrList = new ArrayList<>(revsList.size());
            for (RevisionInfo r : revsList) {
                revsStrList.add(r.getRev());
            }
            purgeEntity.put(id, revsStrList);
        } else {
            purgeEntity.put(id, Arrays.asList(revs));
        }
        dbInstance.execute(new HttpCall<Void>() {
            @Override
            public HttpRequest getRequest() throws URISyntaxException, IOException {
                HttpPost command = new HttpPost(buildUri("/" + dbName + "/_purge"));
                prepareCommandBody(command, mapper.writeValueAsBytes(purgeEntity));
                return command;
            }

            @Override
            public Void doWithResponse(HttpResponse response) throws IOException { return null; }
        });
    }


    /**
     * Creates a new database query
     * @param view name of the view
     * @param viewFunc function name in the view
     * @param clazz class of the result documents
     * @return new query object
     */
    public <T> Query<T> query(String view, String viewFunc, Class<T> clazz) {
        return new Query<>(dbInstance, "/" + dbName + "/_design/" + view + "/_view/" + viewFunc, clazz);
    }

    /**
     * Creates a new changes query.
     * @return new changes query object.
     */
    public ChangesQuery changes() {
        return new ChangesQuery(dbInstance, dbName);
    }

    /**
     * Shorthand method for query(view, viewFunc, clazz).list();
     * @param view
     * @param viewFunc
     * @param clazz
     * @param <T>
     * @return
     */
    public <T> PagingResult<T> list(String view, String viewFunc, Class<T> clazz) {
        return new Query<>(dbInstance, "/" + dbName + "/_design/" + view + "/_view/" + viewFunc, clazz).list();
    }

    @Override
    public String toString() {
        return dbInfo;
    }

    public static byte[] toBytes(Object couchEntity, ObjectMapper mapper) {
        try {
            return mapper.writeValueAsBytes(couchEntity);
        } catch (IOException e) {
            throw new MarshallingException(e);
        }
    }

    private byte[] toBytes(Object couchEntity) {
        return toBytes(couchEntity, mapper);
    }

    public static URI buildUri(String path, BasicNameValuePair... params) throws URISyntaxException {
        URIBuilder uriBuilder = new URIBuilder();
        uriBuilder.setPath(path);
        if (params != null) {
            for (BasicNameValuePair p : params) {
                uriBuilder.addParameter(p.getName(), p.getValue());
            }
        }
        return uriBuilder.build();
    }
}
TOP

Related Classes of ru.dreamteam.couch.Db

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.