ResourceResource.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 com.google.common.html.HtmlEscapers;
import com.rometools.rome.feed.synd.SyndContent;
import com.rometools.rome.feed.synd.SyndContentImpl;
import com.rometools.rome.feed.synd.SyndEntry;
import com.rometools.rome.feed.synd.SyndEntryImpl;
import com.rometools.rome.feed.synd.SyndFeed;
import com.rometools.rome.feed.synd.SyndFeedImpl;
import com.rometools.rome.io.FeedException;
import com.rometools.rome.io.SyndFeedOutput;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.io.IOUtils;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrQuery.ORDER;
import org.apache.solr.client.solrj.util.ClientUtils;
import org.eclipse.rdf4j.model.Model;
import org.eclipse.rdf4j.rio.RDFFormat;
import org.entrystore.AuthorizationException;
import org.entrystore.Context;
import org.entrystore.Data;
import org.entrystore.Entry;
import org.entrystore.EntryType;
import org.entrystore.GraphType;
import org.entrystore.Group;
import org.entrystore.QuotaException;
import org.entrystore.Resource;
import org.entrystore.ResourceType;
import org.entrystore.User;
import org.entrystore.impl.DataImpl;
import org.entrystore.impl.ListImpl;
import org.entrystore.impl.RDFResource;
import org.entrystore.impl.StringResource;
import org.entrystore.repository.RepositoryException;
import org.entrystore.repository.config.Settings;
import org.entrystore.repository.security.Password;
import org.entrystore.repository.util.EntryUtil;
import org.entrystore.repository.util.FileOperations;
import org.entrystore.repository.util.SolrSearchIndex;
import org.entrystore.rest.EntryStoreApplication;
import org.entrystore.rest.auth.BasicVerifier;
import org.entrystore.rest.auth.CookieVerifier;
import org.entrystore.rest.auth.LoginTokenCache;
import org.entrystore.rest.auth.UserTempLockoutCache;
import org.entrystore.rest.serializer.ResourceJsonSerializer;
import org.entrystore.rest.serializer.ResourceJsonSerializer.ListParams;
import org.entrystore.rest.util.Email;
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.Client;
import org.restlet.Request;
import org.restlet.Response;
import org.restlet.Uniform;
import org.restlet.data.Disposition;
import org.restlet.data.MediaType;
import org.restlet.data.Method;
import org.restlet.data.Protocol;
import org.restlet.data.Reference;
import org.restlet.data.Status;
import org.restlet.ext.json.JsonRepresentation;
import org.restlet.representation.EmptyRepresentation;
import org.restlet.representation.FileRepresentation;
import org.restlet.representation.Representation;
import org.restlet.representation.StringRepresentation;
import org.restlet.resource.Delete;
import org.restlet.resource.Get;
import org.restlet.resource.Post;
import org.restlet.resource.Put;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringWriter;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Vector;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

/**
 * This class is the resource for entries.
 */
public class ResourceResource extends BaseResource {

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

	private  final EmptyRepresentation EMPTY_REPRESENTATION = new EmptyRepresentation();
	private final List<MediaType> supportedMediaTypes = java.util.List.of(
			MediaType.APPLICATION_RDF_XML,
			MediaType.APPLICATION_JSON,
			MediaType.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("application/rdf+json")
	);

	private UserTempLockoutCache userTempLockoutCache;
	private ResourceJsonSerializer resourceSerializer;

	private static Boolean rewriteMediaTypeJavaScript;

	@Override
	public void doInit() {
		this.userTempLockoutCache = getUserTempLockoutCache();
		this.resourceSerializer = new ResourceJsonSerializer(getPM(), getCM(), userTempLockoutCache);
		if (rewriteMediaTypeJavaScript == null) {
			rewriteMediaTypeJavaScript = getRM().getConfiguration().getBoolean(Settings.HTTP_ALLOW_MEDIA_TYPE_JAVASCRIPT, false);
		}
	}

