
prerna.io.connector.couch.CouchUtil Maven / Gradle / Ivy
The newest version!
package prerna.io.connector.couch;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import javax.imageio.ImageIO;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
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.client.methods.HttpUriRequest;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import prerna.auth.utils.AbstractSecurityUtils;
import prerna.auth.utils.SecurityProjectUtils;
import prerna.cluster.util.ClusterUtil;
import prerna.engine.api.IEngine;
import prerna.masterdatabase.utility.MasterDatabaseUtility;
import prerna.util.AssetUtility;
import prerna.util.Constants;
import prerna.util.EngineUtility;
import prerna.util.Utility;
import prerna.util.insight.InsightUtility;
import prerna.util.insight.TextToGraphic;
/**
* Utility class to handle image operations in a partitioned CouchDB database.
* This allows downloads, uploads, and deletion of images in the partitions for
* databases, projects, and insights. One should verify that CouchDB is enabled
* before using this class via the
* {@link CouchUtil#COUCH_ENABLED} flag.
*/
public class CouchUtil {
public static final String DATABASE = "database";
public static final String STORAGE = "storage";
public static final String MODEL = "model";
public static final String VECTOR = "vector";
public static final String FUNCTION = "function";
public static final String GUARDRAIL = "guardrail";
public static final String INSIGHT = "insight";
public static final String PROJECT = "project";
private static final Logger classLogger = LogManager.getLogger(CouchUtil.class);
private static final String DIR_SEPARATOR = java.nio.file.FileSystems.getDefault().getSeparator();
private static final String COUCH_ENABLED_KEY = "COUCH_ENABLED";
/**
* boolean indicator that CouchDB is configured to be enabled
*/
public static final boolean COUCH_ENABLED =
StringUtils.isEmpty(Utility.getDIHelperProperty(COUCH_ENABLED_KEY))
?
(
System.getenv().containsKey(COUCH_ENABLED_KEY)
?
Boolean.parseBoolean(System.getenv(COUCH_ENABLED_KEY))
:
false
)
:
Boolean.parseBoolean(Utility.getDIHelperProperty(COUCH_ENABLED_KEY))
;
private static final String COUCH_ENDPOINT_KEY = "COUCH_ENDPOINT";
private static final String COUCH_ENDPOINT =
StringUtils.isEmpty(Utility.getDIHelperProperty(COUCH_ENDPOINT_KEY))
?
(
System.getenv().containsKey(COUCH_ENDPOINT_KEY)
?
System.getenv(COUCH_ENDPOINT_KEY)
:
""
)
:
Utility.getDIHelperProperty(COUCH_ENDPOINT_KEY)
;
private static final String COUCH_CREDS_KEY = "COUCH_CREDS";
private static final String COUCH_CREDS =
StringUtils.isEmpty(Utility.getDIHelperProperty(COUCH_CREDS_KEY))
?
(
System.getenv().containsKey(COUCH_CREDS_KEY)
?
System.getenv(COUCH_CREDS_KEY)
:
""
)
:
Utility.getDIHelperProperty(COUCH_CREDS_KEY)
;
private static final String COUCH_AUTH = "Basic "
+ new String(Base64.encodeBase64(COUCH_CREDS.getBytes(StandardCharsets.ISO_8859_1)));
private static final ObjectMapper MAPPER = new ObjectMapper();
/**
* Retrieve the image attachment data from a CouchDB document in the given
* partition with matching field data. The entries of the map are used to form a
* document selector used to query CouchDB for matching documents in the
* partition. If a document is found, the attachment data is retrieved.
* Otherwise, a new document with a default image attachment is created. The
* retrieved or created image data is used to build a JAX-RS Response object to
* download it.
*
* @param partitionId The partition of the database to query for document
* attachments
* @param referenceData A map whose key-value pairs are used to build a document
* selector
* @return A download {@link Response} containing the
* bytes of the attachment
* @see CouchUtil#getSelectorString
* @see CouchUtil#retrieveDocumentsInPartitionForSelector
* @see CouchUtil#retrieveDocument
* @see CouchUtil#createDefault
* @throws IllegalArgumentException If the referenceData map is null or empty
* @throws CouchException If another exception is encountered
*/
public static Response download(String partitionId, Map referenceData) throws CouchException {
if(referenceData == null || referenceData.isEmpty()) {
throw new IllegalArgumentException("Selector list is empty");
}
String selector = getSelectorString(referenceData);
String documentId = null;
ObjectNode docJson = null;
String attachmentId = null;
try {
CouchResponse findResponse = retrieveDocumentsInPartitionForSelector(partitionId, selector);
JsonNode findRespJson = MAPPER.readTree(findResponse.getResponseBody());
docJson = (ObjectNode) findRespJson.path("docs").get(0);
if(docJson != null) {
documentId = docJson.path("_id").textValue();
JsonNode attachmentsJson = docJson.get("_attachments");
if(attachmentsJson != null) {
Iterator fields = attachmentsJson.fieldNames();
if(fields.hasNext()) {
attachmentId = fields.next();
}
}
}
} catch (JsonProcessingException e) {
throw new CouchException("Error parsing document search response", e);
}
byte[] attachmentBytes = null;
if(documentId != null && attachmentId != null) {
CouchResponse documentResponse = retrieveDocument(documentId, true);
try {
JsonNode docAttJson = MAPPER.readTree(documentResponse.getResponseBody());
String attachmentData = docAttJson.path("_attachments").path(attachmentId).path("data").textValue();
attachmentBytes = Base64.decodeBase64(attachmentData);
} catch (JsonProcessingException e) {
throw new CouchException("Error parsing document search response", e);
}
} else {
if(docJson == null) {
docJson = MAPPER.createObjectNode();
for(String key : referenceData.keySet()) {
docJson.put(key, referenceData.get(key));
}
}
attachmentBytes = createDefault(partitionId, docJson);
}
String eTag = null;
try {
eTag = new String(Base64.encodeBase64(MessageDigest.getInstance("MD5").digest(attachmentBytes)));
} catch (NoSuchAlgorithmException e) {
classLogger.error("Error building byte digest", e);
}
ResponseBuilder builder = Response.ok(attachmentBytes)
.header("Content-Disposition", "attachment; filename=\"" + attachmentId + "\"");
if(eTag != null) {
builder = builder.tag(eTag);
}
return builder.build();
}
/**
* Upload image data from a File to CouchDB with the given partition and
* document field data. The entries of the map are used to form a document
* selector used to query CouchDB for matching documents in the partition. If a
* document is found, it is updated to contain the fields and attachment
* provided. Otherwise, a new document with an attachment of the imageFile
* contents is created.
*
* @param partitionId The partition of the database that will contain the
* document
* @param referenceData A map whose key-value pairs are placed in the document's
* fields
* @param imageFile A {@link File} whose contents
* will be attached to the document
*
* @see CouchUtil#getSelectorString
* @see CouchUtil#retrieveDocumentsInPartitionForSelector
* @see CouchUtil#updateDocument
* @throws IllegalArgumentException If the referenceData map is null or invalid
* for the partition
* @throws CouchException If another exception is encountered
*/
public static void upload(String partitionId, Map referenceData, File imageFile) throws CouchException {
if(referenceData == null || !referenceData.containsKey(partitionId)) {
throw new IllegalArgumentException("Upload data is missing required value for key: " + partitionId);
}
try {
String selector = getSelectorString(referenceData);
String documentId;
String revisionId;
ObjectNode docJson = null;
CouchResponse findResponse = retrieveDocumentsInPartitionForSelector(partitionId, selector);
JsonNode findRespJson = MAPPER.readTree(findResponse.getResponseBody());
docJson = (ObjectNode) findRespJson.path("docs").get(0);
if(docJson != null) {
documentId = docJson.path("_id").textValue();
revisionId = docJson.path("_rev").textValue();
} else {
documentId = partitionId + ":" + UUID.randomUUID().toString();
revisionId = null;
docJson = MAPPER.createObjectNode();
for(String key : referenceData.keySet()) {
docJson.put(key, referenceData.get(key));
}
}
String contentType;
String fileExtension = FilenameUtils.getExtension(imageFile.getName()).trim();
if(fileExtension == null || fileExtension.isEmpty()) {
contentType = "application/octet-stream";
} else {
contentType = "image/"+fileExtension;
}
String attachmentName = "image." + fileExtension;
updateDocument(documentId, revisionId, docJson, attachmentName,
contentType, FileUtils.readFileToByteArray(imageFile), true);
} catch (IOException e) {
classLogger.error(Constants.STACKTRACE, e);
throw new CouchException("Error processing upload");
}
}
/**
* Upload image data from a FileItem to CouchDB with the given partition and
* document field data. The entries of the map are used to form a document
* selector used to query CouchDB for matching documents in the partition. If a
* document is found, it is updated to contain the fields and attachment
* provided. Otherwise, a new document with an attachment of the imageFile
* contents is created.
*
* @param partitionId The partition of the database that will contain the
* document
* @param referenceData A map whose key-value pairs are placed in the document's
* fields
* @param imageFile A {@link FileItem} whose contents
* will be attached to the document
*
* @see CouchUtil#getSelectorString
* @see CouchUtil#retrieveDocumentsInPartitionForSelector
* @see CouchUtil#updateDocument
* @throws IllegalArgumentException If the referenceData map is null or invalid
* for the partition
* @throws CouchException If another exception is encountered
*/
public static void upload(String partitionId, Map referenceData, FileItem imageFile) throws CouchException {
if(referenceData == null || !referenceData.containsKey(partitionId)) {
throw new IllegalArgumentException("Upload data is missing required value for key: " + partitionId);
}
try {
String selector = getSelectorString(referenceData);
String documentId;
String revisionId;
ObjectNode docJson = null;
CouchResponse findResponse = retrieveDocumentsInPartitionForSelector(partitionId, selector);
JsonNode findRespJson = MAPPER.readTree(findResponse.getResponseBody());
docJson = (ObjectNode) findRespJson.path("docs").get(0);
if(docJson != null) {
documentId = docJson.path("_id").textValue();
revisionId = docJson.path("_rev").textValue();
} else {
documentId = partitionId + ":" + UUID.randomUUID().toString();
revisionId = null;
docJson = MAPPER.createObjectNode();
for(String key : referenceData.keySet()) {
docJson.put(key, referenceData.get(key));
}
}
String attachmentName = "image." + imageFile.getContentType().split("/")[1];
updateDocument(documentId, revisionId, docJson, attachmentName,
imageFile.getContentType(), IOUtils.toByteArray(imageFile.getInputStream()), true);
} catch (IOException e) {
classLogger.error(Constants.STACKTRACE, e);
throw new CouchException("Error processing upload");
}
}
/**
* Delete a document from CouchDB in the given partition with the matching
* document field data. The entries of the map are used to form a document
* selector used to query CouchDB for matching documents in the partition. If a
* document is found, it is deleted.
*
* @param partitionId The partition of the database that will contain the
* document
* @param referenceData A map whose key-value pairs are placed in the document's
* fields
*
* @see CouchUtil#getSelectorString
* @see CouchUtil#retrieveDocumentsInPartitionForSelector
* @see CouchUtil#deleteDocument
* @throws IllegalArgumentException If the referenceData map is null or empty
* @throws CouchException If another exception is encountered
*/
public static void delete(String partitionId, Map referenceData) throws CouchException {
if(referenceData == null || referenceData.isEmpty()) {
throw new IllegalArgumentException("Selector list is empty");
}
try {
String selector = getSelectorString(referenceData);
String documentId;
String revisionId;
CouchResponse findResponse = retrieveDocumentsInPartitionForSelector(partitionId, selector);
JsonNode findRespJson;
findRespJson = MAPPER.readTree(findResponse.getResponseBody());
JsonNode docJson = findRespJson.path("docs").get(0);
if(docJson == null) {
// if it isn't found then deletion is unnecessary. return as if successful.
classLogger.warn("Couch deletion call on missing document: " + selector);
return;
}
documentId = docJson.path("_id").textValue();
revisionId = docJson.path("_rev").textValue();
deleteDocument(documentId, revisionId);
} catch (JsonProcessingException e) {
classLogger.error(Constants.STACKTRACE, e);
throw new CouchException("Error processing delete", e);
}
}
/**
* Verify if a document with the given selector exists in the CouchDB partition.
*
* @param partitionId The partition of the database to query for the document
* @param searchSelector The JSON string of the search selector
* @return A boolean representing if the document exists or not
* @see CouchUtil#retrieveDocumentsInPartitionForSelector
* @throws IllegalArgumentException If the searchSelector is null or empty
* @throws CouchException If another exception is encountered
*/
public static boolean documentExists(String partitionId, String searchSelector) throws CouchException {
if(searchSelector == null || searchSelector.isEmpty()) {
throw new IllegalArgumentException("Invalid searchSelector");
}
try {
CouchResponse searchResponse = CouchUtil.retrieveDocumentsInPartitionForSelector(partitionId,
searchSelector);
JsonNode searchRespJson;
searchRespJson = MAPPER.readTree(searchResponse.getResponseBody());
ObjectNode docJson = (ObjectNode) searchRespJson.path("docs").get(0);
return docJson != null;
} catch (JsonProcessingException e) {
classLogger.error(Constants.STACKTRACE, e);
throw new CouchException("Error processing search", e);
}
}
/**
* Build a JSON selector string for the given map. The entries of the map are
* used to form a document selector with key = value over all key-value pairs in
* the map. The JSON created is of the form: {"selector": { "key1": {"$eq":
* "value1"}, "key2": {"$eq": "value2"}, ... }}
*
* @param referenceData A map whose key-value pairs are used to build the
* selector
* @return The selector JSON String
*/
private static String getSelectorString(Map referenceData) {
StringBuilder selectorBuilder = new StringBuilder("{\"selector\": {");
for(String key : referenceData.keySet()) {
String searchValue = referenceData.get(key);
selectorBuilder.append("\"").append(key).append("\":{\"$eq\":");
if(searchValue == null) {
selectorBuilder.append("null");
} else {
selectorBuilder.append("\"").append(searchValue).append("\"");
}
selectorBuilder.append("},");
}
selectorBuilder.replace(selectorBuilder.length()-1, selectorBuilder.length(), "");
selectorBuilder.append("}}");
return selectorBuilder.toString();
}
/**
* Build a default image for the given partition and data. If the provided
* documentData includes an _id, indicating a document already exists, a
* revision tag is sought for the eventual CouchDB update of the image
* attachment. The default image is created by first searching for a local image
* in the associated DB, project, and insight image locations. If found, the
* byte array contents are returned. Otherwise, an image is formed based on the
* database/project name or the insight layout type as appropriate. Before
* returning, the image is also uploaded to CouchDB for later use.
*
* @param partitionId The partition of the database that will contain the image
* @param documentData A {@link ObjectNode} with contents
* needed for the partition choice
* @return The byte[] of image contents
* @see CouchUtil#retrieveDocumentInfo
* @see CouchUtil#updateDocument
* @see InsightUtility#findImageFile
* @see TextToGraphic#buildBufferedImage
* @see AbstractSecurityUtils#getStockImage
* @throws CouchException If an exception is encountered
*/
private static byte[] createDefault(String partitionId, ObjectNode documentData) throws CouchException {
String documentId = null;
String revisionId = null;
if(documentData.has("_id")) {
documentId = documentData.get("_id").textValue();
if(documentData.has("_rev")) {
revisionId = documentData.get("_rev").textValue();
} else {
try {
CouchResponse infoCheckResponse = retrieveDocumentInfo(documentId);
revisionId = infoCheckResponse.getRevision();
} catch (CouchException e) {
if(e.getStatusCode() != HttpStatus.SC_NOT_FOUND) {
throw e;
}
revisionId = null;
}
}
} else {
documentId = partitionId + ":" + UUID.randomUUID().toString();
}
String databaseId = documentData.path(DATABASE).textValue();
String projectId = documentData.path(PROJECT).textValue();
String insightId = documentData.path(INSIGHT).textValue();
try {
String attachmentName;
String contentType;
byte[] fileContent;
if(DATABASE.equals(partitionId)) {
String databaseName = MasterDatabaseUtility.getDatabaseAliasForId(databaseId);
File[] images;
if(ClusterUtil.IS_CLUSTER) {
String imagePath = ClusterUtil.IMAGES_FOLDER_PATH
+ DIR_SEPARATOR + "databases";
images = InsightUtility.findImageFile(imagePath, databaseId);
} else {
String imagePath = EngineUtility.getSpecificEngineVersionFolder(
IEngine.CATALOG_TYPE.DATABASE,
databaseId,
databaseName);
images = InsightUtility.findImageFile(imagePath);
}
if(images != null && images.length > 0) {
File insightImageFile = images[0];
String extension = FilenameUtils.getExtension(insightImageFile.getName());
attachmentName = "image." + extension;
contentType = "image/" + extension;
fileContent = FileUtils.readFileToByteArray(insightImageFile);
} else {
attachmentName = "image.png";
contentType = "image/png";
BufferedImage img = TextToGraphic.buildBufferedImage(databaseName);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(img, "png", baos);
fileContent = baos.toByteArray();
}
} else if(PROJECT.equals(partitionId)) {
String projectName = SecurityProjectUtils.getProjectAliasForId(projectId);
File[] images;
if(ClusterUtil.IS_CLUSTER) {
String imagePath = ClusterUtil.IMAGES_FOLDER_PATH
+ DIR_SEPARATOR + "projects";
images = InsightUtility.findImageFile(imagePath, projectId);
} else {
String imagePath = AssetUtility.getProjectVersionFolder(projectName, projectId);
images = InsightUtility.findImageFile(imagePath);
}
if(images != null && images.length > 0) {
File insightImageFile = images[0];
String extension = FilenameUtils.getExtension(insightImageFile.getName());
attachmentName = "image." + extension;
contentType = "image/" + extension;
fileContent = FileUtils.readFileToByteArray(insightImageFile);
} else {
attachmentName = "image.png";
contentType = "image/png";
BufferedImage img = TextToGraphic.buildBufferedImage(projectName);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(img, "png", baos);
fileContent = baos.toByteArray();
}
} else {
String projectName = SecurityProjectUtils.getProjectAliasForId(projectId);
String imagePath = AssetUtility.getProjectVersionFolder(projectName, projectId)
+ DIR_SEPARATOR + insightId;
File[] images = InsightUtility.findImageFile(imagePath);
File insightImageFile = null;
if(images != null && images.length > 0) {
insightImageFile = images[0];
attachmentName = insightImageFile.getName();
String extension = FilenameUtils.getExtension(insightImageFile.getName());
contentType = "image/" + extension;
} else {
insightImageFile = AbstractSecurityUtils.getStockImage(projectId, insightId);
attachmentName = "image.png";
contentType = "image/png";
}
fileContent = FileUtils.readFileToByteArray(insightImageFile);
}
updateDocument(documentId, revisionId, documentData, attachmentName, contentType, fileContent, false);
return fileContent;
} catch (IOException e) {
classLogger.error(Constants.STACKTRACE, e);
throw new CouchException("Error processing image creation");
}
}
/**
* Call CouchDB with an HTTP HEAD request to /{db}/{docid} to lookup document
* information.
*
* @param documentId The document identifier used by CouchDB
* @return A {@link CouchResponse} wrapper for the HTTP
* response
* @see CouchUtil#executeRequest
* @see Couch
* API Reference on Document Info Lookup
* @throws CouchException If an exception is encountered during the request
*/
private static CouchResponse retrieveDocumentInfo(String documentId) throws CouchException {
HttpHead documentInfoGet = new HttpHead(COUCH_ENDPOINT + documentId);
CouchResponse response = executeRequest(documentInfoGet);
classLogger.debug("Successfully retrieved info: " + response.toString());
return response;
}
/**
* Call CouchDB with an HTTP PUT request to /{db}/{docid} to update the
* document. The provided revisionId, if non-null, is used as a query parameter
* in the PUT. The provided attachment data is used to update the _attachments
* portion of the documentData before the request is performed.
*
* @param documentId The document identifier for use by CouchDB
* @param revisionId Revision identifier used in the PUT when the
* document exists. Null otherwise
* @param documentData A {@link ObjectNode}
* whose contents are mapped to the CouchDB
* document
* @param attachmentName The name of the attachment as it will appear in
* the _attachments object. For example, image.png
* @param attachmentContentType The content type of the attachment. For example,
* image/png
* @param attachmentBytes The byte array contents of the attachment. These
* will be Base64 encoded before being added to the
* document data
* @param ignoreExtension Boolean if true, to ignore the extension and remove
* all attachments that contains the same file name
* @return A {@link CouchResponse} wrapper for the HTTP
* response
* @see CouchUtil#executeRequest
* @see Couch
* API Reference on Document Updates
* @throws IllegalArgumentException If the documentData fails to be encoded into
* the request
* @throws CouchException If another exception is encountered
*/
private static CouchResponse updateDocument(String documentId, String revisionId, ObjectNode documentData, String attachmentName,
String attachmentContentType, byte[] attachmentBytes, boolean ignoreExtension) throws CouchException {
String attachmentData = new String(Base64.encodeBase64(attachmentBytes));
ObjectNode attachmentsNode = null;
if(documentData.has("_attachments")) {
attachmentsNode = (ObjectNode) documentData.get("_attachments");
} else {
attachmentsNode = documentData.putObject("_attachments");
}
if(!ignoreExtension) {
if(attachmentsNode.has(attachmentName)) {
attachmentsNode.remove(attachmentName);
}
} else {
// loop through and remove all attachments with the same name (minus extension)
// as what we are uploading
String attachmentNameNoExt = attachmentName.substring(0, attachmentName.lastIndexOf('.'));
Set removeSet = new HashSet<>();
Iterator existingAttachmentsIt = attachmentsNode.fieldNames();
while(existingAttachmentsIt.hasNext()) {
String existingAttachment = existingAttachmentsIt.next();
String existingAttachmentNameNoExt = existingAttachment.substring(0, existingAttachment.lastIndexOf('.'));
if(existingAttachmentNameNoExt.equals(attachmentNameNoExt)) {
removeSet.add(existingAttachment);
}
}
// remove all the existing keys
if(!removeSet.isEmpty()) {
attachmentsNode.remove(removeSet);
}
}
// add the new attachment to the node
attachmentsNode = attachmentsNode
.putObject(attachmentName)
.put("content_type", attachmentContentType)
.put("data", attachmentData);
try {
HttpPut docCreate;
if(revisionId == null) {
docCreate = new HttpPut(COUCH_ENDPOINT + documentId);
} else {
docCreate = new HttpPut(COUCH_ENDPOINT + documentId + "?rev=" + revisionId);
}
docCreate.setEntity(new StringEntity(documentData.toString()));
CouchResponse response = executeRequest(docCreate);
classLogger.debug("Successful document creation: " + response.toString());
return response;
} catch (UnsupportedEncodingException e) {
classLogger.error(Constants.STACKTRACE, e);
throw new IllegalArgumentException("The document data encoding isn't supported");
}
}
/**
* Call CouchDB with an HTTP POST request to /_partition/{partitionId}/_find to
* search for documents in the partition matching the given selector string.
*
* @param partitionId The partition of the database to search for documents in
* @param selector A JSON selector string with the search criteria
* @return A {@link CouchResponse} wrapper for the HTTP
* response
* @see CouchUtil#executeRequest
* @see Couch
* API Reference on Document Search in Partition
* @see Couch
* API Reference on Selector Syntax
* @throws IllegalArgumentException If the selector fails to be encoded into the
* request
* @throws CouchException If another exception is encountered
*/
private static CouchResponse retrieveDocumentsInPartitionForSelector(String partitionId, String selector) throws CouchException {
try {
HttpPost findPost = new HttpPost(COUCH_ENDPOINT + "_partition/" + partitionId + "/_find");
findPost.setEntity(new StringEntity(selector));
// Explicitly tell CouchDB to expect a JSON to avoid 415 errors
findPost.setHeader(HttpHeaders.CONTENT_TYPE, "application/json");
CouchResponse response = executeRequest(findPost);
classLogger.debug("Successfully retrieved documents for selector: " + response.toString());
return response;
} catch (UnsupportedEncodingException e) {
classLogger.error(Constants.STACKTRACE, e);
throw new IllegalArgumentException("The selector encoding isn't supported");
}
}
/**
* Call CouchDB with an HTTP GET request to /{db}/{docId} to retrieve a document
* with the given identifier and the option to include attachment data.
*
* @param documentId The document identifier as used by CouchDB
* @param withAttachments A boolean flagging if the document attachment contents
* should be requested as well
* @return A {@link CouchResponse} wrapper for the HTTP
* response
* @see CouchUtil#executeRequest
* @see Couch
* API Reference on Document Retrieval
* @throws CouchException If an exception is encountered
*/
private static CouchResponse retrieveDocument(String documentId, boolean withAttachments) throws CouchException {
HttpGet documentGet = new HttpGet(COUCH_ENDPOINT + documentId + "?attachments=" + withAttachments);
// add accepts application/json to get the attachment data in the JSON structure instead of as multipart
documentGet.setHeader(HttpHeaders.ACCEPT, "application/json");
CouchResponse response = executeRequest(documentGet);
classLogger.debug("Successful document retrieval: " + response.toString());
return response;
}
/**
* Call CouchDB with an HTTP DELETE request to /{db}/{docId} to delete a
* document with the given identifier and revision.
*
* @param documentId The document identifier as used by CouchDB
* @param revisionId The current revision code used by CouchDB to validate the
* deletion request
* @return A {@link CouchResponse} wrapper for the HTTP
* response
* @see CouchUtil#executeRequest
* @see Couch
* API Reference on Document Deletes
* @throws CouchException If an exception is encountered
*/
private static CouchResponse deleteDocument(String documentId, String revisionId) throws CouchException {
HttpDelete documentDelete = new HttpDelete(COUCH_ENDPOINT + documentId + "?rev=" + revisionId);
CouchResponse response = executeRequest(documentDelete);
classLogger.debug("Successful document deletion: " + response.toString());
return response;
}
/**
* A generalized helper method to send a variety of HTTP requests to CouchDB.
* All requests get a header added for request authorization.
*
* @param request The {@link HttpUriRequest} request to
* execute with the default
* {@link CloseableHttpClient}
* @return A {@link CouchResponse} wrapper for the HTTP
* response
* @throws CouchException If an exception is encountered
*/
private static CouchResponse executeRequest(HttpUriRequest request) throws CouchException {
try (CloseableHttpClient client = HttpClientBuilder.create().build()) {
request.setHeader(HttpHeaders.AUTHORIZATION, COUCH_AUTH);
HttpResponse response = client.execute(request);
String responseBody = null;
HttpEntity entity = response.getEntity();
if (entity != null) {
responseBody = EntityUtils.toString(entity);
}
StatusLine statusLine = response.getStatusLine();
if(statusLine == null) {
throw new CouchException(String.format("Unsuccessful CouchDB request. Request URI: %s. Status: null.", request.getURI().toString()));
} else if(statusLine.getStatusCode() / 100 != 2) {
throw new CouchException(Integer.valueOf(statusLine.getStatusCode()), String.format("Unsuccessful CouchDB request. Request URI: %s. Status: %s.", request.getURI().toString(), statusLine.toString()));
}
String revision = null;
Header eTagHeader = response.getFirstHeader("ETag");
if(eTagHeader != null) {
revision = eTagHeader.getValue().replace("\"", "");
}
return new CouchResponse(statusLine.getStatusCode(), responseBody, revision);
} catch (IOException e) {
classLogger.error(Constants.STACKTRACE, e);
throw new CouchException(String.format("Error during CouchDB request. Request URI: %s", request.getURI().toString()));
}
}
/* Run this to init a new database in couch:
public static void main(String[] args) throws Exception {
// (re-)initialize a DB on localhost
String couchEndpoint = "http://localhost:5984/";
String dbName = "userfiles";
String creds = "Basic "
+ new String(Base64.encodeBase64("admin:admin".getBytes(StandardCharsets.ISO_8859_1)));
// strong arm the credentials into the class
Field field = CouchUtil.class.getDeclaredField("COUCH_AUTH");
Field modifiers = Field.class.getDeclaredField("modifiers");
boolean modifiersAccessible = modifiers.isAccessible();
modifiers.setAccessible(true);
modifiers.setInt(field, field.getModifiers() & ~Modifier.FINAL);
field.setAccessible(true);
field.set(null, creds);
field.setAccessible(false);
modifiers.setAccessible(modifiersAccessible);
// delete if exists
HttpDelete dbDelete = new HttpDelete(couchEndpoint + dbName);
try {
executeRequest(dbDelete);
System.out.println("Successfully deleted database " + dbName);
} catch (CouchException e) {
if(e.getStatusCode() != HttpStatus.SC_NOT_FOUND) {
System.out.println("Failed to delete database: " + e);
throw e;
}
System.out.println("Database doesn't exist to delete");
}
// create a partitioned DB
HttpPut dbCreate = new HttpPut(couchEndpoint + dbName + "?partitioned=true");
try {
executeRequest(dbCreate);
System.out.println("Successfully created database " + dbName);
} catch (CouchException e) {
System.out.println("Failed to create database: " + e);
throw e;
}
// add indexes for database, project, and insight & project field selectors
String indexEndpoint = couchEndpoint + dbName + "/_index";
String indexBodyTemplate = "{\"index\": {\"fields\": [%s]}, \"name\": \"%s-index\", \"type\": \"json\", \"partitioned\": true}";
try {
HttpPost indexCreate = new HttpPost(indexEndpoint);
indexCreate.addHeader("Content-Type", "application/json");
indexCreate.setEntity(new StringEntity(String.format(indexBodyTemplate, "\"database\"", "database")));
executeRequest(indexCreate);
System.out.println("Successfully created database index");
indexCreate.setEntity(new StringEntity(String.format(indexBodyTemplate, "\"project\"", "project")));
executeRequest(indexCreate);
System.out.println("Successfully created project index");
indexCreate.setEntity(new StringEntity(String.format(indexBodyTemplate, "\"project\", \"insight\"", "insight")));
executeRequest(indexCreate);
System.out.println("Successfully created insight index");
} catch (CouchException e) {
System.out.println("Failed to create all indexes: "+ e);
throw e;
}
}*/
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy