/* * Licensed to DuraSpace under one or more contributor license agreements. * See the NOTICE file distributed with this work for additional information * regarding copyright ownership. * * DuraSpace licenses this file to you 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.fcrepo.http.commons.api.rdf; import static java.util.Collections.singleton; import static com.google.common.collect.ImmutableList.of; import static org.apache.commons.lang3.StringUtils.EMPTY; import static org.apache.commons.lang3.StringUtils.replaceOnce; import static org.apache.jena.rdf.model.ResourceFactory.createResource; import static org.fcrepo.kernel.api.FedoraTypes.FCR_METADATA; import static org.fcrepo.kernel.api.FedoraTypes.FCR_VERSIONS; import static org.fcrepo.kernel.modeshape.FedoraSessionImpl.getJcrSession; import static org.fcrepo.kernel.modeshape.identifiers.NodeResourceConverter.nodeConverter; import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.getClosestExistingAncestor; import static org.fcrepo.kernel.modeshape.utils.NamespaceTools.validatePath; import static org.slf4j.LoggerFactory.getLogger; import static org.springframework.web.context.ContextLoader.getCurrentWebApplicationContext; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.jcr.ItemNotFoundException; import javax.jcr.Node; import javax.jcr.PathNotFoundException; import javax.jcr.Property; import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.jcr.version.VersionHistory; import javax.ws.rs.core.UriBuilder; import org.fcrepo.http.commons.session.HttpSession; import org.fcrepo.kernel.api.FedoraSession; import org.fcrepo.kernel.api.exception.IdentifierConversionException; import org.fcrepo.kernel.api.exception.InvalidResourceIdentifierException; import org.fcrepo.kernel.api.exception.RepositoryRuntimeException; import org.fcrepo.kernel.api.exception.TombstoneException; import org.fcrepo.kernel.api.identifiers.IdentifierConverter; import org.fcrepo.kernel.api.models.NonRdfSourceDescription; import org.fcrepo.kernel.api.models.FedoraResource; import org.fcrepo.kernel.modeshape.TombstoneImpl; import org.fcrepo.kernel.modeshape.identifiers.HashConverter; import org.fcrepo.kernel.modeshape.identifiers.NamespaceConverter; import org.glassfish.jersey.uri.UriTemplate; import org.slf4j.Logger; import org.springframework.context.ApplicationContext; import com.google.common.base.Converter; import com.google.common.collect.Lists; import org.apache.jena.rdf.model.Resource; /** * Convert between Jena Resources and JCR Nodes using a JAX-RS UriBuilder to mediate the * URI translation. * * @author cabeer * @since 10/5/14 */ public class HttpResourceConverter extends IdentifierConverter<Resource,FedoraResource> { private static final Logger LOGGER = getLogger(HttpResourceConverter.class); protected List<Converter<String, String>> translationChain; private final FedoraSession session; private final UriBuilder uriBuilder; protected Converter<String, String> forward = identity(); protected Converter<String, String> reverse = identity(); private final UriTemplate uriTemplate; private final boolean batch; /** * Create a new identifier converter within the given session with the given URI template * @param session the session * @param uriBuilder the uri builder */ public HttpResourceConverter(final HttpSession session, final UriBuilder uriBuilder) { this.session = session.getFedoraSession(); this.uriBuilder = uriBuilder; this.batch = session.isBatchSession(); this.uriTemplate = new UriTemplate(uriBuilder.toTemplate()); resetTranslationChain(); } private UriBuilder uriBuilder() { return UriBuilder.fromUri(uriBuilder.toTemplate()); } @Override protected FedoraResource doForward(final Resource resource) { final Map<String, String> values = new HashMap<>(); final String path = asString(resource, values); final Session jcrSession = getJcrSession(session); try { if (path != null) { final Node node = getNode(path); final boolean metadata = values.containsKey("path") && values.get("path").endsWith("/" + FCR_METADATA); final FedoraResource fedoraResource = nodeConverter.convert(node); if (!metadata && fedoraResource instanceof NonRdfSourceDescription) { return fedoraResource.getDescribedResource(); } return fedoraResource; } throw new IdentifierConversionException("Asked to translate a resource " + resource + " that doesn't match the URI template"); } catch (final RepositoryException e) { validatePath(jcrSession, path); if ( e instanceof PathNotFoundException ) { try { final Node preexistingNode = getClosestExistingAncestor(jcrSession, path); if (TombstoneImpl.hasMixin(preexistingNode)) { throw new TombstoneException(new TombstoneImpl(preexistingNode)); } } catch (final RepositoryException inner) { LOGGER.debug("Error checking for parent tombstones", inner); } } throw new RepositoryRuntimeException(e); } } @Override protected Resource doBackward(final FedoraResource resource) { return toDomain(doBackwardPathOnly(resource)); } @Override public boolean inDomain(final Resource resource) { final Map<String, String> values = new HashMap<>(); return uriTemplate.match(resource.getURI(), values) && values.containsKey("path") || isRootWithoutTrailingSlash(resource); } @Override public Resource toDomain(final String path) { final String realPath; if (path == null) { realPath = ""; } else if (path.startsWith("/")) { realPath = path.substring(1); } else { realPath = path; } final UriBuilder uri = uriBuilder(); if (realPath.contains("#")) { final String[] split = realPath.split("#", 2); uri.resolveTemplate("path", split[0], false); uri.fragment(split[1]); } else { uri.resolveTemplate("path", realPath, false); } return createResource(uri.build().toString()); } @Override public String asString(final Resource resource) { final Map<String, String> values = new HashMap<>(); return asString(resource, values); } /** * Convert the incoming Resource to a JCR path (but don't attempt to load the node). * * @param resource Jena Resource to convert * @param values a map that will receive the matching URI template variables for future use. * @return */ private String asString(final Resource resource, final Map<String, String> values) { if (uriTemplate.match(resource.getURI(), values) && values.containsKey("path")) { String path = "/" + values.get("path"); final boolean metadata = path.endsWith("/" + FCR_METADATA); if (metadata) { path = replaceOnce(path, "/" + FCR_METADATA, EMPTY); } path = forward.convert(path); if (path == null) { return null; } try { path = URLDecoder.decode(path, "UTF-8"); } catch (final UnsupportedEncodingException e) { LOGGER.debug("Unable to URL-decode path " + e + " as UTF-8", e); } if (path.isEmpty()) { return "/"; } // Validate path if (path.contains("//")) { throw new InvalidResourceIdentifierException("Path contains empty element! " + path); } return path; } if (isRootWithoutTrailingSlash(resource)) { return "/"; } return null; } private Node getNode(final String path) throws RepositoryException { if (path.contains(FCR_VERSIONS)) { final String[] split = path.split("/" + FCR_VERSIONS + "/", 2); final String versionedPath = split[0]; final String versionAndPathIntoVersioned = split[1]; final String[] split1 = versionAndPathIntoVersioned.split("/", 2); final String version = split1[0]; final String pathIntoVersioned; if (split1.length > 1) { pathIntoVersioned = split1[1]; } else { pathIntoVersioned = ""; } final Node node = getFrozenNodeByLabel(versionedPath, version); if (pathIntoVersioned.isEmpty()) { return node; } else if (node != null) { return node.getNode(pathIntoVersioned); } else { throw new PathNotFoundException("Unable to find versioned resource at " + path); } } try { return getJcrSession(session).getNode(path); } catch (IllegalArgumentException ex) { throw new InvalidResourceIdentifierException("Illegal path: " + path); } } /** * A private helper method that tries to look up frozen node for the given subject * by a label. That label may either be one that was assigned at creation time * (and is a version label in the JCR sense) or a system assigned identifier that * was used for versions created without a label. The current implementation * uses the JCR UUID for the frozen node as the system-assigned label. */ private Node getFrozenNodeByLabel(final String baseResourcePath, final String label) { try { final Node n = getNode(baseResourcePath, label); if (n != null) { return n; } /* * Though a node with an id of the label was found, it wasn't the * node we were looking for, so fall through and look for a labeled * node. */ final VersionHistory hist = getJcrSession(session).getWorkspace().getVersionManager().getVersionHistory(baseResourcePath); if (hist.hasVersionLabel(label)) { LOGGER.debug("Found version for {} by label {}.", baseResourcePath, label); return hist.getVersionByLabel(label).getFrozenNode(); } LOGGER.warn("Unknown version {} with label or uuid {}!", baseResourcePath, label); throw new PathNotFoundException("Unknown version " + baseResourcePath + " with label or uuid " + label); } catch (final RepositoryException e) { throw new RepositoryRuntimeException(e); } } private Node getNode(final String baseResourcePath, final String label) throws RepositoryException { try { final Node frozenNode = getJcrSession(session).getNodeByIdentifier(label); /* * We found a node whose identifier is the "label" for the version. Now * we must do due diligence to make sure it's a frozen node representing * a version of the subject node. */ final Property p = frozenNode.getProperty("jcr:frozenUuid"); if (p != null) { final Node subjectNode = getJcrSession(session).getNode(baseResourcePath); if (p.getString().equals(subjectNode.getIdentifier())) { return frozenNode; } } } catch (final ItemNotFoundException ex) { /* * the label wasn't a uuid of a frozen node but * instead possibly a version label. */ } return null; } private static String getPath(final FedoraResource resource) { if (resource.isFrozenResource()) { // the versioned resource we're in final FedoraResource versionableFrozenResource = resource.getVersionedAncestor(); // the unfrozen equivalent for the versioned resource final FedoraResource unfrozenVersionableResource = versionableFrozenResource.getUnfrozenResource(); // the label for this version final String versionLabel = versionableFrozenResource.getVersionLabelOfFrozenResource(); // the path to this resource within the versioning tree final String pathWithinVersionable; if (!resource.equals(versionableFrozenResource)) { pathWithinVersionable = getRelativePath(resource, versionableFrozenResource); } else { pathWithinVersionable = ""; } // and, finally, the path we want to expose in the URI final String path = unfrozenVersionableResource.getPath() + "/" + FCR_VERSIONS + (versionLabel != null ? "/" + versionLabel : "") + pathWithinVersionable; return path.startsWith("/") ? path : "/" + path; } return resource.getPath(); } private static String getRelativePath(final FedoraResource child, final FedoraResource ancestor) { return child.getPath().substring(ancestor.getPath().length()); } /** * Get only the resource path to this resource, before embedding it in a full URI * @param resource * @return */ private String doBackwardPathOnly(final FedoraResource resource) { final String path = reverse.convert(getPath(resource)); if (path != null) { if (resource instanceof NonRdfSourceDescription) { return path + "/" + FCR_METADATA; } return path; } throw new RepositoryRuntimeException("Unable to process reverse chain for resource " + resource); } protected void resetTranslationChain() { if (translationChain == null) { translationChain = getTranslationChain(); final List<Converter<String, String>> newChain = new ArrayList<>(singleton(new TransactionIdentifierConverter(session, batch))); newChain.addAll(translationChain); setTranslationChain(newChain); } } private void setTranslationChain(final List<Converter<String, String>> chained) { translationChain = chained; for (final Converter<String, String> t : translationChain) { forward = forward.andThen(t); } for (final Converter<String, String> t : Lists.reverse(translationChain)) { reverse = reverse.andThen(t.reverse()); } } private static final List<Converter<String, String>> minimalTranslationChain = of(new NamespaceConverter(), new HashConverter()); protected List<Converter<String,String>> getTranslationChain() { final ApplicationContext context = getApplicationContext(); if (context != null) { @SuppressWarnings("unchecked") final List<Converter<String,String>> tchain = getApplicationContext().getBean("translationChain", List.class); return tchain; } return minimalTranslationChain; } protected ApplicationContext getApplicationContext() { return getCurrentWebApplicationContext(); } /** * Translate the current transaction into the identifier */ static class TransactionIdentifierConverter extends Converter<String, String> { public static final String TX_PREFIX = "tx:"; private final FedoraSession session; private final boolean batch; public TransactionIdentifierConverter(final FedoraSession session, final boolean batch) { this.session = session; this.batch = batch; } @Override protected String doForward(final String path) { if (path.contains(TX_PREFIX) && !path.contains(txSegment())) { throw new RepositoryRuntimeException("Path " + path + " is not in current transaction " + session.getId()); } return replaceOnce(path, txSegment(), EMPTY); } @Override protected String doBackward(final String path) { return txSegment() + path; } private String txSegment() { return batch ? "/" + TX_PREFIX + session.getId() : EMPTY; } } private boolean isRootWithoutTrailingSlash(final Resource resource) { final Map<String, String> values = new HashMap<>(); return uriTemplate.match(resource.getURI() + "/", values) && values.containsKey("path") && values.get("path").isEmpty(); } }