	/**
	 * GET
	 *
	 * From the REST API:
	 *
	 * <pre>
	 * GET {baseURI}/{portfolio-id}/resource/{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);
			}

			Representation result = null;

			// the check for resource safety is necessary to avoid an implicit
			// getMetadata() in the case of a PUT on (not yet) existant metadata
			// - this is e.g. the case if conditional requests are issued
			if (Method.GET.equals(getRequest().getMethod())) {
				result = getResource();
			} else {
				result = new EmptyRepresentation();
			}

			if (result != null) {
				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) {
		if (entry == null) {
			getResponse().setStatus(Status.CLIENT_ERROR_NOT_FOUND);
			return;
		}

		try {
			modifyResource();
			entry.updateModificationDate();
			getResponse().setEntity(createEmptyRepresentationWithLastModified(entry.getModifiedDate()));
		} catch(AuthorizationException e) {
			unauthorizedPUT();
		}
	}

	@Delete
	public void removeRepresentation() {
		if (entry == null) {
			getResponse().setStatus(Status.CLIENT_ERROR_NOT_FOUND);
			return;
		}

		EntryType entryType = entry.getEntryType();

		try {
			if ((entryType == EntryType.Link || entryType == EntryType.Reference || entryType == EntryType.LinkReference)
					 && "true".equalsIgnoreCase(parameters.get("proxy"))) {

				final Response delResponse = deleteRemoteResource(entry.getResourceURI().toString(), 0);
				if (delResponse != null) {
					getResponse().setEntity(delResponse.getEntity());
					getResponse().setStatus(delResponse.getStatus());
					getResponse().setOnSent(new Uniform() {
						public void handle(Request request, Response response) {
							try {
								delResponse.release();
							} catch (Exception e) {
								log.error(e.getMessage());
							}
						}
					});
				} else {
					getResponse().setStatus(Status.SERVER_ERROR_INTERNAL);
				}
			} else {
				deleteLocalResource();
			}
		} catch(AuthorizationException e) {
			unauthorizedDELETE();
		}
	}

	@Post
	public void acceptRepresentation(Representation r) {
		if (entry == null) {
			getResponse().setStatus(Status.CLIENT_ERROR_NOT_FOUND);
			return;
		}

		GraphType graphType = entry.getGraphType();

		try {
			if (graphType == GraphType.List
				 && parameters.containsKey("import")
				 && MediaType.APPLICATION_ZIP.equals(getRequestEntity().getMediaType())) {

			getResponse().setStatus(importFromZIP(getRequestEntity()));
		} else if (graphType == GraphType.List
							 && parameters.containsKey("moveEntry")
							 && parameters.containsKey("fromList")) {

				// POST 3/resource/45?moveEntry=2/entry/34&fromList=2/resource/67
				ListImpl dest = (ListImpl) this.entry.getResource();
				String movableEntryString = parameters.get("moveEntry");
				String movableEntrySourceString = parameters.get("fromList");
				boolean removeAll = parameters.get("removeAll") != null;

				String baseURI = getRM().getRepositoryURL().toString();
				if (!baseURI.endsWith("/")) {
					baseURI += "/";
				}

				// Entry URI of the Entry to be moved
				URI movableEntry = movableEntryString.startsWith("http://") ? URI.create(movableEntryString) : URI.create(baseURI + movableEntryString);
				// Resource URI of the source List

				URI movableEntrySource = null;
				if (movableEntrySourceString != null) {
					movableEntrySource = movableEntrySourceString.startsWith("http://") ? URI.create(movableEntrySourceString) : URI.create(baseURI + movableEntrySourceString);
				}

				Entry movedEntry = null;
				String error = null;
				try {
					movedEntry = dest.moveEntryHere(movableEntry, movableEntrySource, removeAll);
				} catch (QuotaException qe) {
					error = qe.getMessage();
					log.warn(qe.getMessage());
					getResponse().setStatus(Status.CLIENT_ERROR_REQUEST_ENTITY_TOO_LARGE);
				} catch (IOException ioe) {
					error = ioe.getMessage();
					log.error(ioe.getMessage());
					getResponse().setStatus(Status.SERVER_ERROR_INTERNAL);
				}
				if (error != null) {
					JSONObject jsonError = new JSONObject();
					try {
						jsonError.put("error", error);
					} catch (JSONException e) {
						log.error(e.getMessage());
					}
					getResponse().setEntity(new JsonRepresentation(jsonError));
					return;
				}

				JSONObject result = new JSONObject();
				try {
					result.put("entryURI", movedEntry.getEntryURI());
				} catch (JSONException e) {
					log.error(e.getMessage());
				}
				getResponse().setEntity(new JsonRepresentation(result));
				getResponse().setStatus(Status.SUCCESS_OK);
			} else {
				getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
			}
		} catch(AuthorizationException e) {
			unauthorizedPOST();
		}
	}

	/**
	 * Gets the resource's JSON representation
	 *
	 * @return JSON representation
	 */
	private Representation getResource() throws AuthorizationException {
		// RSS feed
		if (parameters.containsKey("syndication")) {
			try {
				if (getRM().getIndex() == null) {
					getResponse().setStatus(Status.SERVER_ERROR_NOT_IMPLEMENTED);
					return new JsonRepresentation("{\"error\":\"Feeds are not supported by this installation\"}");
				}
				StringRepresentation rep = getSyndicationSolr(entry, parameters.get("syndication"));
				if (rep == null) {
					getResponse().setStatus(Status.CLIENT_ERROR_METHOD_NOT_ALLOWED);
					return new JsonRepresentation(JSONErrorMessages.errorNotAContext);
				}
				return rep;
			} catch (IllegalArgumentException e) {
				getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
				return new JsonRepresentation(JSONErrorMessages.syndicationFormat);
			}
		}

		/*
		 * Resource
		 */

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

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

		// Resource missing
		if (graphType == null) {
			getResponse().setStatus(Status.CLIENT_ERROR_NOT_FOUND);
			return new JsonRepresentation("{\"error\":\"No resource available for resource " + entry.getResourceURI());
		}

		MediaType preferredMediaType = getRequest().getClientInfo().getPreferredMediaType(supportedMediaTypes);
		if (preferredMediaType == null) {
			preferredMediaType = MediaType.APPLICATION_RDF_XML;
		}
		preferredMediaType = (format != null) ? format : preferredMediaType;

		// Graph and List resource

		if (graphType == GraphType.Graph || graphType == GraphType.List) {
			boolean isList = (entry.getGraphType() == GraphType.List);
			Model graph = null;

			if (isList) {
				graph = ((org.entrystore.List) entry.getResource()).getGraph();
			} else {
				graph = ((RDFResource) entry.getResource()).getGraph();
			}

			if (graph != null) {
				String serializedGraph = null;
				if (MediaType.APPLICATION_JSON.equals(preferredMediaType)) {
					if (isList) {
						return serializeJsonRepresentationResourceList(entry, new ListParams(parameters));
					}
					serializedGraph = RDFJSON.graphToRdfJson(graph);
				} else {
					serializedGraph = GraphUtil.serializeGraph(graph, preferredMediaType);
				}

				if (serializedGraph != null) {
					getResponse().setStatus(Status.SUCCESS_OK);
					return new StringRepresentation(serializedGraph, preferredMediaType);
				} else {
					getResponse().setStatus(Status.CLIENT_ERROR_NOT_ACCEPTABLE);
					return new JsonRepresentation(JSONErrorMessages.errorUnknownFormat);
				}
			}
		}

		// Remote Document (GraphType == None) Resource

		if (entryType == EntryType.Link || entryType == EntryType.LinkReference || entryType == EntryType.Reference) {
			if (graphType == GraphType.None) {
				getResponse().setLocationRef(new Reference(entry.getResourceURI().toString()));
				getResponse().setStatus(Status.REDIRECTION_SEE_OTHER);
				return null;
			}
		}

		/*
		 * Local Resource
		 */

		if (entryType == EntryType.Local) {

			// Named Local Resource
			if (resourceType == ResourceType.NamedResource) {
				redirectSeeOther(entry.getLocalMetadataURI().toString());
				return new EmptyRepresentation();
			}

			try {
				Resource resource = entry.getResource();
				return switch (graphType) {
					case None -> serializeFileRepresentationResourceNone(entry);
					case User -> new JsonRepresentation(resourceSerializer.serializeResourceUser(resource));
					case Group -> new JsonRepresentation(resourceSerializer.serializeResourceGroup(resource, rdfFormat));
					case String -> new JsonRepresentation(resourceSerializer.serializeResourceString(resource));
					case Context -> new JsonRepresentation(resourceSerializer.serializeResourceContext(resource));
					case SystemContext -> new JsonRepresentation(resourceSerializer.serializeResourceSystemContext(resource));
					case Pipeline -> {
						if (resource instanceof RDFResource pipeline) {
							if (pipeline.getGraph() == null) {
								getResponse().setStatus(Status.CLIENT_ERROR_NOT_FOUND);
								yield new JsonRepresentation("{\"error\":\"The pipeline has not been set\"}");
							}
							yield new JsonRepresentation(resourceSerializer.serializeResourcePipeline(pipeline, rdfFormat));
						}
						yield EMPTY_REPRESENTATION;
					}
					case ResultList, PipelineResult -> EMPTY_REPRESENTATION;
                    default -> EMPTY_REPRESENTATION;
                };
			} catch (IllegalArgumentException e){
				log.error(e.getMessage(), e);
				getResponse().setStatus(Status.SERVER_ERROR_INTERNAL);
				return new JsonRepresentation("{\"error\":\"Internal Server Error\"}");
			}
		}
		return EMPTY_REPRESENTATION;
	}

