EntryResource.java

/*
 * Copyright (c) 2007-2017 MetaSolutions AB
 *
 * 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.entrystore.rest.resources;

import org.eclipse.rdf4j.model.Model;
import org.eclipse.rdf4j.model.Statement;
import org.eclipse.rdf4j.model.impl.LinkedHashModel;
import org.eclipse.rdf4j.rio.RDFFormat;
import org.entrystore.AuthorizationException;
import org.entrystore.Context;
import org.entrystore.EntryType;
import org.entrystore.GraphType;
import org.entrystore.Metadata;
import org.entrystore.Resource;
import org.entrystore.exception.EntryMissingException;
import org.entrystore.impl.RepositoryProperties;
import org.entrystore.rest.auth.UserTempLockoutCache;
import org.entrystore.rest.serializer.ResourceJsonSerializer;
import org.entrystore.rest.serializer.ResourceJsonSerializer.ListParams;
import org.entrystore.rest.util.GraphUtil;
import org.entrystore.rest.util.JSONErrorMessages;
import org.entrystore.rest.util.RDFJSON;
import org.entrystore.rest.util.Util;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.restlet.data.MediaType;
import org.restlet.data.Status;
import org.restlet.ext.json.JsonRepresentation;
import org.restlet.representation.EmptyRepresentation;
import org.restlet.representation.Representation;
import org.restlet.representation.StringRepresentation;
import org.restlet.representation.Variant;
import org.restlet.resource.Delete;
import org.restlet.resource.Get;
import org.restlet.resource.Put;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.Date;
import java.util.List;
import java.util.Optional;

import static org.entrystore.EntryType.Link;
import static org.entrystore.EntryType.LinkReference;
import static org.entrystore.EntryType.Local;
import static org.entrystore.EntryType.Reference;
import static org.entrystore.rest.serializer.ResourceJsonSerializer.IMMUTABLE_EMPTY_JSONOBJECT;
import static org.restlet.data.MediaType.APPLICATION_JSON;
import static org.restlet.data.MediaType.APPLICATION_RDF_XML;
import static org.restlet.data.MediaType.TEXT_RDF_N3;


/**
 * Handles entries.
 *
 * @author Eric Johansson
 * @author Hannes Ebner
 */
public class EntryResource extends BaseResource {

	private final Logger log = LoggerFactory.getLogger(EntryResource.class);

	// TODO: Use GraphUtil.supportedMediaTypes instead of this list?
	private final List<MediaType> supportedMediaTypes = List.of(
		APPLICATION_RDF_XML,
		APPLICATION_JSON,
		TEXT_RDF_N3,
		new MediaType(RDFFormat.TURTLE.getDefaultMIMEType()),
		new MediaType(RDFFormat.TRIX.getDefaultMIMEType()),
		new MediaType(RDFFormat.NTRIPLES.getDefaultMIMEType()),
		new MediaType(RDFFormat.TRIG.getDefaultMIMEType()),
		new MediaType(RDFFormat.JSONLD.getDefaultMIMEType()),
		new MediaType(RDFFormat.RDFJSON.getDefaultMIMEType())
	);

	private UserTempLockoutCache userTempLockoutCache;
	private ResourceJsonSerializer resourceSerializer;

	@Override
	public void doInit() {
		this.userTempLockoutCache = getUserTempLockoutCache();
		this.resourceSerializer = new ResourceJsonSerializer(getPM(), getCM(), userTempLockoutCache);
	}

	@Override
	public Representation head() {
		return head(null);
	}

	@Override
	public Representation head(Variant v) {
		try {
			if (entry == null) {
				getResponse().setStatus(Status.CLIENT_ERROR_NOT_FOUND);
				return new EmptyRepresentation();
			}
			return createEmptyRepresentationWithLastModified(entry.getModifiedDate());
		} catch (AuthorizationException e) {
			return unauthorizedHEAD();
		}
	}

