EntryService.java

/*
 * Copyright (c) 2007-2026 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.springboot.service;

import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.solr.common.SolrDocument;
import org.eclipse.rdf4j.model.IRI;
import org.eclipse.rdf4j.model.Model;
import org.eclipse.rdf4j.model.Statement;
import org.eclipse.rdf4j.model.Value;
import org.eclipse.rdf4j.model.ValueFactory;
import org.eclipse.rdf4j.model.impl.LinkedHashModel;
import org.eclipse.rdf4j.rio.RDFParseException;
import org.entrystore.AuthorizationException;
import org.entrystore.Context;
import org.entrystore.ContextManager;
import org.entrystore.Entry;
import org.entrystore.EntryType;
import org.entrystore.GraphType;
import org.entrystore.Group;
import org.entrystore.List;
import org.entrystore.Metadata;
import org.entrystore.PrincipalManager;
import org.entrystore.Resource;
import org.entrystore.ResourceType;
import org.entrystore.User;
import org.entrystore.exception.EntryMissingException;
import org.entrystore.exception.InvalidExternalMetadataURIException;
import org.entrystore.impl.ContextImpl;
import org.entrystore.impl.EntryImpl;
import org.entrystore.impl.RDFResource;
import org.entrystore.impl.RepositoryManagerImpl;
import org.entrystore.impl.RepositoryProperties;
import org.entrystore.impl.StringResource;
import org.entrystore.repository.RepositoryException;
import org.entrystore.repository.util.ModelUtil;
import org.entrystore.repository.util.NS;
import org.entrystore.repository.util.SolrSearchIndex;
import org.entrystore.repository.util.URISplit;
import org.entrystore.rest.springboot.model.api.CreateEntryRequestBody;
import org.entrystore.rest.springboot.model.api.EntryResourceJson;
import org.entrystore.rest.springboot.model.api.GetEntryResponse;
import org.entrystore.rest.springboot.model.api.ListFilter;
import org.entrystore.rest.springboot.model.dto.ListParams;
import org.entrystore.rest.springboot.model.exception.BadRequestException;
import org.entrystore.rest.springboot.model.exception.DataConflictException;
import org.entrystore.rest.springboot.model.exception.EntityNotFoundException;
import org.entrystore.rest.springboot.model.exception.InternalServerErrorException;
import org.entrystore.rest.springboot.util.GraphUtil;
import org.entrystore.rest.springboot.util.RDFJSON;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.springframework.stereotype.Service;
import tools.jackson.core.JacksonException;

import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
import java.util.regex.Pattern;

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.springboot.service.ResourceSerializationService.IMMUTABLE_EMPTY_JSONOBJECT;

@Slf4j
@Service
@RequiredArgsConstructor
public class EntryService {

	private static final int JSON_OBJECT_TO_STRING_INDENT_SIZE = 0;
	private static final Pattern ENTRY_ID_PATTERN = Pattern.compile("^[\\w\\-]+$");
	private static final String NEW_ID_STAND_IN = "10";

	private final PrincipalManager principalManager;
	private final RepositoryManagerImpl repositoryManager;
	private final ContextService contextService;
	private final ReservedNamesService reservedNamesService;
	private final ResourceSerializationService resourceSerializationService;

	private final ObjectMapper objectMapper;

	/**
	 * Checks whether the provided ID contains only allowed characters.
	 *
	 * @return True if supplied ID is valid.
	 */
	public static boolean isEntryIdValid(String id) {
		return ENTRY_ID_PATTERN.matcher(id).matches();
	}

	/**
	 * @param mayReadNothing whether the caller {@linkplain #mayReadNothing may read nothing} of the entry, which
	 *                       reduces the entry information
	 */
	public String getEntryInRdfFormat(Entry entry, String mediaType, boolean mayReadNothing) {
		return GraphUtil.serializeGraph(resourceSerializationService.getEntryInfo(entry, mayReadNothing), mediaType);
	}

	/**
	 * Whether the current user may read neither the metadata nor the resource of the entry, so that it gets the
	 * reduced entry information, see {@link ResourceSerializationService#mayReadNothing(Entry)}.
	 */
	public boolean mayReadNothing(Entry entry) {
		return resourceSerializationService.mayReadNothing(entry);
	}

	public Entry getEntryByContextIdAndEntryId(String contextId, String entryId) {
		Context context = contextService.getContext(contextId);
		if (context == null) {
			// throw the same exception message for missing Context and missing Entry to avoid leaking information about context existence
			throw new EntityNotFoundException("No entry with id '" + entryId + "' found in context '" + contextId + "'");
		}

		Entry entry = context.get(entryId);
		if (entry == null) {
			throw new EntityNotFoundException("No entry with id '" + entryId + "' found in context '" + contextId + "'");
		}

		return entry;
	}

	/**
	 * Validates if the currently authenticated user has the specified access property to the given entry.
	 *
	 * @param entry          The entry object for which access needs to be checked.
	 * @param accessProperty The access property defining the type of access to be validated.
	 * @throws AuthorizationException If the user does not have the required access permissions.
	 */
	public void checkEntryUserAccess(Entry entry, PrincipalManager.AccessProperty accessProperty) throws AuthorizationException {
		principalManager.checkAuthenticatedUserAuthorized(entry, accessProperty);
	}

	/**
	 * @param mayReadNothing whether the caller {@linkplain #mayReadNothing may read nothing} of the entry, which
	 *                       reduces the entry information and omits a context's quota
	 */
	public GetEntryResponse getEntryInJsonFormat(Entry entry, String rdfFormat, boolean includeAll,
												 ListFilter listFilter, boolean mayReadNothing) {

		ContextManager cm = repositoryManager.getContextManager();

		var responseBuilder = GetEntryResponse.builder();

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

		/*
		 * Entry id
		 */
		responseBuilder.entryId(entry.getId());

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

		/*
		 * Entry information
		 */
		Model entryGraph = resourceSerializationService.getEntryInfo(entry, mayReadNothing);
		JSONObject entryObj = GraphUtil.serializeGraphToJson(entryGraph, rdfFormat);
		responseBuilder.info(entryObj.toString(JSON_OBJECT_TO_STRING_INDENT_SIZE));

		/*
		 * 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 (!includeAll) {
			return responseBuilder.build();
		}

		/*
		 * 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);
					responseBuilder.cachedExternalMetadata(cachedExternalMetadataJsonObject.toString(JSON_OBJECT_TO_STRING_INDENT_SIZE));
				}
			} 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);
					responseBuilder.metadata(localMetaDataJsonObject.toString(JSON_OBJECT_TO_STRING_INDENT_SIZE));
				}
			} 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
		 */
		Model relations = entry.getRelations();
		if (relations != null) {
			JSONObject relationsJsonObject = GraphUtil.serializeGraphToJson(relations, rdfFormat);
			responseBuilder.relations(relationsJsonObject.toString(JSON_OBJECT_TO_STRING_INDENT_SIZE));
		}

		/*
		 * Rights
		 */
		JSONArray rights = resourceSerializationService.serializeRights(entry);
		responseBuilder.rights(rights.toString(JSON_OBJECT_TO_STRING_INDENT_SIZE));

		/*
		 * Local resource
		 */
		if (entryType == Local) {
			Resource resource = entry.getResource();
			if (resource == null) {
				log.error("Resource is null for Context '{}', EntryId '{}', GraphType '{}', skipping resource serialization",
						entry.getContext().getURI(), entry.getId(), graphType);
			} else {
				try {
					String resourceString = serializeResourceToRawJsonString(resource, graphType, rdfFormat, listFilter);
					if (resourceString != null) {
						responseBuilder.resource(graphType == GraphType.String
								? new EntryResourceJson.Text(resourceString)
								: new EntryResourceJson.Json(resourceString));
					}
				} catch (IllegalArgumentException | RepositoryException | JSONException e) {
					log.error("Failed to serialize resource for Context '{}', EntryId '{}', GraphType '{}'. Error: {}",
							entry.getContext().getURI(), entry.getId(), graphType, e.getMessage(), e);
				}
			}
		}
		return responseBuilder.build();
	}

	private String serializeResourceToRawJsonString(Resource resource, GraphType graphType, String rdfFormat, ListFilter listFilter) {
		if (graphType == GraphType.String) {
			return resourceSerializationService.serializeResourceString(resource);
		}
		JSONObject jsonObject = serializeResourceToJson(resource, graphType, rdfFormat, listFilter);

		return jsonObject != null ?
				jsonObject.toString(JSON_OBJECT_TO_STRING_INDENT_SIZE)
				: null;
	}

	// TODO: move this method to ResourceSerializer class?
	private JSONObject serializeResourceToJson(Resource resource, GraphType graphType, String rdfFormat, ListFilter listFilter) {
		if (graphType == null) {
			return IMMUTABLE_EMPTY_JSONOBJECT;
		}
		return switch (graphType) {
			case List -> resourceSerializationService.serializeResourceList(resource,
					new ListParams(listFilter), rdfFormat);
			case User -> resourceSerializationService.serializeResourceUser(resource);
			case Group -> resourceSerializationService.serializeResourceGroup(resource, rdfFormat);
			case None -> resourceSerializationService.serializeResourceNone(resource);
			case Graph -> resourceSerializationService.serializeResourceGraph(resource, rdfFormat);
			case Pipeline -> resourceSerializationService.serializeResourcePipeline(resource, rdfFormat);
			case String -> null;
			// TODO: other types, for example PrincipalManager, etc
			// Not the context's entry IDs: listing them here would skip every ACL check, and 5.x returned {}.
			case ResultList, PipelineResult, Context, SystemContext -> IMMUTABLE_EMPTY_JSONOBJECT;
		};
	}

	public Entry createEntry(Context context, String entryId, EntryType entryType, GraphType graphType,
							 URI resourceUri, URI listUri, URI groupUri, URI cachedExternalMetadataUri,
							 String informationResource, URI templateUri, CreateEntryRequestBody body) {

		if (entryId != null) {
			if (!EntryService.isEntryIdValid(entryId)) {
				throw new BadRequestException("Invalid entry ID of '" + entryId + "'");
			}
			Entry preExistingEntry = context.get(entryId);
			if (preExistingEntry != null) {
				throw new DataConflictException("Entry with provided ID already exists. EntryID: '" + entryId + "'");
			}
		}

		checkGraphsOfRequestBody(entryId, entryType, body);

		Entry entry = null; // A variable to store the new entry in.

		try {
			checkExternalMetadataURIOfEntryGraph(context, entryId, entryType, body);
			// Local
			if (entryType == null || entryType == EntryType.Local) {
				entry = createLocalEntry(context, entryId, graphType, listUri, groupUri, body);
			} else {
				// Link
				if (entryType == EntryType.Link && resourceUri != null) {
					entry = createLinkEntry(context, entryId, graphType, resourceUri, listUri, body);
				}
				// Reference
				else if (entryType == EntryType.Reference
						&& resourceUri != null
						&& cachedExternalMetadataUri != null) {

					entry = createReferenceEntry(context, entryId, graphType, resourceUri, listUri, cachedExternalMetadataUri, body);
				}
				// LinkReference
				else if (entryType == EntryType.LinkReference
						&& resourceUri != null
						&& cachedExternalMetadataUri != null) {

					entry = createLinkReferenceEntry(context, entryId, graphType, resourceUri, listUri, cachedExternalMetadataUri, body);
				}
			}
		} catch (IllegalArgumentException iae) {
			throw new BadRequestException(iae.getMessage(), iae); // Core exception — message is safe to return
		}

		if (entry != null) {
			ResourceType rt = mapToResourceType(informationResource);
			entry.setResourceType(rt);

			if (templateUri != null) {
				Entry templateEntry = context.getByEntryURI(templateUri);
				if (templateEntry != null && templateEntry.getLocalMetadata() != null) {
					Model templateMD = templateEntry.getLocalMetadata().getGraph();
					Model inheritedMD = new LinkedHashModel();
					if (templateMD != null) {
						ValueFactory vf = repositoryManager.getValueFactory();
						IRI oldResURI = vf.createIRI(templateEntry.getResourceURI().toString());
						IRI newResURI = vf.createIRI(entry.getResourceURI().toString());

						java.util.List<IRI> predicateBlackList = new ArrayList<>();
						predicateBlackList.add(vf.createIRI(NS.dc, "title"));
						predicateBlackList.add(vf.createIRI(NS.dcterms, "title"));
						predicateBlackList.add(vf.createIRI(NS.dc, "description"));
						predicateBlackList.add(vf.createIRI(NS.dcterms, "description"));
						java.util.List<Value> subjectBlackList = new ArrayList<>();

						for (Statement statement : templateMD) {
							if (predicateBlackList.contains(statement.getPredicate())) {
								subjectBlackList.add(statement.getObject());
								continue;
							}
							if (!subjectBlackList.contains(statement.getSubject())) {
								inheritedMD.add(statement);
							}
						}
						inheritedMD = ModelUtil.replaceSubject(inheritedMD, oldResURI, newResURI);
					}
					if (!inheritedMD.isEmpty() && entry.getLocalMetadata() != null) {
						Model mergedGraph = new LinkedHashModel();
						mergedGraph.addAll(entry.getLocalMetadata().getGraph());
						mergedGraph.addAll(inheritedMD);
						entry.getLocalMetadata().setGraph(mergedGraph);
					}
				}
			}
		}

		if (entry == null) {
			throw new BadRequestException("Cannot create an entry with provided JSON");
		} else {
			return entry;
		}
	}

	private ResourceType mapToResourceType(String rt) {
		if (rt == null || !rt.equals("false")) {
			return ResourceType.InformationResource;
		} else {
			return ResourceType.NamedResource;
		}
	}


	/**
	 * Creates a local entry.
	 *
	 * <p><b>Error-propagation contract:</b>
	 * <ul>
	 *   <li>JSON/RDF parse failures of the user-supplied body are converted to {@link BadRequestException} (400)
	 *       via {@code safeRollback} (logs the cleanup failure if it fires, attaches it as a suppressed exception).
	 *   <li>Any other {@code RuntimeException} from a post-skeleton step (RDF4J {@code RepositoryException},
	 *       ACL-copy failure, etc.) is rethrown unchanged so {@code AppExceptionHandler.handleGenericException}
	 *       sanitises it to a generic 500.
	 *   <li>If {@code setResource} returns {@code false} (validation rejected the body) the just-created
	 *       skeleton is removed via {@code removeOrphan}, which throws {@link InternalServerErrorException}
	 *       (500) if cleanup fails — so a partial store-inconsistency cannot be masked as the caller's 400.
	 * </ul>
	 * <p>Callers <b>must not</b> wrap-and-rethrow propagated {@code RuntimeException}s as
	 * {@code BadRequestException(e.getMessage(), e)} (or any handler that echoes the message) — the
	 * original messages may carry RDF4J/Jackson internals (file paths, store details) that must not
	 * reach the client.
	 *
	 * @return the new created entry
	 */
	public Entry createLocalEntry(Context context, String entryId, GraphType graphType,
								  URI listUri, URI groupUri, CreateEntryRequestBody body) {

		Entry entry = context.createResource(entryId, graphType, null, listUri);
		try {
			if (setResource(context, entry, body, groupUri)) {
				setLocalMetadataGraph(entry, body);
				setEntryGraph(entry, body);
				if (listUri != null) {
					((ContextImpl) context).copyACL(listUri, entry);
				}
				return entry;
			}
		} catch (JacksonException | RDFParseException e) {
			// Both fire on user-supplied content (JSON envelope or inner RDFJSON in `info`/`metadata`)
			// — client-side input failure, mapped to 400. The cause chain preserves the underlying
			// parser detail for server-side debugging without echoing it to the client.
			safeRollback(context, entry, e);
			throw new BadRequestException("Cannot create an entry with provided JSON/RDF", e);
		} catch (RuntimeException e) {
			safeRollback(context, entry, e);
			throw e;
		}
		// setResource returned false (validation failure) — clean up the just-created skeleton. This
		// path is outside the try/catch above so that a cleanup failure here (rethrown by removeOrphan)
		// does NOT then trigger safeRollback to call context.remove a second time on the same entry.
		removeOrphan(context, entry);
		return null;
	}

	private void safeRollback(Context context, Entry entry, Throwable original) {
		try {
			context.remove(entry.getEntryURI());
		} catch (RuntimeException cleanupEx) {
			log.error("Failed to remove orphan entry {} after create failure", entry.getEntryURI(), cleanupEx);
			original.addSuppressed(cleanupEx);
		}
	}

	private void removeOrphan(Context context, Entry entry) {
		try {
			context.remove(entry.getEntryURI());
		} catch (RuntimeException cleanupEx) {
			log.error("Failed to remove orphan entry {}", entry.getEntryURI(), cleanupEx);
			throw new InternalServerErrorException("Orphan cleanup failed for entry " + entry.getEntryURI(), cleanupEx);
		}
	}

	/**
	 * Creates a link entry.
	 *
	 * @return the new created entry
	 */
	public Entry createLinkEntry(Context context, String entryId, GraphType graphType, URI resourceUri, URI listUri, CreateEntryRequestBody body) {

		Entry entry = context.createLink(entryId, resourceUri, listUri);
		return finishEntry(context, entry, graphType, listUri, body);
	}

	/**
	 * Creates a Reference entry.
	 *
	 * @return the new created entry
	 */
	public Entry createReferenceEntry(Context context, String entryId, GraphType graphType, URI resourceUri,
									  URI listUri, URI cachedExternalMetadataUri, CreateEntryRequestBody body) {

		if (resourceUri != null &&
				cachedExternalMetadataUri != null) {

			Entry entry = context.createReference(entryId, resourceUri, cachedExternalMetadataUri, listUri);
			return finishEntry(context, entry, graphType, listUri, body);
		}

		return null;
	}


	/**
	 * Creates a LinkReference entry.
	 *
	 * @return the new created entry object.
	 */
	public Entry createLinkReferenceEntry(Context context, String entryId, GraphType graphType, URI resourceUri,
										  URI listUri, URI cachedExternalMetadataUri, CreateEntryRequestBody body) {

		if (resourceUri != null) {

			Entry entry = context.createLinkReference(entryId, resourceUri, cachedExternalMetadataUri, listUri);
			return finishEntry(context, entry, graphType, listUri, body);
		}

		return null;
	}

	/**
	 * Shared tail of the link/reference/link-reference create pipelines: applies the request body's
	 * graphs (each setter no-ops for entry types it does not apply to), the optional graph type, and
	 * copies the parent list's ACL. A failure removes the just-created entry, with the error-propagation
	 * contract of {@link #createLocalEntry}: invalid RDF/JSON that {@link #checkGraphsOfRequestBody} could not
	 * detect before creation becomes a {@link BadRequestException}.
	 */
	private Entry finishEntry(Context context, Entry entry, GraphType graphType, URI listUri, CreateEntryRequestBody body) {
		if (entry == null) {
			return null;
		}
		try {
			setLocalMetadataGraph(entry, body);
			setCachedMetadataGraph(entry, body);
			setEntryGraph(entry, body);
			if (graphType != null) {
				entry.setGraphType(graphType);
			}
			if (listUri != null) {
				((ContextImpl) context).copyACL(listUri, entry);
			}
			return entry;
		} catch (RDFParseException e) {
			safeRollback(context, entry, e);
			throw new BadRequestException("Cannot create an entry with provided JSON/RDF", e);
		} catch (RuntimeException e) {
			safeRollback(context, entry, e);
			throw e;
		}
	}

	public Entry modifyEntry(Entry entry, String body, String mediaType, boolean applyACLtoChildren) throws AuthorizationException {

		Model deserializedGraph = GraphUtil.deserializeGraph(body, mediaType);
		try {
			entry.setGraph(deserializedGraph);
		} catch (IllegalArgumentException iae) {
			throw new BadRequestException(iae.getMessage()); // core validation message; no cause, it is expected client input
		}
		if (applyACLtoChildren &&
				GraphType.List.equals(entry.getGraphType()) &&
				Local.equals(entry.getEntryType())) {
			((org.entrystore.List) entry.getResource()).applyACLtoChildren(true);
		}
		return entry;
	}

	public void deleteEntry(Entry entry, boolean recursive) {
		try {
			if (GraphType.List.equals(entry.getGraphType()) && 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 {
				entry.getContext().remove(entry.getEntryURI());
			}
		} catch (EntryMissingException e) {
			throw new EntityNotFoundException("Entry requested for deletion was not found", e);
		}
	}

	public String getEntryName(Entry entry) {
		String name = null;
		GraphType bt = entry.getGraphType();
		if (GraphType.Group.equals(bt)) {
			name = ((Group) entry.getResource()).getName();
		} else if (GraphType.User.equals(bt)) {
			name = ((User) entry.getResource()).getName();
		} else if (GraphType.Context.equals(bt)) {
			ContextManager cm = repositoryManager.getContextManager();
			Context c = cm.getContext(entry.getId());
			name = cm.getName(c.getURI());
		}
		return name;
	}

	public boolean setEntryName(Entry entry, String requestName) {

		String newName;
		if (StringUtils.isEmpty(requestName)) {
			newName = null;
		} else {
			newName = requestName.trim();
		}

		GraphType bt = entry.getGraphType();
		boolean success = false;

		if (GraphType.Group.equals(bt)) {
			success = ((Group) entry.getResource()).setName(newName);

		} else if (GraphType.User.equals(bt)) {
			// Users must always have a name
			if (newName == null) {
				throw new BadRequestException("User must have a name.");
			}
			success = ((User) entry.getResource()).setName(newName);

		} else if (GraphType.Context.equals(bt)) {
			if (reservedNamesService.isReservedName(StringUtils.trimToEmpty(newName).toLowerCase())) {
				throw new BadRequestException("Requested name to be set of '" + newName + "' is a reserved word.");
			} else {
				ContextManager cm = repositoryManager.getContextManager();
				Context c = cm.getContext(entry.getId());
				success = cm.setName(c.getURI(), newName);
			}
		}

		return success;
	}

	public Map<String, Object> getEntryIndex(Entry entry) {
		SolrDocument doc = ((SolrSearchIndex) repositoryManager.getIndex()).fetchDocument(entry.getEntryURI().toString());
		if (doc == null) {
			throw new EntityNotFoundException("Entry Index data not found.");
		}

		Map<String, Object> result = new HashMap<>();
		for (String field : doc.getFieldValuesMap().keySet()) {
			Collection<Object> values = doc.getFieldValues(field);
			if (values.size() > 1) {
				result.put(field, values);
			} else if (values.size() == 1) {
				result.put(field, values.iterator().next());
			}
		}
		return result;
	}

	/**
	 * Sets resource to an entry.
	 *
	 * @param context     Context
	 * @param entry       Entry to set the resource on
	 * @param requestBody Input data with "resource" field to be read
	 * @param groupUri    GroupURI
	 * @return false if there is a resource provided, but it cannot be interpreted.
	 * @throws JacksonException Exception if request resource is a malformed JSON
	 */
	private boolean setResource(Context context, Entry entry, CreateEntryRequestBody requestBody, URI groupUri) throws JacksonException {

		ContextManager cm = repositoryManager.getContextManager();

		if (requestBody == null || StringUtils.isEmpty(requestBody.resource())) {
			return true;
		}

		String resource = requestBody.resource().replaceAll("_newId", entry.getId());

		switch (entry.getGraphType()) {

			case User:
				User user = (User) entry.getResource();

				Map<String, Object> resourceMap = objectMapper.readValue(resource, new TypeReference<>() {
				});

				if (resourceMap.containsKey("name")) {
					if (!user.setName(resourceMap.get("name").toString())) {
						return false;
					}
				} else {
					return false;
				}

				if (resourceMap.containsKey("homecontext")) {
					Entry homeContextEntry = cm.get(resourceMap.get("homecontext").toString());
					if (homeContextEntry != null) {
						user.setHomeContext((Context) homeContextEntry.getResource());
					}
				}

				if (groupUri != null) {
					Entry groupEntry = cm.getEntry(groupUri);
					if (groupEntry == null) {
						throw new BadRequestException("No group found with URI " + groupUri);
					}
					Group group = (Group) groupEntry.getResource();
					group.addMember(user);
				}
				break;

			case Group:
				Group group = (Group) entry.getResource();

				Map<String, Object> groupResource = objectMapper.readValue(resource, new TypeReference<>() {
				});

				if (groupResource.containsKey("name")) {
					group.setName(groupResource.get("name").toString());
				}
				break;

			case List:
				List list = (List) entry.getResource();
				java.util.List<String> listResource = objectMapper.readValue(resource, new TypeReference<>() {
				});

				if (listResource != null) {
					listResource.forEach(childUri -> {
						Entry child = context.get(childUri);
						if (child != null) {
							list.addChild(child.getEntryURI());
						}
					});
				}
				break;

			case Context:
				Context cont = (Context) entry.getResource();

				Map<String, Object> contResource = objectMapper.readValue(resource, new TypeReference<>() {
				});

				if (contResource.containsKey("name")) {
					cm.setName(cont.getURI(), contResource.get("name").toString());
				}
				if (contResource.containsKey("quota")) {
					String quotaString = contResource.get("quota").toString();
					long quota;
					try {
						quota = Long.parseLong(quotaString);
					} catch (NumberFormatException e) {
						log.warn("Unable to parse new quota value '{}': {}", quotaString, e.getMessage());
						throw new IllegalArgumentException("Unable to parse new quota value as Long: " + quotaString, e);
					}
					cont.setQuota(quota);
				}
				break;

			case String:
				StringResource stringRes = (StringResource) entry.getResource();
				stringRes.setString(resource);
				break;

			case Graph:
			case Pipeline:
				RDFResource RDFRes = (RDFResource) entry.getResource();
				Model g = RDFJSON.rdfJsonToGraph(resource);
				RDFRes.setGraph(g);
				break;
			case PipelineResult:
			case None:
				break;
		}
		return true;
	}

	/**
	 * Extracts metadata from the request body and sets it as the entry's local metadata graph.
	 *
	 * @param entry The entry to set the metadata on
	 */
	private void setLocalMetadataGraph(Entry entry, CreateEntryRequestBody requestBody) {

		if (requestBody == null || StringUtils.isEmpty(requestBody.metadata())) {
			return;
		}
		if (EntryType.Reference.equals(entry.getEntryType())) {
			return;
		}

		applyGraph(entry, requestBody.metadata(), "local metadata", graph -> entry.getLocalMetadata().setGraph(graph));
	}

	/**
	 * First caching of metadata.
	 *
	 * @param entry The entry to set the metadata on.
	 */
	private void setCachedMetadataGraph(Entry entry, CreateEntryRequestBody requestBody) {

		if (requestBody == null || StringUtils.isEmpty(requestBody.cachedExternalMetadata())) {
			return;
		}

		if (EntryType.Reference.equals(entry.getEntryType()) ||
				EntryType.LinkReference.equals(entry.getEntryType())) {
			applyGraph(entry, requestBody.cachedExternalMetadata(), "cached external metadata",
					graph -> entry.getCachedExternalMetadata().setGraph(graph));
		}
	}

	/**
	 * Rejects invalid RDF/JSON in the graphs of the request body that the entry type applies, before the entry is
	 * created, so that a rejected request takes no entry ID from the context's counter and leaves no deletion
	 * record. As in the graph setters, {@code _newId} is replaced with the given entry ID; a generated ID is not
	 * known yet, so it is replaced with the numeric stand-in "10". The generated ID can only make a difference
	 * where {@code _newId} adjoins other digits or a percent sign: a body that passes here and fails with the
	 * generated ID is removed again after creation, and one that fails here but would pass with it (for example
	 * {@code _newId} in an IPv6 literal after other digits) is rejected although no client sends such IRIs.
	 * Malformed JSON is left to the graph setters, which ignore it as in 5.x.
	 *
	 * @throws BadRequestException if a graph that the entry type applies is invalid RDF/JSON
	 */
	private void checkGraphsOfRequestBody(String entryId, EntryType entryType, CreateEntryRequestBody body) {
		if (body == null) {
			return;
		}
		String newId = entryId != null ? entryId : NEW_ID_STAND_IN;
		if (entryType != Reference) {
			checkGraph(body.metadata(), newId);
		}
		if (entryType == Reference || entryType == LinkReference) {
			checkGraph(body.cachedExternalMetadata(), newId);
		}
		checkGraph(body.info(), newId);
	}

	private void checkGraph(String rawRdfJson, String newId) {
		if (StringUtils.isEmpty(rawRdfJson)) {
			return;
		}
		try {
			RDFJSON.rdfJsonToGraph(new JSONObject(rawRdfJson.replaceAll("_newId", newId)));
		} catch (JSONException e) {
			// Ignored by the graph setters as well
		} catch (RDFParseException e) {
			throw new BadRequestException("Cannot create an entry with provided JSON/RDF", e);
		}
	}

	/**
	 * Validates the external metadata URI that the entry graph in the request body sets, before the entry is
	 * created: the entry graph is applied only after the entry has been created, and a rejected URI must not leave
	 * a created entry behind. The entry graph may refer to the new entry as {@code _newId}; as in
	 * {@link #setEntryGraph}, this is replaced with the given ID, and without one the entry URI that the URI is
	 * validated against uses {@code _newId} as well. An entry that is not a Reference or LinkReference has no
	 * external metadata URI, so its entry graph must not set one.
	 *
	 * @throws InvalidExternalMetadataURIException if the entry graph sets an external metadata URI that is not
	 *                                             acceptable, see {@link EntryImpl#checkExternalMetadataURI}, or
	 *                                             sets one for an entry of another type
	 */
	private void checkExternalMetadataURIOfEntryGraph(Context context, String entryId, EntryType entryType,
													  CreateEntryRequestBody body) {
		if (body == null || StringUtils.isEmpty(body.info())) {
			return;
		}
		Model graph;
		try {
			// The entry graph as setEntryGraph applies it
			String info = entryId != null ? body.info().replaceAll("_newId", entryId) : body.info();
			graph = RDFJSON.rdfJsonToGraph(new JSONObject(info));
		} catch (JSONException | RDFParseException e) {
			return; // Malformed JSON is ignored by setEntryGraph, invalid RDF/JSON is rejected elsewhere
		}
		if (graph == null) {
			return;
		}
		URL repositoryURL = repositoryManager.getRepositoryURL();
		URI entryURI = URISplit.createURI(repositoryURL.toString(), context.getEntry().getId(),
				RepositoryProperties.ENTRY_PATH, entryId != null ? entryId : "_newId");
		IRI entryIRI = repositoryManager.getValueFactory().createIRI(entryURI.toString());
		for (Statement statement : graph.filter(entryIRI, RepositoryProperties.externalMetadata, null)) {
			if (statement.getObject() instanceof IRI externalMetadataIRI) {
				if (entryType != Reference && entryType != LinkReference) {
					throw new InvalidExternalMetadataURIException("An entry of type "
							+ (entryType != null ? entryType : Local) + " has no external metadata URI");
				}
				EntryImpl.checkExternalMetadataURI(URI.create(externalMetadataIRI.stringValue()), entryURI,
						repositoryURL);
			}
		}
	}

	/**
	 * Extracts entry info from the request body and sets it as the entry's graph.
	 * Since it assumes this is the creation step, the Entries URIs was not available
	 * on the client, hence the special "_newId" entryId has been used.
	 * Make sure this is replaced with the new entryId first.
	 *
	 * @param entry The entry to set the metadata on.
	 */
	private void setEntryGraph(Entry entry, CreateEntryRequestBody requestBody) {

		if (requestBody == null || StringUtils.isEmpty(requestBody.info())) {
			return;
		}

		applyGraph(entry, requestBody.info(), "entry info", entry::setGraph);
	}

	/**
	 * Parses an RDF/JSON string (with the client-side "_newId" placeholder replaced by the entry's
	 * id) and hands the resulting graph to {@code target}; a malformed body is logged at INFO and
	 * ignored, matching the graph setters' log-and-continue contract.
	 */
	private void applyGraph(Entry entry, String rawRdfJson, String description, Consumer<Model> target) {
		try {
			JSONObject jsonObj = new JSONObject(rawRdfJson.replaceAll("_newId", entry.getId()));
			Model graph = RDFJSON.rdfJsonToGraph(jsonObj);
			if (graph != null) {
				target.accept(graph);
			} else {
				log.info("Could not convert {} to a graph for entry {}", description, entry.getEntryURI());
			}
		} catch (JSONException e) {
			log.info("Failed to parse {} for entry {}: {}", description, entry.getEntryURI(), e.getMessage());
		}
	}

}