	private FileRepresentation serializeFileRepresentationResourceNone(Entry entry) {
		if (entry.getResourceType() == ResourceType.InformationResource) {
			// Local data
			File file = ((Data)entry.getResource()).getDataFile();
			if  (file != null) {
				String medTyp = entry.getMimetype();
				FileRepresentation rep;
				if (medTyp != null) {
					try {
						if (rewriteMediaTypeJavaScript) {
							if (medTyp.toLowerCase().contains("javascript")) {
								log.info("Rewriting media type {} to text/plain for {}", medTyp, entry.getResourceURI());
								medTyp = MediaType.TEXT_PLAIN.toString();
							}
						}
						rep = new FileRepresentation(file, MediaType.valueOf(medTyp));
					} catch (IllegalArgumentException iae) {
						log.warn("Invalid media type for {}: {}", entry.getEntryURI(), iae.getMessage());
						rep = new FileRepresentation(file, MediaType.ALL);
					}
				} else {
					rep = new FileRepresentation(file, MediaType.ALL);
				}
				String fileName = entry.getFilename();
				if (fileName == null) {
					fileName = entry.getId();
				}
				Disposition disp = rep.getDisposition();
				disp.setFilename(fileName);
				if (!getRM().getConfiguration().getBoolean(Settings.HTTP_ALLOW_CONTENT_DISPOSITION_INLINE, true)
						|| parameters.containsKey("download")) {
					disp.setType(Disposition.TYPE_ATTACHMENT);
				} else {
					disp.setType(Disposition.TYPE_INLINE);
				}

				DataImpl data = new DataImpl(entry);
				String digest = data.readDigest();
				if (digest != null) {
					getResponse().getHeaders().set("Digest", "sha-256=" + digest);
				} else {
					log.debug("Digest does not exist for [{}]", entry.getResourceURI());
				}

				return rep;
			}
		} else if (entry.getResourceType() == ResourceType.NamedResource) {
			// If there is no resource we redirect to the metadata
			getResponse().setLocationRef(new Reference(entry.getLocalMetadataURI()));
			getResponse().setStatus(Status.REDIRECTION_SEE_OTHER);
		}
		// NOT USED YET
		//	if (ResourceType.Unknown.equals(entry.getResourceType())) {}
		return null;
	}