	/**
	 * GET
	 * <p>
	 * From the REST API:
	 *
	 * <pre>
	 * GET {baseURI}/{portfolio-id}/entry/{entry-id}
	 * </pre>
	 *
	 * @return The Representation as JSON
	 */
	@Get
	public Representation represent() {
		try {
			if (entry == null) {
				getResponse().setStatus(Status.CLIENT_ERROR_NOT_FOUND);
				return new JsonRepresentation(JSONErrorMessages.errorEntryNotFound);
			}

			// the check for resource safety is necessary to avoid an implicit
			// getMetadata() in the case of a PUT on (not yet) existent metadata
			// - this is e.g. the case if conditional requests are issued
			Optional<MediaType> preferredMediaType = Optional.ofNullable(getRequest().getClientInfo().getPreferredMediaType(supportedMediaTypes));

			MediaType rdfFormat = MediaType.APPLICATION_JSON;
			if (RDFFormat.JSONLD.getDefaultMIMEType().equals(parameters.get("rdfFormat"))) {
				rdfFormat = new MediaType(RDFFormat.JSONLD.getDefaultMIMEType());
			}

			Representation result = switch (getRequest().getMethod().getName()) {
				case "GET" -> getEntry((format != null) ? format : preferredMediaType.orElse(APPLICATION_RDF_XML), rdfFormat);
				default -> new EmptyRepresentation();
			};

			Date lastMod = entry.getModifiedDate();
			if (lastMod != null) {
				result.setModificationDate(lastMod);
				result.setTag(Util.createTag(lastMod));
			}

			return result;
		} catch (AuthorizationException e) {
			return unauthorizedGET();
		}
	}

	@Put
	public void storeRepresentation(Representation r) {
		try {
			if (getRequest().getEntity().isEmpty()) {
				getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
				return;
			}
			modifyEntry((format != null) ? format : getRequestEntity().getMediaType());
			getResponse().setEntity(createEmptyRepresentationWithLastModified(entry.getModifiedDate()));
		} catch (AuthorizationException e) {
			unauthorizedPUT();
		}
	}

	@Delete
	public void removeRepresentations() {
		try {
			if (entry != null && context != null) {
				if (GraphType.List.equals(entry.getGraphType()) && parameters.containsKey("recursive")) {
					org.entrystore.List l = (org.entrystore.List) entry.getResource();
					if (l != null) {
						l.removeTree();
					} else {
						log.warn("Resource of the following list is null: " + entry.getEntryURI());
					}
				} else {
					context.remove(entry.getEntryURI());
				}
			} else {
				getResponse().setStatus(Status.CLIENT_ERROR_NOT_FOUND);
			}
		} catch (AuthorizationException e) {
			unauthorizedDELETE();
		} catch (EntryMissingException e) {
			getResponse().setStatus(Status.CLIENT_ERROR_NOT_FOUND);
		}
	}

	private Representation getEntry(MediaType mediaType, MediaType rdfFormat) {
		String serializedGraph;
		/* if (MediaType.TEXT_HTML.equals(mediaType)) {
			return getEntryInHTML();
		} else */
		if (APPLICATION_JSON.equals(mediaType)) {
			return getEntryInJSON(rdfFormat);
		} else {
			Model graph = entry.getGraph();
			serializedGraph = GraphUtil.serializeGraph(graph, mediaType);
			if (serializedGraph != null) {
				getResponse().setStatus(Status.SUCCESS_OK);
				return new StringRepresentation(serializedGraph, mediaType);
			}
		}

		getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
		return new EmptyRepresentation();
	}

