Java tutorial
/* * Copyright (c) Codice Foundation * * <p>This is free software: you can redistribute it and/or modify it under the terms of the GNU * Lesser General Public License as published by the Free Software Foundation, either version 3 of * the License, or any later version. * * <p>This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. A copy of the GNU Lesser General Public * License is distributed along with this program and can be found at * <http://www.gnu.org/licenses/lgpl.html>. */ package org.codice.ddf.catalog.ui.metacard; import static ddf.catalog.util.impl.ResultIterable.resultIterable; import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE; import static javax.ws.rs.core.MediaType.APPLICATION_JSON; import static javax.ws.rs.core.MediaType.TEXT_PLAIN; import static org.apache.commons.lang.StringUtils.isEmpty; import static org.codice.ddf.catalog.ui.metacard.query.util.QueryAttributes.QUERY_TAG; import static org.codice.gsonsupport.GsonTypeAdapters.LIST_STRING; import static org.codice.gsonsupport.GsonTypeAdapters.MAP_STRING_TO_OBJECT_TYPE; import static spark.Spark.after; import static spark.Spark.delete; import static spark.Spark.exception; import static spark.Spark.get; import static spark.Spark.patch; import static spark.Spark.post; import static spark.Spark.put; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.common.io.ByteSource; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; import ddf.catalog.CatalogFramework; import ddf.catalog.content.data.ContentItem; import ddf.catalog.content.data.impl.ContentItemImpl; import ddf.catalog.content.operation.impl.CreateStorageRequestImpl; import ddf.catalog.content.operation.impl.UpdateStorageRequestImpl; import ddf.catalog.core.versioning.DeletedMetacard; import ddf.catalog.core.versioning.MetacardVersion; import ddf.catalog.core.versioning.MetacardVersion.Action; import ddf.catalog.core.versioning.impl.MetacardVersionImpl; import ddf.catalog.data.Attribute; import ddf.catalog.data.AttributeDescriptor; import ddf.catalog.data.AttributeRegistry; import ddf.catalog.data.AttributeType; import ddf.catalog.data.BinaryContent; import ddf.catalog.data.Metacard; import ddf.catalog.data.MetacardType; import ddf.catalog.data.Result; import ddf.catalog.data.impl.AttributeImpl; import ddf.catalog.data.impl.MetacardImpl; import ddf.catalog.data.impl.ResultImpl; import ddf.catalog.data.impl.types.SecurityAttributes; import ddf.catalog.data.types.Core; import ddf.catalog.federation.FederationException; import ddf.catalog.filter.FilterBuilder; import ddf.catalog.operation.DeleteResponse; import ddf.catalog.operation.QueryResponse; import ddf.catalog.operation.ResourceResponse; import ddf.catalog.operation.UpdateResponse; import ddf.catalog.operation.impl.CreateRequestImpl; import ddf.catalog.operation.impl.DeleteRequestImpl; import ddf.catalog.operation.impl.QueryImpl; import ddf.catalog.operation.impl.QueryRequestImpl; import ddf.catalog.operation.impl.ResourceRequestById; import ddf.catalog.operation.impl.SourceResponseImpl; import ddf.catalog.operation.impl.UpdateRequestImpl; import ddf.catalog.resource.ResourceNotFoundException; import ddf.catalog.resource.ResourceNotSupportedException; import ddf.catalog.source.IngestException; import ddf.catalog.source.SourceUnavailableException; import ddf.catalog.source.UnsupportedQueryException; import ddf.catalog.transform.QueryResponseTransformer; import ddf.catalog.util.impl.ResultIterable; import ddf.security.Subject; import ddf.security.SubjectIdentity; import ddf.security.SubjectUtils; import ddf.security.common.audit.SecurityLogger; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.Serializable; import java.lang.reflect.Type; import java.time.Instant; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.function.Supplier; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.zip.GZIPOutputStream; import javax.ws.rs.NotFoundException; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.shiro.SecurityUtils; import org.apache.shiro.subject.ExecutionException; import org.codice.ddf.catalog.ui.config.ConfigurationApplication; import org.codice.ddf.catalog.ui.enumeration.ExperimentalEnumerationExtractor; import org.codice.ddf.catalog.ui.metacard.associations.Associated; import org.codice.ddf.catalog.ui.metacard.edit.AttributeChange; import org.codice.ddf.catalog.ui.metacard.edit.MetacardChanges; import org.codice.ddf.catalog.ui.metacard.history.HistoryResponse; import org.codice.ddf.catalog.ui.metacard.notes.NoteConstants; import org.codice.ddf.catalog.ui.metacard.notes.NoteMetacard; import org.codice.ddf.catalog.ui.metacard.notes.NoteUtil; import org.codice.ddf.catalog.ui.metacard.query.data.metacard.QueryMetacardImpl; import org.codice.ddf.catalog.ui.metacard.transform.CsvTransform; import org.codice.ddf.catalog.ui.metacard.validation.Validator; import org.codice.ddf.catalog.ui.metacard.workspace.WorkspaceConstants; import org.codice.ddf.catalog.ui.metacard.workspace.WorkspaceMetacardImpl; import org.codice.ddf.catalog.ui.metacard.workspace.transformer.WorkspaceTransformer; import org.codice.ddf.catalog.ui.metacard.workspace.transformer.impl.AssociatedQueryMetacardsHandler; import org.codice.ddf.catalog.ui.query.monitor.api.WorkspaceService; import org.codice.ddf.catalog.ui.security.Constants; import org.codice.ddf.catalog.ui.security.accesscontrol.AccessControlSecurityConfiguration; import org.codice.ddf.catalog.ui.subscription.SubscriptionsPersistentStore; import org.codice.ddf.catalog.ui.util.EndpointUtil; import org.codice.ddf.security.common.Security; import org.codice.gsonsupport.GsonTypeAdapters.DateLongFormatTypeAdapter; import org.codice.gsonsupport.GsonTypeAdapters.LongDoubleTypeAdapter; import org.opengis.filter.Filter; import org.opengis.filter.sort.SortBy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import spark.servlet.SparkApplication; public class MetacardApplication implements SparkApplication { private static final Logger LOGGER = LoggerFactory.getLogger(MetacardApplication.class); private static final String UPDATE_ERROR_MESSAGE = "Item is either restricted or not found."; private static final Set<Action> CONTENT_ACTIONS = ImmutableSet.of(Action.VERSIONED_CONTENT, Action.DELETED_CONTENT); private static final Set<Action> DELETE_ACTIONS = ImmutableSet.of(Action.DELETED, Action.DELETED_CONTENT); private static final Security SECURITY = Security.getInstance(); private static final String ERROR_RESPONSE_TYPE = "error"; private static final String SUCCESS_RESPONSE_TYPE = "success"; private static final MetacardType SECURITY_ATTRIBUTES = new SecurityAttributes(); private static final Type METACARD_CHANGES_LIST_TYPE = new TypeToken<List<MetacardChanges>>() { }.getType(); private static final Type ATTRIBUTE_CHANGE_TYPE = new TypeToken<AttributeChange>() { }.getType(); private static final Type ASSOCIATED_EDGE_LIST_TYPE = new TypeToken<List<Associated.Edge>>() { }.getType(); private static int pageSize = 250; private static final Gson GSON = new GsonBuilder().disableHtmlEscaping().serializeNulls() .registerTypeAdapterFactory(LongDoubleTypeAdapter.FACTORY) .registerTypeAdapter(Date.class, new DateLongFormatTypeAdapter()).create(); private final CatalogFramework catalogFramework; private final FilterBuilder filterBuilder; private final EndpointUtil util; private final Validator validator; private final WorkspaceTransformer transformer; private final ExperimentalEnumerationExtractor enumExtractor; private final SubscriptionsPersistentStore subscriptions; private final List<MetacardType> types; private final Associated associated; private final QueryResponseTransformer csvQueryResponseTransformer; private final SubjectIdentity subjectIdentity; private final AttributeRegistry attributeRegistry; private final ConfigurationApplication configuration; private final NoteUtil noteUtil; private final AccessControlSecurityConfiguration accessControlSecurityConfiguration; private final WorkspaceService workspaceService; private final AssociatedQueryMetacardsHandler queryMetacardsHandler; public MetacardApplication(CatalogFramework catalogFramework, FilterBuilder filterBuilder, EndpointUtil endpointUtil, Validator validator, WorkspaceTransformer transformer, ExperimentalEnumerationExtractor enumExtractor, SubscriptionsPersistentStore subscriptions, List<MetacardType> types, Associated associated, QueryResponseTransformer csvQueryResponseTransformer, AttributeRegistry attributeRegistry, ConfigurationApplication configuration, NoteUtil noteUtil, SubjectIdentity subjectIdentity, AccessControlSecurityConfiguration accessControlSecurityConfiguration, WorkspaceService workspaceService, AssociatedQueryMetacardsHandler queryMetacardsHandler) { this.catalogFramework = catalogFramework; this.filterBuilder = filterBuilder; this.util = endpointUtil; this.validator = validator; this.transformer = transformer; this.enumExtractor = enumExtractor; this.subscriptions = subscriptions; this.types = types; this.associated = associated; this.csvQueryResponseTransformer = csvQueryResponseTransformer; this.attributeRegistry = attributeRegistry; this.configuration = configuration; this.noteUtil = noteUtil; this.subjectIdentity = subjectIdentity; this.accessControlSecurityConfiguration = accessControlSecurityConfiguration; this.workspaceService = workspaceService; this.queryMetacardsHandler = queryMetacardsHandler; } private String getSubjectEmail() { return SubjectUtils.getEmailAddress(SecurityUtils.getSubject()); } private List<String> getSubjectRoles() { return SubjectUtils.getAttribute(SecurityUtils.getSubject(), Constants.ROLES_CLAIM_URI); } private String getSubjectIdentifier() { return subjectIdentity.getUniqueIdentifier(SecurityUtils.getSubject()); } @Override public void init() { get("/metacardtype", (req, res) -> util.getJson(util.getMetacardTypeMap())); get("/metacard/:id", (req, res) -> { String id = req.params(":id"); return util.metacardToJson(id); }); get("/metacard/:id/attribute/validation", (req, res) -> { String id = req.params(":id"); return util.getJson(validator.getValidation(util.getMetacardById(id))); }); get("/metacard/:id/validation", (req, res) -> { String id = req.params(":id"); return util.getJson(validator.getFullValidation(util.getMetacardById(id))); }); post("/prevalidate", APPLICATION_JSON, (req, res) -> { Map<String, Object> stringObjectMap = GSON.fromJson(util.safeGetBody(req), MAP_STRING_TO_OBJECT_TYPE); MetacardImpl metacard = new MetacardImpl(); stringObjectMap.keySet().stream() .map(s -> new AttributeImpl(s, (List<Serializable>) stringObjectMap.get(s))) .forEach(metacard::setAttribute); return util.getJson(validator.getValidation(metacard)); }); post("/metacards", APPLICATION_JSON, (req, res) -> { List<String> ids = GSON.fromJson(util.safeGetBody(req), LIST_STRING); List<Metacard> metacards = util.getMetacardsWithTagById(ids, "*").entrySet().stream() .map(Map.Entry::getValue).map(Result::getMetacard).collect(Collectors.toList()); return util.metacardsToJson(metacards); }); delete("/metacards", APPLICATION_JSON, (req, res) -> { List<String> ids = GSON.fromJson(util.safeGetBody(req), LIST_STRING); DeleteResponse deleteResponse = catalogFramework .delete(new DeleteRequestImpl(new ArrayList<>(ids), Metacard.ID, null)); if (deleteResponse.getProcessingErrors() != null && !deleteResponse.getProcessingErrors().isEmpty()) { res.status(500); return ImmutableMap.of("message", "Unable to archive metacards."); } return ImmutableMap.of("message", "Successfully archived metacards."); }, util::getJson); patch("/metacards", APPLICATION_JSON, (req, res) -> { String body = util.safeGetBody(req); List<MetacardChanges> metacardChanges = GSON.fromJson(body, METACARD_CHANGES_LIST_TYPE); UpdateResponse updateResponse = patchMetacards(metacardChanges, getSubjectIdentifier()); if (updateResponse.getProcessingErrors() != null && !updateResponse.getProcessingErrors().isEmpty()) { res.status(500); return updateResponse.getProcessingErrors(); } return body; }); put("/validate/attribute/:attribute", TEXT_PLAIN, (req, res) -> { String attribute = req.params(":attribute"); String value = util.safeGetBody(req); return util.getJson(validator.validateAttribute(attribute, value)); }); get("/history/:id", (req, res) -> { String id = req.params(":id"); List<Result> queryResponse = getMetacardHistory(id); if (queryResponse.isEmpty()) { res.status(204); return "[]"; } List<HistoryResponse> response = queryResponse.stream().map(Result::getMetacard) .map(mc -> new HistoryResponse(mc.getId(), (String) mc.getAttribute(MetacardVersion.EDITED_BY).getValue(), (Date) mc.getAttribute(MetacardVersion.VERSIONED_ON).getValue())) .sorted(Comparator.comparing(HistoryResponse::getVersioned)).collect(Collectors.toList()); return util.getJson(response); }); get("/history/revert/:id/:revertid", (req, res) -> { String id = req.params(":id"); String revertId = req.params(":revertid"); Metacard versionMetacard = util.getMetacardById(revertId); List<Result> queryResponse = getMetacardHistory(id); if (queryResponse == null || queryResponse.isEmpty()) { throw new NotFoundException("Could not find metacard with id: " + id); } Optional<Metacard> contentVersion = queryResponse.stream().map(Result::getMetacard) .filter(mc -> getVersionedOnDate(mc).isAfter(getVersionedOnDate(versionMetacard)) || getVersionedOnDate(mc).equals(getVersionedOnDate(versionMetacard))) .filter(mc -> CONTENT_ACTIONS.contains(Action.ofMetacard(mc))) .filter(mc -> mc.getResourceURI() != null) .filter(mc -> ContentItem.CONTENT_SCHEME.equals(mc.getResourceURI().getScheme())) .sorted(Comparator.comparing((Metacard mc) -> util .parseToDate(mc.getAttribute(MetacardVersion.VERSIONED_ON).getValue()))) .findFirst(); if (!contentVersion.isPresent()) { /* no content versions, just restore metacard */ revertMetacard(versionMetacard, id, false); } else { revertContentandMetacard(contentVersion.get(), versionMetacard, id); } return util.metacardToJson(MetacardVersionImpl.toMetacard(versionMetacard, types)); }); get("/associations/:id", (req, res) -> { String id = req.params(":id"); return util.getJson(associated.getAssociations(id)); }); put("/associations/:id", (req, res) -> { String id = req.params(":id"); String body = util.safeGetBody(req); List<Associated.Edge> edges = GSON.fromJson(body, ASSOCIATED_EDGE_LIST_TYPE); associated.putAssociations(id, edges); return body; }); post("/subscribe/:id", (req, res) -> { String userid = getSubjectIdentifier(); String email = getSubjectEmail(); if (isEmpty(email)) { throw new NotFoundException( "Unable to subscribe to workspace, " + userid + " has no email address."); } String id = req.params(":id"); subscriptions.addEmail(id, email); return ImmutableMap.of("message", String.format("Successfully subscribed to id = %s.", id)); }, util::getJson); post("/unsubscribe/:id", (req, res) -> { String userid = getSubjectIdentifier(); String email = getSubjectEmail(); if (isEmpty(email)) { throw new NotFoundException( "Unable to un-subscribe from workspace, " + userid + " has no email address."); } String id = req.params(":id"); if (StringUtils.isEmpty(req.body())) { subscriptions.removeEmail(id, email); return ImmutableMap.of("message", String.format("Successfully un-subscribed to id = %s.", id)); } else { String body = req.body(); AttributeChange attributeChange = GSON.fromJson(body, ATTRIBUTE_CHANGE_TYPE); subscriptions.removeEmails(id, new HashSet<>(attributeChange.getValues())); return ImmutableMap.of("message", String.format("Successfully un-subscribed emails %s id = %s.", attributeChange.getValues().toString(), id)); } }, util::getJson); get("/workspaces/:id", (req, res) -> { String id = req.params(":id"); String email = getSubjectEmail(); Metacard metacard = util.getMetacardById(id); // NOTE: the isEmpty is to guard against users with no email (such as guest). boolean isSubscribed = !isEmpty(email) && subscriptions.getEmails(metacard.getId()).contains(email); return ImmutableMap.builder().putAll(transformer.transform(metacard)).put("subscribed", isSubscribed) .build(); }, util::getJson); get("/workspaces", (req, res) -> { String email = getSubjectEmail(); // NOTE: the isEmpty is to guard against users with no email (such as guest). Set<String> ids = isEmpty(email) ? Collections.emptySet() : subscriptions.getSubscriptions(email); return util.getMetacardsByTag(WorkspaceConstants.WORKSPACE_TAG).entrySet().stream() .map(Map.Entry::getValue).map(Result::getMetacard).map(metacard -> { boolean isSubscribed = ids.contains(metacard.getId()); try { return ImmutableMap.builder().putAll(transformer.transform(metacard)) .put("subscribed", isSubscribed).build(); } catch (RuntimeException e) { LOGGER.debug( "Could not transform metacard. WARNING: This indicates there is invalid data in the system. Metacard title: '{}', id:'{}'", metacard.getTitle(), metacard.getId(), e); } return null; }).filter(Objects::nonNull).collect(Collectors.toList()); }, util::getJson); post("/workspaces", APPLICATION_JSON, (req, res) -> { Map<String, Object> incoming = GSON.fromJson(util.safeGetBody(req), MAP_STRING_TO_OBJECT_TYPE); List<Metacard> queries = ((List<Map<String, Object>>) incoming .getOrDefault(WorkspaceConstants.WORKSPACE_QUERIES, Collections.emptyList())).stream() .map(transformer::transform).collect(Collectors.toList()); queryMetacardsHandler.create(Collections.emptyList(), queries); Metacard saved = saveMetacard(transformer.transform(incoming)); Map<String, Object> response = transformer.transform(saved); res.status(201); return util.getJson(response); }); put("/workspaces/:id", APPLICATION_JSON, (req, res) -> { String id = req.params(":id"); WorkspaceMetacardImpl existingWorkspace = workspaceService.getWorkspaceMetacard(id); List<String> existingQueryIds = existingWorkspace.getQueries(); Map<String, Object> updatedWorkspace = GSON.fromJson(util.safeGetBody(req), MAP_STRING_TO_OBJECT_TYPE); List<Metacard> updatedQueryMetacards = ((List<Map<String, Object>>) updatedWorkspace .getOrDefault("queries", Collections.emptyList())).stream().map(transformer::transform) .collect(Collectors.toList()); List<String> updatedQueryIds = updatedQueryMetacards.stream().map(Metacard::getId) .collect(Collectors.toList()); List<QueryMetacardImpl> existingQueryMetacards = workspaceService.getQueryMetacards(existingWorkspace); queryMetacardsHandler.create(existingQueryIds, updatedQueryMetacards); queryMetacardsHandler.delete(existingQueryIds, updatedQueryIds); queryMetacardsHandler.update(existingQueryIds, existingQueryMetacards, updatedQueryMetacards); List<Map<String, String>> queryIdModel = updatedQueryIds.stream() .map(queryId -> ImmutableMap.of("id", queryId)).collect(Collectors.toList()); updatedWorkspace.put("queries", queryIdModel); Metacard metacard = transformer.transform(updatedWorkspace); metacard.setAttribute(new AttributeImpl(Core.ID, id)); Metacard updated = updateMetacard(id, metacard); return transformer.transform(updated); }, util::getJson); delete("/workspaces/:id", APPLICATION_JSON, (req, res) -> { String id = req.params(":id"); WorkspaceMetacardImpl workspace = workspaceService.getWorkspaceMetacard(id); String[] queryIds = workspace.getQueries().toArray(new String[0]); if (queryIds.length > 0) { catalogFramework.delete(new DeleteRequestImpl(queryIds)); } catalogFramework.delete(new DeleteRequestImpl(id)); subscriptions.removeSubscriptions(id); return ImmutableMap.of("message", "Successfully deleted."); }, util::getJson); get("/workspaces/:id/queries", (req, res) -> { String workspaceId = req.params(":id"); WorkspaceMetacardImpl workspace = workspaceService.getWorkspaceMetacard(workspaceId); List<String> queryIds = workspace.getQueries(); return util.getMetacardsWithTagById(queryIds, QUERY_TAG).values().stream().map(Result::getMetacard) .map(transformer::transform).collect(Collectors.toList()); }, util::getJson); get("/enumerations/metacardtype/:type", APPLICATION_JSON, (req, res) -> { return util.getJson(enumExtractor.getEnumerations(req.params(":type"))); }); get("/enumerations/attribute/:attribute", APPLICATION_JSON, (req, res) -> { return util.getJson(enumExtractor.getAttributeEnumerations(req.params(":attribute"))); }); get("/localcatalogid", (req, res) -> { return String.format("{\"%s\":\"%s\"}", "local-catalog-id", catalogFramework.getId()); }); post("/transform/csv", APPLICATION_JSON, (req, res) -> { String body = util.safeGetBody(req); CsvTransform queryTransform = GSON.fromJson(body, CsvTransform.class); Map<String, Object> transformMap = GSON.fromJson(body, MAP_STRING_TO_OBJECT_TYPE); queryTransform.setMetacards((List<Map<String, Object>>) transformMap.get("metacards")); List<Result> metacards = queryTransform.getTransformedMetacards(types, attributeRegistry).stream() .map(ResultImpl::new).collect(Collectors.toList()); Set<String> matchedHiddenFields = Collections.emptySet(); if (queryTransform.isApplyGlobalHidden()) { matchedHiddenFields = getHiddenFields(metacards); } SourceResponseImpl response = new SourceResponseImpl(null, metacards, Long.valueOf(metacards.size())); Map<String, Serializable> arguments = ImmutableMap.<String, Serializable>builder() .put("hiddenFields", new HashSet<>(Sets.union(matchedHiddenFields, queryTransform.getHiddenFields()))) .put("columnOrder", new ArrayList<>(queryTransform.getColumnOrder())) .put("aliases", new HashMap<>(queryTransform.getColumnAliasMap())).build(); BinaryContent content = csvQueryResponseTransformer.transform(response, arguments); String acceptEncoding = req.headers("Accept-Encoding"); // Very naive way to handle accept encoding, does not respect full spec boolean shouldGzip = StringUtils.isNotBlank(acceptEncoding) && acceptEncoding.toLowerCase().contains("gzip"); // Respond with content res.type("text/csv"); String attachment = String.format("attachment;filename=export-%s.csv", Instant.now().toString()); res.header("Content-Disposition", attachment); if (shouldGzip) { res.raw().addHeader("Content-Encoding", "gzip"); } try ( // OutputStream servletOutputStream = res.raw().getOutputStream(); InputStream resultStream = content.getInputStream()) { if (shouldGzip) { try (OutputStream gzipServletOutputStream = new GZIPOutputStream(servletOutputStream)) { IOUtils.copy(resultStream, gzipServletOutputStream); } } else { IOUtils.copy(resultStream, servletOutputStream); } } return ""; }); post("/annotations", (req, res) -> { Map<String, Object> incoming = GSON.fromJson(util.safeGetBody(req), MAP_STRING_TO_OBJECT_TYPE); String workspaceId = incoming.get("workspace").toString(); String queryId = incoming.get("parent").toString(); String annotation = incoming.get("note").toString(); String user = getSubjectIdentifier(); if (user == null) { res.status(401); return util.getResponseWrapper(ERROR_RESPONSE_TYPE, "You are not authorized to create notes! A user email is required. " + "Please ensure you are logged in and/or have a valid email registered in the system."); } if (StringUtils.isBlank(annotation)) { res.status(400); return util.getResponseWrapper(ERROR_RESPONSE_TYPE, "No annotation!"); } NoteMetacard noteMetacard = new NoteMetacard(queryId, user, annotation); Metacard workspaceMetacard = util.findWorkspace(workspaceId); if (workspaceMetacard == null) { res.status(404); return util.getResponseWrapper(ERROR_RESPONSE_TYPE, "Cannot find the workspace metacard!"); } util.copyAttributes(workspaceMetacard, SECURITY_ATTRIBUTES, noteMetacard); Metacard note = saveMetacard(noteMetacard); SecurityLogger.auditWarn("Attaching an annotation to a resource: resource={} annotation={}", SecurityUtils.getSubject(), workspaceId, noteMetacard.getId()); Map<String, String> responseNote = noteUtil.getResponseNote(note); if (responseNote == null) { res.status(500); return util.getResponseWrapper(ERROR_RESPONSE_TYPE, "Cannot serialize note metacard to json!"); } return util.getResponseWrapper(SUCCESS_RESPONSE_TYPE, util.getJson(responseNote)); }); get("/annotations/:queryid", (req, res) -> { String queryId = req.params(":queryid"); List<Metacard> retrievedMetacards = noteUtil.getAssociatedMetacardsByTwoAttributes( NoteConstants.PARENT_ID, Core.METACARD_TAGS, queryId, "note"); ArrayList<String> getResponse = new ArrayList<>(); retrievedMetacards.sort(Comparator.comparing(Metacard::getCreatedDate)); for (Metacard metacard : retrievedMetacards) { Map<String, String> responseNote = noteUtil.getResponseNote(metacard); if (responseNote != null) { getResponse.add(util.getJson(responseNote)); } } return util.getResponseWrapper(SUCCESS_RESPONSE_TYPE, getResponse.toString()); }); put("/annotations/:id", APPLICATION_JSON, (req, res) -> { Map<String, Object> incoming = GSON.fromJson(util.safeGetBody(req), MAP_STRING_TO_OBJECT_TYPE); String noteMetacardId = req.params(":id"); String note = incoming.get("note").toString(); Metacard metacard; try { metacard = util.getMetacardById(noteMetacardId); } catch (NotFoundException e) { LOGGER.debug("Note metacard was not found for updating. id={}", noteMetacardId); res.status(404); return util.getResponseWrapper(ERROR_RESPONSE_TYPE, "Note metacard was not found!"); } Attribute attribute = metacard.getAttribute(Core.METACARD_OWNER); if (attribute != null && attribute.getValue() != null && !attribute.getValue().equals(getSubjectEmail())) { res.status(401); return util.getResponseWrapper(ERROR_RESPONSE_TYPE, "Owner of note metacard is invalid!"); } metacard.setAttribute(new AttributeImpl(NoteConstants.COMMENT, note)); metacard = updateMetacard(metacard.getId(), metacard); Map<String, String> responseNote = noteUtil.getResponseNote(metacard); return util.getResponseWrapper(SUCCESS_RESPONSE_TYPE, util.getJson(responseNote)); }); delete("/annotations/:id", (req, res) -> { String noteToDeleteMetacardId = req.params(":id"); Metacard metacard; try { metacard = util.getMetacardById(noteToDeleteMetacardId); } catch (NotFoundException e) { LOGGER.debug("Note metacard was not found for deleting. id={}", noteToDeleteMetacardId); res.status(404); return util.getResponseWrapper(ERROR_RESPONSE_TYPE, "Note metacard was not found!"); } Attribute attribute = metacard.getAttribute(Core.METACARD_OWNER); if (attribute != null && attribute.getValue() != null && !attribute.getValue().equals(getSubjectEmail())) { res.status(401); return util.getResponseWrapper(ERROR_RESPONSE_TYPE, "Owner of note metacard is invalid!"); } DeleteResponse deleteResponse = catalogFramework.delete(new DeleteRequestImpl(noteToDeleteMetacardId)); if (deleteResponse.getDeletedMetacards() != null && !deleteResponse.getDeletedMetacards().isEmpty()) { Map<String, String> responseNote = noteUtil .getResponseNote(deleteResponse.getDeletedMetacards().get(0)); return util.getResponseWrapper(SUCCESS_RESPONSE_TYPE, util.getJson(responseNote)); } res.status(500); return util.getResponseWrapper(ERROR_RESPONSE_TYPE, "Could not delete note metacard!"); }); after((req, res) -> { res.type(APPLICATION_JSON); }); exception(IngestException.class, (ex, req, res) -> { LOGGER.debug("Failed to ingest metacard", ex); res.status(404); res.header(CONTENT_TYPE, APPLICATION_JSON); res.body(util.getJson(ImmutableMap.of("message", UPDATE_ERROR_MESSAGE))); }); exception(NotFoundException.class, (ex, req, res) -> { LOGGER.debug("Failed to find metacard.", ex); res.status(404); res.header(CONTENT_TYPE, APPLICATION_JSON); res.body(util.getJson(ImmutableMap.of("message", ex.getMessage()))); }); exception(NumberFormatException.class, (ex, req, res) -> { res.status(400); res.header(CONTENT_TYPE, APPLICATION_JSON); res.body(util.getJson(ImmutableMap.of("message", "Invalid values for numbers"))); }); exception(EntityTooLargeException.class, util::handleEntityTooLargeException); exception(IOException.class, util::handleIOException); exception(RuntimeException.class, util::handleRuntimeException); } private Set<String> getHiddenFields(List<Result> metacards) { Set<String> matchedHiddenFields; List<Pattern> hiddenFieldPatterns = configuration.getHiddenAttributes().stream().map(Pattern::compile) .collect(Collectors.toList()); matchedHiddenFields = metacards.stream().map(Result::getMetacard).map(Metacard::getMetacardType) .map(MetacardType::getAttributeDescriptors).flatMap(Collection::stream) .map(AttributeDescriptor::getName).filter(attr -> hiddenFieldPatterns.stream() .map(Pattern::asPredicate).anyMatch(pattern -> pattern.test(attr))) .collect(Collectors.toSet()); return matchedHiddenFields; } private void revertMetacard(Metacard versionMetacard, String id, boolean alreadyCreated) throws SourceUnavailableException, IngestException, FederationException, UnsupportedQueryException { LOGGER.trace("Reverting metacard [{}] to version [{}]", id, versionMetacard.getId()); Metacard revertMetacard = MetacardVersionImpl.toMetacard(versionMetacard, types); Action action = Action.fromKey((String) versionMetacard.getAttribute(MetacardVersion.ACTION).getValue()); if (DELETE_ACTIONS.contains(action)) { attemptDeleteDeletedMetacard(id); if (!alreadyCreated) { catalogFramework.create(new CreateRequestImpl(revertMetacard)); } } else { tryUpdate(4, () -> { catalogFramework.update(new UpdateRequestImpl(id, revertMetacard)); return true; }); } } private void revertContentandMetacard(Metacard latestContent, Metacard versionMetacard, String id) throws SourceUnavailableException, IngestException, ResourceNotFoundException, IOException, ResourceNotSupportedException, FederationException, UnsupportedQueryException { LOGGER.trace( "Reverting content and metacard for metacard [{}]. \nLatest content: [{}] \nVersion metacard: [{}]", id, latestContent.getId(), versionMetacard.getId()); Map<String, Serializable> properties = new HashMap<>(); properties.put("no-default-tags", true); ResourceResponse latestResource = catalogFramework .getLocalResource(new ResourceRequestById(latestContent.getId(), properties)); ContentItemImpl contentItem = new ContentItemImpl(id, new ByteSourceWrapper(() -> latestResource.getResource().getInputStream()), latestResource.getResource().getMimeTypeValue(), latestResource.getResource().getName(), latestResource.getResource().getSize(), MetacardVersionImpl.toMetacard(versionMetacard, types)); // Try to delete the "deleted metacard" marker first. boolean alreadyCreated = false; Action action = Action.fromKey((String) versionMetacard.getAttribute(MetacardVersion.ACTION).getValue()); if (DELETE_ACTIONS.contains(action)) { alreadyCreated = true; catalogFramework.create( new CreateStorageRequestImpl(Collections.singletonList(contentItem), id, new HashMap<>())); } else { // Currently we can't guarantee the metacard will exist yet because of the 1 second // soft commit in solr. this busy wait loop should be fixed when alternate solution // is found. tryUpdate(4, () -> { catalogFramework.update( new UpdateStorageRequestImpl(Collections.singletonList(contentItem), id, new HashMap<>())); return true; }); } LOGGER.trace("Successfully reverted metacard content for [{}]", id); revertMetacard(versionMetacard, id, alreadyCreated); } private void trySleep(long millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } private void tryUpdate(int retries, Callable<Boolean> func) throws IngestException, SourceUnavailableException { if (retries <= 0) { throw new IngestException("Could not update metacard!"); } LOGGER.trace("Trying to update metacard."); try { func.call(); LOGGER.trace("Successfully updated metacard."); } catch (Exception e) { LOGGER.trace("Failed to update metacard"); trySleep(350); tryUpdate(retries - 1, func); } } private void attemptDeleteDeletedMetacard(String id) throws UnsupportedQueryException, SourceUnavailableException, FederationException { LOGGER.trace("Attemping to delete metacard [{}]", id); Filter tags = filterBuilder.attribute(Metacard.TAGS).is().like().text(DeletedMetacard.DELETED_TAG); Filter deletion = filterBuilder.attribute(DeletedMetacard.DELETION_OF_ID).is().like().text(id); Filter filter = filterBuilder.allOf(tags, deletion); QueryResponse response = null; try { response = catalogFramework.query(new QueryRequestImpl(new QueryImpl(filter), false)); } catch (UnsupportedQueryException | SourceUnavailableException | FederationException e) { LOGGER.debug("Could not find the deleted metacard marker to delete", e); } if (response == null || response.getResults() == null || response.getResults().size() != 1) { LOGGER.debug("There should have been one deleted metacard marker"); return; } final DeleteRequestImpl deleteRequest = new DeleteRequestImpl( response.getResults().get(0).getMetacard().getId()); deleteRequest.getProperties().put("operation.query-tags", ImmutableSet.of("*")); try { executeAsSystem(() -> catalogFramework.delete(deleteRequest)); } catch (ExecutionException e) { LOGGER.debug("Could not delete the deleted metacard marker", e); } LOGGER.trace("Deleted delete marker metacard successfully"); } /** * Caution should be used with this, as it elevates the permissions to the System user. * * @param func What to execute as the System * @param <T> Generic return type of func * @return result of the callable func */ private <T> T executeAsSystem(Callable<T> func) { Subject systemSubject = SECURITY.runAsAdmin(SECURITY::getSystemSubject); if (systemSubject == null) { throw new SecurityException("Could not get systemSubject to version metacards."); } return systemSubject.execute(func); } private Instant getVersionedOnDate(Metacard mc) { return util.parseToDate(mc.getAttribute(MetacardVersion.VERSIONED_ON).getValue()); } private AttributeDescriptor getDescriptor(Metacard target, String attribute) { return Optional.ofNullable(target).map(Metacard::getMetacardType) .map(mt -> mt.getAttributeDescriptor(attribute)) .orElseThrow(() -> new RuntimeException("Could not find attribute descriptor for: " + attribute)); } protected UpdateResponse patchMetacards(List<MetacardChanges> metacardChanges, String subjectIdentifer) throws SourceUnavailableException, IngestException { Set<String> changedIds = metacardChanges.stream().flatMap(mc -> mc.getIds().stream()) .collect(Collectors.toSet()); Map<String, Result> results = util.getMetacardsWithTagById(changedIds, "*"); for (MetacardChanges changeset : metacardChanges) { for (AttributeChange attributeChange : changeset.getAttributes()) { for (String id : changeset.getIds()) { List<String> values = attributeChange.getValues(); Result result = results.get(id); if (result == null) { LOGGER.debug( "Metacard {} either does not exist or user {} does not have permission to see it", id, subjectIdentifer); throw new NotFoundException("Result was not found"); } Metacard resultMetacard = result.getMetacard(); Function<Serializable, Serializable> mapFunc = Function.identity(); if (isChangeTypeDate(attributeChange, resultMetacard)) { mapFunc = mapFunc.andThen(serializable -> Date.from(util.parseDate(serializable))); } resultMetacard.setAttribute(new AttributeImpl(attributeChange.getAttribute(), values.stream().filter(Objects::nonNull).map(mapFunc).collect(Collectors.toList()))); } } } List<Metacard> changedMetacards = results.values().stream().map(Result::getMetacard) .collect(Collectors.toList()); return catalogFramework.update(new UpdateRequestImpl( changedMetacards.stream().map(Metacard::getId).toArray(String[]::new), changedMetacards)); } private boolean isChangeTypeDate(AttributeChange attributeChange, Metacard result) { return getDescriptor(result, attributeChange.getAttribute()).getType().getAttributeFormat() .equals(AttributeType.AttributeFormat.DATE); } private List<Result> getMetacardHistory(String id) { Filter historyFilter = filterBuilder.attribute(Metacard.TAGS).is().equalTo() .text(MetacardVersion.VERSION_TAG); Filter idFilter = filterBuilder.attribute(MetacardVersion.VERSION_OF_ID).is().equalTo().text(id); Filter filter = filterBuilder.allOf(historyFilter, idFilter); ResultIterable resultIterable = resultIterable(catalogFramework, new QueryRequestImpl( new QueryImpl(filter, 1, pageSize, SortBy.NATURAL_ORDER, false, TimeUnit.SECONDS.toMillis(10)), false)); return Lists.newArrayList(resultIterable); } private Metacard updateMetacard(String id, Metacard metacard) throws SourceUnavailableException, IngestException { return catalogFramework.update(new UpdateRequestImpl(id, metacard)).getUpdatedMetacards().get(0) .getNewMetacard(); } private Metacard saveMetacard(Metacard metacard) throws IngestException, SourceUnavailableException { return catalogFramework.create(new CreateRequestImpl(metacard)).getCreatedMetacards().get(0); } private static class ByteSourceWrapper extends ByteSource { Supplier<InputStream> supplier; ByteSourceWrapper(Supplier<InputStream> supplier) { this.supplier = supplier; } @Override public InputStream openStream() throws IOException { return supplier.get(); } } }