	private JsonRepresentation serializeJsonRepresentationResourceList(Entry entry, ListParams listParams) {
		JSONArray array = new JSONArray();
		org.entrystore.List l = (org.entrystore.List) this.entry.getResource();
		List<URI> uris = l.getChildren();
		Set<String> IDs = new HashSet<>();
		for (URI u : uris) {
			String id = (u.toASCIIString()).substring((u.toASCIIString()).lastIndexOf('/') + 1);
			IDs.add(id);
		}

		if (parameters.containsKey("sort") && (IDs.size() < 501)) {
			List<Entry> childrenEntries = new ArrayList<Entry>();
			for (String id : IDs) {
				Entry childEntry = this.context.get(id);
				if (childEntry != null) {
					childrenEntries.add(childEntry);
				} else {
					log.warn("Child entry " + id + " in context " + context.getURI() + " does not exist, but is referenced by a list.");
				}
			}

			Date before = new Date();
			boolean asc = !"desc".equalsIgnoreCase(parameters.get("order"));
			GraphType prioritizedResourceType = null;
			if (parameters.containsKey("prio")) {
				prioritizedResourceType = GraphType.valueOf(parameters.get("prio"));
			}
			String sortType = parameters.get("sort");
			if ("title".equalsIgnoreCase(sortType)) {
				String lang = parameters.get("lang");
				EntryUtil.sortAfterTitle(childrenEntries, lang, asc, prioritizedResourceType);
			} else if ("modified".equalsIgnoreCase(sortType)) {
				EntryUtil.sortAfterModificationDate(childrenEntries, asc, prioritizedResourceType);
			} else if ("created".equalsIgnoreCase(sortType)) {
				EntryUtil.sortAfterCreationDate(childrenEntries, asc, prioritizedResourceType);
			} else if ("size".equalsIgnoreCase(sortType)) {
				EntryUtil.sortAfterFileSize(childrenEntries, asc, prioritizedResourceType);
			}
			long sortDuration = new Date().getTime() - before.getTime();
			log.debug("List entry sorting took " + sortDuration + " ms");

			for (Entry childEntry : childrenEntries) {
				URI childURI = childEntry.getEntryURI();
				String id = (childURI.toASCIIString()).substring((childURI.toASCIIString()).lastIndexOf('/') + 1);
				array.put(id);
			}
		} else {
			if (IDs.size() > 500) {
				log.warn("No sorting performed because of list size bigger than 500 children");
			}
			for (String id : IDs) {
				array.put(id);
			}
		}
		return new JsonRepresentation(array.toString());
	}

	public Set<Entry> getListChildrenRecursively(Entry listEntry) {
		Set<Entry> result = new HashSet<>();
		if (GraphType.List.equals(listEntry.getGraphType()) && EntryType.Local.equals(listEntry.getEntryType())) {
			org.entrystore.List l = (org.entrystore.List) listEntry.getResource();
			List<URI> c = l.getChildren();
			for (URI uri : c) {
				Entry e = getRM().getContextManager().getEntry(uri);
				if (e != null) {
					if (GraphType.List.equals(e.getGraphType())) {
						result.addAll(getListChildrenRecursively(e));
					} else {
						result.add(e);
					}
				}
			}
		} else {
			result.add(listEntry);
		}
		return result;
	}

	/**
	 * Deletes the resource if the entry has any.
	 */
	private void deleteLocalResource() {
		/*
		 * List
		 */
		if (entry.getGraphType() == GraphType.List) {
			ListImpl l = (ListImpl) entry.getResource();
			if (parameters.containsKey("recursive")) {
				l.removeTree();
			} else {
				l.setChildren(new Vector<URI>());
			}
		}

		/*
		 * None
		 */
		if (entry.getGraphType() == GraphType.None ) {
			if (entry.getResourceType() == ResourceType.InformationResource) {
				Data data = (Data) entry.getResource();
				if (!data.delete()) {
					log.error("Unable to delete resource of entry " + entry.getEntryURI());
					getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
					getResponse().setEntity(new JsonRepresentation(JSONErrorMessages.errorUnknownKind));
				}
			}
		}
	}