	/* Temporarily disabled code, see ENTRYSTORE-435 for details
	private Representation getEntryInHTML() {
		try {
			if (parameters != null) {
				parameters.put("includeAll", "true");
			}
			JSONObject jobj = this.getEntryAsJSONObject();
			String storejs = config.getString(Settings.STOREJS_JS, "/storejs/storejs.js");
			String storecss = config.getString(Settings.STOREJS_CSS, "/storejs/storejs.css");
			if (jobj != null) {
				return new StringRepresentation(
						"<html><head><meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\">"+
						"<script type=\"text/javascript\">dojoConfig = {deps: [\"store/boot\"]};</script>"+
						"<script type=\"text/javascript\" src=\""+storejs+"\"></script>"+
						"<link type=\"text/css\" href=\""+storecss+"\" rel=\"stylesheet\"></link></head><body><textarea>" +
						jobj.toString(2) +
						"</textarea></body></html>", MediaType.TEXT_HTML);
			}
		} catch (JSONException e) {
			log.error(e.getMessage());
		}
		getResponse().setStatus(Status.CLIENT_ERROR_NOT_FOUND);
		return new JsonRepresentation(JSONErrorMessages.errorEntryNotFound);
	}
	*/

	/**
	 * Gets the entry JSON
	 *
	 * @return JSON representation
	 */
	private Representation getEntryInJSON(MediaType rdfFormat) {
		try {
			JSONObject jobj = getEntryAsJSONObject(rdfFormat);
			return new JsonRepresentation(jobj.toString(2));
		} catch (JSONException e) {
			log.error(e.getMessage(), e);
		} catch (IllegalArgumentException e) {
			log.error(e.getMessage(), e);
			getResponse().setStatus(Status.SERVER_ERROR_INTERNAL);
			return new JsonRepresentation(new JSONObject().put("error", e.getMessage()));
		}
		getResponse().setStatus(Status.CLIENT_ERROR_NOT_FOUND);
		return new JsonRepresentation(JSONErrorMessages.errorEntryNotFound);
	}