	private Response deleteRemoteResource(String url, int loopCount) {
		if (loopCount > 10) {
			log.warn("More than 10 redirect loops detected, aborting");
			return null;
		}

		Client client = new Client(Protocol.HTTP);
		client.setContext(new org.restlet.Context());
		client.getContext().getParameters().add("connectTimeout", "10000");
		client.getContext().getParameters().add("readTimeout", "10000");
		client.getContext().getParameters().set("socketTimeout", "10000");
		client.getContext().getParameters().set("socketConnectTimeoutMs", "10000");
		log.info("Initialized HTTP client for proxy request to delete remote resource");

		Request request = new Request(Method.DELETE, url);
		request.getClientInfo().setAcceptedMediaTypes(getRequest().getClientInfo().getAcceptedMediaTypes());
		Response response = client.handle(request);

		if (response.getStatus().isRedirection()) {
			Reference ref = response.getLocationRef();
			response.getEntity().release();
			if (ref != null) {
				String refURL = ref.getIdentifier();
				log.info("Request redirected to " + refURL);
				return deleteRemoteResource(refURL, ++loopCount);
			}
		}

		if (response.getEntity() != null &&
				response.getEntity().getLocationRef() != null &&
				response.getEntity().getLocationRef().getBaseRef() == null) {

			response.getEntity().getLocationRef().setBaseRef(url.substring(0, url.lastIndexOf("/")+1));
		}
		return response;
	}

	private Status importFromZIP(Representation rep) {
		File tmpFile = null;
		try {
			tmpFile = writeStreamToTmpFile(rep.getStream());
			if (tmpFile != null && tmpFile.exists()) {
				ZipFile zipFile = new ZipFile(tmpFile);
				Enumeration<? extends ZipEntry> zipEntries = zipFile.entries();
				while (zipEntries.hasMoreElements()) {
					ZipEntry entry = zipEntries.nextElement();
					String nameLC = entry.getName();
					if (!entry.isDirectory() && (nameLC.endsWith(".xml") || nameLC.endsWith(".rdf"))) {
						InputStream fileIS = zipFile.getInputStream(entry);
						if (fileIS == null) {
							log.error("Unable to get InputStream of ZipEntry: " + nameLC);
							continue;
						}
						String fileString = null;
						try {
							StringWriter writer = new StringWriter();
							IOUtils.copy(fileIS, writer, StandardCharsets.UTF_8);
							fileString = writer.toString();
							if (fileString == null) {
								log.error("[IMPORT] Problem with reading ZipEntry into String");
								continue;
							}
						} finally {
							if (fileIS != null) {
								fileIS.close();
							}
						}
						if (nameLC.endsWith(".rdf")) {
							importRDFResource(fileString);
						}
					}
				}
			} else {
				return Status.SERVER_ERROR_INTERNAL;
			}
		} catch (IOException ioe) {
			log.error(ioe.getMessage());
			return Status.SERVER_ERROR_INTERNAL;
		} finally {
			if (tmpFile != null) {
				tmpFile.delete();
			}
		}

		return Status.SUCCESS_CREATED;
	}

	public File writeStreamToTmpFile(InputStream is) throws IOException {
		File tmpFile = File.createTempFile("scam_import_", ".zip");
		log.info("[IMPORT] Created temporary file: " + tmpFile);
		OutputStream fos = Files.newOutputStream(tmpFile.toPath());
		FileOperations.copyFile(is, fos);
		return tmpFile;
	}

	private void importRDFResource(String rdfString) {
		// TODO
	}

	public StringRepresentation getSyndicationSolr(Entry entry, String type) {
		if (getRM().getIndex() == null) {
			return null;
		}

		int FEED_SIZE = 250;
		if (parameters.containsKey("feedSize")) {
			try {
				FEED_SIZE = Integer.parseInt(parameters.get("feedSize"));
			} catch (NumberFormatException nfe) {
				log.warn("Feed size parameter was not a legal Integer: " + nfe.getMessage());
			}
		}

		SyndFeed feed = new SyndFeedImpl();
		feed.setFeedType(type);

		GraphType gt = entry.getGraphType();
		if (!GraphType.Context.equals(gt) && !GraphType.List.equals(gt)) {
			return null;
		}

		String solrQueryValue;
		String alias;

		if (GraphType.Context.equals(gt)) {
			alias = getCM().getName(entry.getResourceURI());
			solrQueryValue = "context:";
		} else {
			alias = EntryUtil.getTitle(entry, "en");
			solrQueryValue = "lists:";
		}

		feed.setTitle("Feed of \"" + alias + "\"");
		feed.setDescription("A syndication feed containing the 50 most recent items from \"" + alias + "\"");
		feed.setLink(entry.getResourceURI().toString());

		solrQueryValue += ClientUtils.escapeQueryChars(entry.getResourceURI().toString());
		SolrQuery solrQuery = new SolrQuery(solrQueryValue);
		solrQuery.setStart(0);
		solrQuery.setRows(1000);
		solrQuery.setSort("modified", ORDER.desc);

		List<SyndEntry> syndEntries = new ArrayList<SyndEntry>();
		Set<Entry> searchEntries = ((SolrSearchIndex) getRM().getIndex()).sendQuery(solrQuery).getEntries();
		List<Entry> recursiveEntries = new LinkedList<Entry>();
		for (Entry e : searchEntries) {
			recursiveEntries.addAll(getListChildrenRecursively(e));
		}
		EntryUtil.sortAfterModificationDate(recursiveEntries, false, null);

		int limitedCount = 0;
		for (Entry e : recursiveEntries) {
			SyndEntry syndEntry;
			syndEntry = new SyndEntryImpl();
			syndEntry.setTitle(EntryUtil.getTitle(e, "en"));
			syndEntry.setPublishedDate(e.getCreationDate());
			syndEntry.setUpdatedDate(e.getModifiedDate());
			syndEntry.setLink(e.getResourceURI().toString());

			SyndContent description = new SyndContentImpl();
			description.setType("text/plain");

			Map<String, Set<String>> descriptions = EntryUtil.getDescriptions(e);
			Set<Map.Entry<String, Set<String>>> descEntrySet = descriptions.entrySet();
			String desc = null;
			for (Map.Entry<String, Set<String>> descEntry : descEntrySet) {
				desc = descEntry.getKey();
				Set<String> descriptionLangs = descEntry.getValue();

				for (String lang : descriptionLangs) {
					if ("en".equals(lang)) {
						break;
					}
				}
			}

			if (desc != null) {
				description.setValue(desc);
			}

			syndEntry.setDescription(description);

			URI creator = e.getCreator();
			if (creator != null) {
				Entry creatorEntry = getRM().getPrincipalManager().getByEntryURI(creator);
				String creatorName = EntryUtil.getName(creatorEntry);
				if (creatorName != null) {
					syndEntry.setAuthor(creatorName);
				}
			}

			syndEntries.add(syndEntry);

			if (limitedCount++ >= FEED_SIZE) {
				break;
			}
		}

		feed.setEntries(syndEntries);
		String s = null;
		try {
			s = new SyndFeedOutput().outputString(feed, true);
		} catch (FeedException fe) {
			log.error(fe.getMessage());
			s = fe.getMessage();
		}

		String feedType = feed.getFeedType();
		MediaType mediaType = null;
		if (feedType != null) {
			if (feedType.startsWith("rss_")) {
				mediaType = MediaType.APPLICATION_RSS;
			} else if (feedType.startsWith("atom_")) {
				mediaType = MediaType.APPLICATION_ATOM;
			}
		}

		if (mediaType != null) {
			return new StringRepresentation(s, mediaType);
		} else {
			return new StringRepresentation(s);
		}
	}