	private JSONObject getEntryAsJSONObject(MediaType rdfFormat) throws JSONException {
		JSONObject mainJsonObject = new JSONObject();

		GraphType graphType = entry.getGraphType();
		EntryType entryType = entry.getEntryType();

		/*
		 * Entry id
		 */
		mainJsonObject.put("entryId", entry.getId());

		/*
		 * Context or SystemContext
		 */
		if ((graphType == GraphType.Context || graphType == GraphType.SystemContext) && entryType == Local) {
			mainJsonObject.put("name", getCM().getName(entry.getResourceURI()));
			if (entry.getRepositoryManager().hasQuotas()) {
				JSONObject quotaObj = new JSONObject();
				Context c = getCM().getContext(this.entryId);
				if (c != null) {
					quotaObj.put("quota", c.getQuota());
					quotaObj.put("fillLevel", c.getQuotaFillLevel());
				}
				quotaObj.put("hasDefaultQuota", c.hasDefaultQuota());
				mainJsonObject.put("quota", quotaObj);
			}
		}

		/*
		 * Entry information
		 */
		Model entryGraph = entry.getGraph();
		JSONObject entryObj = GraphUtil.serializeGraphToJson(entryGraph, rdfFormat);
		mainJsonObject.accumulate("info", entryObj);

		/*
		 * If the parameter includeAll is set we must return more JDIl with
		 * example local metadata, cached external metadata and maybe a
		 * resource. If not set, return now.
		 */
		if (parameters == null || !parameters.containsKey("includeAll")) {
			return mainJsonObject;
		}

		/*
		 * Cached External Metadata
		 */
		if (entryType == LinkReference || entryType == Reference) {
			try {
				Metadata cachedExternalMetadata = entry.getCachedExternalMetadata();
				Model cachedMetadataGraph = cachedExternalMetadata.getGraph();
				if (cachedMetadataGraph != null) {
					JSONObject cachedExternalMetadataJsonObject = GraphUtil.serializeGraphToJson(cachedMetadataGraph, rdfFormat);
					mainJsonObject.accumulate(RepositoryProperties.EXTERNAL_MD_PATH, cachedExternalMetadataJsonObject);
				}
			} catch (AuthorizationException ae) {
				//mainJsonObject.accumulate("noAccessToMetadata", true);
				//TODO: Replaced by using "rights" in json, do something else in this catch-clause
			}
		}

		/*
		 * Local Metadata
		 */
		if (entryType == Local || entryType == Link || entryType == LinkReference) {
			try {
				Metadata localMetadata = entry.getLocalMetadata();
				Model localMetadataGraph = localMetadata.getGraph();
				if (localMetadataGraph != null) {
					JSONObject localMetaDataJsonObject = GraphUtil.serializeGraphToJson(localMetadataGraph, rdfFormat);
					mainJsonObject.accumulate(RepositoryProperties.MD_PATH, localMetaDataJsonObject);
				}
			} catch (AuthorizationException ae) {
				/*if (!mainJsonObject.has("noAccessToMetadata")) {
					mainJsonObject.accumulate("noAccessToMetadata", true);
				}*/
				//TODO: Replaced by using "rights" in json, do something else in this catch-clause
			}
		}

		/*
		 *	Relations
		 */
		List<Statement> relations = entry.getRelations();
		if (relations != null) {
			JSONObject relationsJsonObject = GraphUtil.serializeGraphToJson(new LinkedHashModel(relations), rdfFormat);
			mainJsonObject.accumulate(RepositoryProperties.RELATION, relationsJsonObject);
		}

		/*
		 * Rights
		 */
		JSONArray rights = resourceSerializer.serializeRights(entry);
		mainJsonObject.put("rights", rights);

		/*
		 * Local resource
		 */
		if (entryType == Local) {
			Resource resource = entry.getResource();
			// As serializeResourceString must return a String, not a JSONObject, we must define this as Object.
			Object resourceObject = (graphType == null) ? IMMUTABLE_EMPTY_JSONOBJECT : switch (graphType) {
				case List -> resourceSerializer.serializeResourceList(resource, new ListParams(parameters), rdfFormat);
				case User -> resourceSerializer.serializeResourceUser(resource);
				case Group -> resourceSerializer.serializeResourceGroup(resource, rdfFormat);
				case None -> resourceSerializer.serializeResourceNone(resource);
				case String -> resourceSerializer.serializeResourceString(resource);
				case Graph -> resourceSerializer.serializeResourceGraph(resource, rdfFormat);
				case Pipeline -> resourceSerializer.serializeResourcePipeline(resource, rdfFormat);
				//TODO other types, for example Context, SystemContext, PrincipalManager, etc
				case ResultList, PipelineResult, Context, SystemContext -> IMMUTABLE_EMPTY_JSONOBJECT;
			};
			mainJsonObject.accumulate("resource", resourceObject);
		}
		return mainJsonObject;
	}

	private void modifyEntry(MediaType mediaType) throws AuthorizationException {

		String graphString;
		try {
			graphString = getRequest().getEntity().getText();
		} catch (IOException e) {
			log.info(e.getMessage());
			getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
			return;
		}

		Model deserializedGraph;
		if (APPLICATION_JSON.equals(mediaType)) {
			try {
				JSONObject rdfJSON = new JSONObject(graphString);
				deserializedGraph = RDFJSON.rdfJsonToGraph(rdfJSON);
			} catch (JSONException e) {
				log.info(e.getMessage());
				getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
				return;
			}
		} else {
			deserializedGraph = GraphUtil.deserializeGraph(graphString, mediaType);
		}

		if (deserializedGraph == null) {
			getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
		} else {
			entry.setGraph(deserializedGraph);
			if (parameters.containsKey("applyACLtoChildren") &&
				GraphType.List.equals(entry.getGraphType()) &&
				Local.equals(entry.getEntryType())) {
				((org.entrystore.List) entry.getResource()).applyACLtoChildren(true);
			}
			getResponse().setStatus(Status.SUCCESS_OK);
		}
	}

}