	/**
	 * Set a resource to an entry.
	 */
	private void modifyResource() throws AuthorizationException {
		GraphType gt = entry.getGraphType();
		MediaType mediaType = getRequestEntity().getMediaType();
		/*
		 * List and Group
		 */
		if (GraphType.List.equals(gt) || GraphType.Group.equals(gt)) {
			String requestBody = null;
			try {
				requestBody = getRequest().getEntity().getText();
			} catch (IOException e) {
				getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
				return;
			}

			if (MediaType.APPLICATION_JSON.equals(mediaType)) {
				try {
					JSONArray childrenJSONArray = new JSONArray(requestBody);
					ArrayList<URI> newResource = new ArrayList<>();

					// Add new entries to the list.
					for (int i = 0; i < childrenJSONArray.length(); i++) {
						String childId = childrenJSONArray.get(i).toString();
						Entry childEntry = context.get(childId);
						if (childEntry == null) {
							getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
							log.debug("Cannot update list, since one of the children does not exist.");
							return;
						} else {
							newResource.add(childEntry.getEntryURI());
						}
					}

					if (entry.getGraphType() == GraphType.List) {
						org.entrystore.List resourceList = (org.entrystore.List) entry.getResource();
						resourceList.setChildren(newResource);
					} else {
						Group resourceGroup = (Group) entry.getResource();
						resourceGroup.setChildren(newResource);
					}
					getResponse().setStatus(Status.SUCCESS_OK);
				} catch (JSONException e) {
					getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
					getResponse().setEntity(new JsonRepresentation(JSONErrorMessages.errorJSONSyntax));
				} catch (RepositoryException re) {
					log.warn(re.getMessage());
					getResponse().setStatus(Status.CLIENT_ERROR_CONFLICT);
					getResponse().setEntity(new JsonRepresentation(JSONErrorMessages.errorChildExistsInList));
				}
				return; // success!
			} else {
				Model graph = GraphUtil.deserializeGraph(requestBody, mediaType);
				if (graph != null && GraphType.List.equals(entry.getGraphType())) {
					((org.entrystore.List) entry.getResource()).setGraph(graph);
				} else {
					getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
				}
				// TODO add support for groups here
			}
		}

		/*
		 * Data
		 */
		if (gt == GraphType.None) {
			boolean textarea = this.parameters.containsKey("textarea");
			String error = null;

			if (MediaType.MULTIPART_FORM_DATA.equals(mediaType, true)) {
				try {
					List<FileItem> items = Util.createRestletFileUpload(getContext()).parseRepresentation(getRequest().getEntity());
					Iterator<FileItem> iter = items.iterator();
					if (iter.hasNext()) {
						FileItem item = iter.next();
						long maxFileSize = getRM().getMaximumFileSize();

						// we check if the file is not too big
						if (maxFileSize != -1 && item.getSize() > maxFileSize) {
							throw new QuotaException(QuotaException.QUOTA_FILE_TOO_BIG);
						}

						((Data) entry.getResource()).setData(item.getInputStream());
						entry.setFileSize(((Data) entry.getResource()).getDataFile().length());
						String mimeType = item.getContentType();
						if (parameters.containsKey("mimeType")) {
							mimeType = parameters.get("mimeType");
						}
						entry.setMimetype(mimeType);
						String name = item.getName();
						if (name != null && !name.isEmpty()) {
							entry.setFilename(Util.sanitizeFilename(name.trim()));
						}
					}
				} catch (FileUploadException e) {
					error = e.getMessage();
					getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
				} catch (IOException ioe) {
					error = ioe.getMessage();
					getResponse().setStatus(Status.SERVER_ERROR_INTERNAL);
				} catch (QuotaException qe) {
					error = qe.getMessage();
					getResponse().setStatus(Status.CLIENT_ERROR_REQUEST_ENTITY_TOO_LARGE);
				}
			} else {
				Request req = getRequest();
				try {
					((Data) entry.getResource()).setData(req.getEntity().getStream());
					entry.setFileSize(((Data) entry.getResource()).getDataFile().length());
					String mimeType = MediaType.APPLICATION_OCTET_STREAM.toString();
					if (parameters.containsKey("mimeType")) {
						mimeType = parameters.get("mimeType");
					} else if (mediaType != null) {
						mimeType = mediaType.toString();
					}
					entry.setMimetype(mimeType);
					Disposition disp = req.getEntity().getDisposition();
					if (disp != null) {
						String name = disp.getFilename();
						if (name != null && !name.isEmpty()) {
							entry.setFilename(Util.sanitizeFilename(name.trim()));
						}
					}
				} catch (QuotaException qe) {
					error = qe.getMessage();
					getResponse().setStatus(Status.CLIENT_ERROR_REQUEST_ENTITY_TOO_LARGE);
				} catch (IOException ioe) {
					if (ioe.getCause() instanceof NullPointerException) {
						error = ioe.getCause().getMessage();
						getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
					} else {
						error = ioe.getMessage();
						getResponse().setStatus(Status.SERVER_ERROR_INTERNAL);
					}
				}
			}

			if (error != null) {
				if (textarea) {
					getResponse().setEntity("<textarea>{\"error\":\"" + error + "\"}</textarea>", MediaType.TEXT_HTML);
				} else {
					JSONObject jsonError = new JSONObject();
					try {
						jsonError.put("error", error);
					} catch (JSONException jsone) {
						log.error(jsone.getMessage());
					}
					getResponse().setEntity(new JsonRepresentation(error));
				}
				return;
			}

			JSONObject result = new JSONObject();
			result.put("success", "The file was uploaded");
			result.put("format", HtmlEscapers.htmlEscaper().escape(entry.getMimetype()));
			if (textarea) {
				getResponse().setEntity("<textarea>" + result + "</textarea>", MediaType.TEXT_HTML);
			} else {
				getResponse().setEntity(new JsonRepresentation(result));
			}
			getResponse().setStatus(Status.SUCCESS_CREATED);
		}

		/*** String  ***/
		if (gt == GraphType.String) {
			try {
				StringResource stringResource = (StringResource)entry.getResource();
				stringResource.setString(getRequest().getEntity().getText());
			} catch (IOException e) {
				getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
				getResponse().setEntity(new JsonRepresentation("{\"error\":\"Problem with input.\"}"));
			}
		}

		/*** Graph and Pipeline ***/
		if (gt == GraphType.Graph || gt == GraphType.Pipeline) {
			RDFResource graphResource = (RDFResource) entry.getResource();
			if (graphResource != null) {
				Model graph = null;
				try {
					graph = GraphUtil.deserializeGraph(getRequest().getEntity().getText(), mediaType);
				} catch (IOException ioe) {
					getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
					getResponse().setEntity(new JsonRepresentation("{\"error\":\"Unable to read request entity\"}"));
				}
				if (graph != null) {
					graphResource.setGraph(graph);
				} else {
					getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
				}
			} else {
				getResponse().setStatus(Status.SERVER_ERROR_INTERNAL);
				getResponse().setEntity(new JsonRepresentation("{\"error\":\"No RDF resource found for this entry\"}"));
				log.error("No RDF resource found for entry with ResourceType Graph");
			}
		}

		/*** User ***/
		if (GraphType.User.equals(gt)) {
			JSONObject entityJSON = null;
			try {
				entityJSON = new JSONObject(getRequest().getEntity().getText());

				User resourceUser = (User) entry.getResource();
				if (entityJSON.has("name")) {
					String oldName = resourceUser.getName();
					String newName = entityJSON.getString("name");
					if (resourceUser.setName(newName)) {
						// the username was successfully changed, so we need to update the UserInfo
						// objects in the LoginTokenCache to not invalidate logged in user sessions
						LoginTokenCache loginTokenCache = ((EntryStoreApplication)getApplication()).getLoginTokenCache();
						loginTokenCache.renameUser(oldName, newName);
					} else {
						getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
						getResponse().setEntity(new JsonRepresentation("{\"error\":\"Name is already in use\"}"));
						return;
					}
				}
				if (entityJSON.has("password")) {
					boolean requireCurrentPassword = getRM().getConfiguration().getBoolean(Settings.AUTH_PASSWORD_REQUIRE_CURRENT_PASSWORD, true);
					String newPassword =  entityJSON.getString("password");

					if (requireCurrentPassword) {
						// we require the current password if:
						// (1) the user is a non-admin user, or
						// (2) the user is an admin user and wants to set her own password
						if (!getPM().currentUserIsAdminOrAdminGroup() ||
								(getPM().currentUserIsAdminOrAdminGroup() && getPM().getAuthenticatedUserURI().equals(resourceUser.getURI()))) {
							if (!entityJSON.has("currentPassword")) {
								getResponse().setStatus(Status.CLIENT_ERROR_FORBIDDEN);
								getResponse().setEntity(new JsonRepresentation("{\"error\":\"Current password is required\"}"));
								return;
							}
							String currentPassword = entityJSON.getString("currentPassword");
							String saltedHashedSecret = BasicVerifier.getSaltedHashedSecret(getPM(), resourceUser.getName());
							if (saltedHashedSecret == null || !Password.check(currentPassword, saltedHashedSecret)) {
								getResponse().setStatus(Status.CLIENT_ERROR_FORBIDDEN);
								getResponse().setEntity(new JsonRepresentation("{\"error\":\"No password set or incorrect current password provided\"}"));
								return;
							}
						}
					}

					if (resourceUser.setSecret(newPassword)) {
						LoginTokenCache loginTokenCache = ((EntryStoreApplication)getApplication()).getLoginTokenCache();
						loginTokenCache.removeTokensButOne(CookieVerifier.getAuthToken(getRequest()));
						Email.sendPasswordChangeConfirmation(getRM().getConfiguration(), entry);
						return;
					} else {
						getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
						getResponse().setEntity(new JsonRepresentation("{\"error\":\"Password must conform to configured rules.\"}"));
						return;
					}
				}
				if (entityJSON.has("language")) {
					String prefLang = entityJSON.getString("language");
					if (prefLang.isEmpty()) {
						resourceUser.setLanguage(null);
					} else if (!resourceUser.setLanguage(prefLang)) {
						getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
						getResponse().setEntity(new JsonRepresentation("{\"error\":\"Preferred language could not be set.\"}"));
						return;
					}
				}
				if (entityJSON.has("homecontext")) {
					String homeContext = entityJSON.getString("homecontext");
					Entry entryHomeContext = getCM().get(homeContext);
					if (entryHomeContext != null) {
						if (!(entryHomeContext.getResource() instanceof Context)
								|| !resourceUser.setHomeContext((Context) entryHomeContext.getResource())) {
							getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
							getResponse().setEntity(new JsonRepresentation("{\"error\":\"Given homecontext is not a context.\"}"));
							return;
						}
					}
				}
				if (entityJSON.has("disabled")) {
					if (this.entry.getResourceURI().equals(getPM().getAuthenticatedUserURI())) {
						getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
						getResponse().setEntity(new JsonRepresentation("{\"error\":\"Users cannot set their own disabled status\"}"));
						return;
					}
					boolean disabled = entityJSON.optBoolean("disabled", false);
					resourceUser.setDisabled(disabled);
					if (disabled) {
						String userName = getPM().getPrincipalName(this.entry.getResourceURI());
						LoginTokenCache loginTokenCache = ((EntryStoreApplication)getApplication()).getLoginTokenCache();
						loginTokenCache.removeTokens(userName);
					}
				}
				if (entityJSON.has("customProperties")) {
					Map<String, String> customPropMap = new HashMap<>();
					JSONObject customPropJson = entityJSON.getJSONObject("customProperties");
					for (Iterator<String> cPIt = customPropJson.keys(); cPIt.hasNext();) {
						String key = cPIt.next();
						customPropMap.put(key, customPropJson.getString(key));
					}
					resourceUser.setCustomProperties(customPropMap);
				}
				getResponse().setStatus(Status.SUCCESS_OK);
			} catch (JSONException e) {
				log.debug("Wrong JSON syntax: {}", e.getMessage());
				getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
				getResponse().setEntity(new JsonRepresentation(JSONErrorMessages.errorJSONSyntax));
			} catch (IOException e) {
				log.error(e.getMessage());
				getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
				getResponse().setEntity(new JsonRepresentation("{\"error\":\"IOException\"}"));
			}
		}
	}

}