001/* 002 * Licensed to DuraSpace under one or more contributor license agreements. 003 * See the NOTICE file distributed with this work for additional information 004 * regarding copyright ownership. 005 * 006 * DuraSpace licenses this file to you under the Apache License, 007 * Version 2.0 (the "License"); you may not use this file except in 008 * compliance with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018package org.fcrepo.http.commons.api.rdf; 019 020import static com.google.common.collect.ImmutableList.of; 021import static java.util.Collections.singleton; 022import static org.apache.commons.lang3.StringUtils.EMPTY; 023import static org.apache.commons.lang3.StringUtils.replaceOnce; 024import static org.apache.jena.rdf.model.ResourceFactory.createResource; 025import static org.fcrepo.kernel.modeshape.FedoraResourceImpl.CONTAINER_WEBAC_ACL; 026import static org.fcrepo.kernel.modeshape.FedoraResourceImpl.LDPCV_TIME_MAP; 027import static org.fcrepo.kernel.api.FedoraTypes.FCR_ACL; 028import static org.fcrepo.kernel.api.FedoraTypes.FCR_METADATA; 029import static org.fcrepo.kernel.api.FedoraTypes.FCR_VERSIONS; 030import static org.fcrepo.kernel.api.RdfLexicon.FEDORA_DESCRIPTION; 031import static org.fcrepo.kernel.modeshape.FedoraSessionImpl.getJcrSession; 032import static org.fcrepo.kernel.modeshape.identifiers.NodeResourceConverter.nodeConverter; 033import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.getClosestExistingAncestor; 034import static org.fcrepo.kernel.modeshape.utils.NamespaceTools.validatePath; 035import static org.slf4j.LoggerFactory.getLogger; 036import static org.springframework.web.context.ContextLoader.getCurrentWebApplicationContext; 037 038import java.io.UnsupportedEncodingException; 039import java.net.URLDecoder; 040import java.util.ArrayList; 041import java.util.HashMap; 042import java.util.List; 043import java.util.Map; 044import java.util.regex.Matcher; 045import java.util.regex.Pattern; 046 047import javax.jcr.Node; 048import javax.jcr.PathNotFoundException; 049import javax.jcr.RepositoryException; 050import javax.jcr.Session; 051import javax.ws.rs.core.UriBuilder; 052 053import org.fcrepo.http.commons.session.HttpSession; 054import org.fcrepo.kernel.api.FedoraSession; 055import org.fcrepo.kernel.api.exception.IdentifierConversionException; 056import org.fcrepo.kernel.api.exception.InvalidResourceIdentifierException; 057import org.fcrepo.kernel.api.exception.RepositoryRuntimeException; 058import org.fcrepo.kernel.api.exception.TombstoneException; 059import org.fcrepo.kernel.api.identifiers.IdentifierConverter; 060import org.fcrepo.kernel.api.models.FedoraResource; 061import org.fcrepo.kernel.api.models.NonRdfSourceDescription; 062import org.fcrepo.kernel.modeshape.TombstoneImpl; 063import org.fcrepo.kernel.modeshape.identifiers.HashConverter; 064import org.fcrepo.kernel.modeshape.identifiers.NamespaceConverter; 065 066import org.apache.jena.rdf.model.Resource; 067import org.glassfish.jersey.uri.UriTemplate; 068import org.slf4j.Logger; 069import org.springframework.context.ApplicationContext; 070 071import com.google.common.base.Converter; 072import com.google.common.collect.Lists; 073 074/** 075 * Convert between Jena Resources and JCR Nodes using a JAX-RS UriBuilder to mediate the 076 * URI translation. 077 * 078 * @author cabeer 079 * @since 10/5/14 080 */ 081public class HttpResourceConverter extends IdentifierConverter<Resource,FedoraResource> { 082 083 private static final Logger LOGGER = getLogger(HttpResourceConverter.class); 084 085 // Regex pattern which decomposes a http resource uri into components 086 // The first group determines if it is an fcr:metadata non-rdf source. 087 // The second group determines if the path is for a memento or timemap. 088 // The third group allows for a memento identifier. 089 // The fourth group for allows ACL. 090 // The fifth group allows for any hashed suffixes. 091 private final static Pattern FORWARD_COMPONENT_PATTERN = Pattern.compile( 092 ".*?(/" + FCR_METADATA + ")?(/" + FCR_VERSIONS + "(/\\d{14})?)?(/" + FCR_ACL + ")?(\\#\\w+)?$"); 093 094 protected List<Converter<String, String>> translationChain; 095 096 private final FedoraSession session; 097 private final UriBuilder uriBuilder; 098 099 protected Converter<String, String> forward = identity(); 100 protected Converter<String, String> reverse = identity(); 101 102 private final UriTemplate uriTemplate; 103 private final boolean batch; 104 105 /** 106 * Create a new identifier converter within the given session with the given URI template 107 * @param session the session 108 * @param uriBuilder the uri builder 109 */ 110 public HttpResourceConverter(final HttpSession session, 111 final UriBuilder uriBuilder) { 112 113 this.session = session.getFedoraSession(); 114 this.uriBuilder = uriBuilder; 115 this.batch = session.isBatchSession(); 116 this.uriTemplate = new UriTemplate(uriBuilder.toTemplate()); 117 118 resetTranslationChain(); 119 } 120 121 private UriBuilder uriBuilder() { 122 return UriBuilder.fromUri(uriBuilder.toTemplate()); 123 } 124 125 @Override 126 protected FedoraResource doForward(final Resource resource) { 127 final Map<String, String> values = new HashMap<>(); 128 final String path = asString(resource, values); 129 final Session jcrSession = getJcrSession(session); 130 try { 131 if (path != null) { 132 final Node node = getNode(path); 133 134 final boolean metadata = values.containsKey("path") 135 && values.get("path").contains("/" + FCR_METADATA); 136 137 final FedoraResource fedoraResource = nodeConverter.convert(node); 138 139 if (!metadata && fedoraResource instanceof NonRdfSourceDescription) { 140 return fedoraResource.getDescribedResource(); 141 } 142 return fedoraResource; 143 } 144 throw new IdentifierConversionException("Asked to translate a resource " + resource 145 + " that doesn't match the URI template"); 146 } catch (final RepositoryException e) { 147 validatePath(jcrSession, path); 148 149 if ( e instanceof PathNotFoundException ) { 150 try { 151 final Node preexistingNode = getClosestExistingAncestor(jcrSession, path); 152 if (TombstoneImpl.hasMixin(preexistingNode)) { 153 throw new TombstoneException(new TombstoneImpl(preexistingNode)); 154 } 155 } catch (final RepositoryException inner) { 156 LOGGER.debug("Error checking for parent tombstones", inner); 157 } 158 } 159 throw new RepositoryRuntimeException(e); 160 } 161 } 162 163 @Override 164 protected Resource doBackward(final FedoraResource resource) { 165 return toDomain(doBackwardPathOnly(resource)); 166 } 167 168 @Override 169 public boolean inDomain(final Resource resource) { 170 final Map<String, String> values = new HashMap<>(); 171 172 return uriTemplate.match(resource.getURI(), values) && values.containsKey("path") || 173 isRootWithoutTrailingSlash(resource); 174 } 175 176 @Override 177 public Resource toDomain(final String path) { 178 179 final String realPath; 180 if (path == null) { 181 realPath = ""; 182 } else if (path.startsWith("/")) { 183 realPath = path.substring(1); 184 } else { 185 realPath = path; 186 } 187 188 final UriBuilder uri = uriBuilder(); 189 190 if (realPath.contains("#")) { 191 192 final String[] split = realPath.split("#", 2); 193 194 uri.resolveTemplate("path", split[0], false); 195 uri.fragment(split[1]); 196 } else { 197 uri.resolveTemplate("path", realPath, false); 198 199 } 200 return createResource(uri.build().toString()); 201 } 202 203 @Override 204 public String asString(final Resource resource) { 205 final Map<String, String> values = new HashMap<>(); 206 207 return asString(resource, values); 208 } 209 210 /** 211 * Convert the incoming Resource to a JCR path (but don't attempt to load the node). 212 * 213 * @param resource Jena Resource to convert 214 * @param values a map that will receive the matching URI template variables for future use. 215 * @return String of JCR path 216 */ 217 private String asString(final Resource resource, final Map<String, String> values) { 218 if (uriTemplate.match(resource.getURI(), values) && values.containsKey("path")) { 219 String path = "/" + values.get("path"); 220 221 final Matcher matcher = FORWARD_COMPONENT_PATTERN.matcher(path); 222 223 if (matcher.matches()) { 224 final boolean metadata = matcher.group(1) != null; 225 final boolean versioning = matcher.group(2) != null; 226 final boolean webacAcl = matcher.group(4) != null; 227 228 if (versioning) { 229 path = replaceOnce(path, "/" + FCR_VERSIONS, "/" + LDPCV_TIME_MAP); 230 } 231 232 if (metadata) { 233 path = replaceOnce(path, "/" + FCR_METADATA, "/" + FEDORA_DESCRIPTION); 234 } 235 236 if (webacAcl) { 237 path = replaceOnce(path, "/" + FCR_ACL, "/" + CONTAINER_WEBAC_ACL); 238 } 239 } 240 241 path = forward.convert(path); 242 243 if (path == null) { 244 return null; 245 } 246 247 try { 248 path = URLDecoder.decode(path, "UTF-8"); 249 } catch (final UnsupportedEncodingException e) { 250 LOGGER.debug("Unable to URL-decode path " + e + " as UTF-8", e); 251 } 252 253 if (path.isEmpty()) { 254 return "/"; 255 } 256 257 // Validate path 258 if (path.contains("//")) { 259 throw new InvalidResourceIdentifierException("Path contains empty element! " + path); 260 } 261 return path; 262 } 263 264 if (isRootWithoutTrailingSlash(resource)) { 265 return "/"; 266 } 267 268 return null; 269 } 270 271 272 private Node getNode(final String path) throws RepositoryException { 273 try { 274 return getJcrSession(session).getNode(path); 275 } catch (final IllegalArgumentException ex) { 276 throw new InvalidResourceIdentifierException("Illegal path: " + path); 277 } 278 } 279 280 /** 281 * Get only the resource path to this resource, before embedding it in a full URI 282 * @param resource with desired path 283 * @return path 284 */ 285 private String doBackwardPathOnly(final FedoraResource resource) { 286 287 final String path = reverse.convert(resource.getPath()); 288 if (path == null) { 289 throw new RepositoryRuntimeException("Unable to process reverse chain for resource " + resource); 290 } 291 292 return convertToExternalPath(path); 293 } 294 295 /** 296 * Converts internal path segments to their external formats. 297 * @param path the internal path 298 * @return the external path 299 */ 300 public static String convertToExternalPath(final String path) { 301 String newPath = replaceOnce(path, "/" + CONTAINER_WEBAC_ACL, "/" + FCR_ACL); 302 303 newPath = replaceOnce(newPath, "/" + LDPCV_TIME_MAP, "/" + FCR_VERSIONS); 304 305 newPath = replaceOnce(newPath, "/" + FEDORA_DESCRIPTION, "/" + FCR_METADATA); 306 307 return newPath; 308 } 309 310 protected void resetTranslationChain() { 311 if (translationChain == null) { 312 translationChain = getTranslationChain(); 313 final List<Converter<String, String>> newChain = 314 new ArrayList<>(singleton(new TransactionIdentifierConverter(session, batch))); 315 newChain.addAll(translationChain); 316 setTranslationChain(newChain); 317 } 318 } 319 320 private void setTranslationChain(final List<Converter<String, String>> chained) { 321 322 translationChain = chained; 323 324 for (final Converter<String, String> t : translationChain) { 325 forward = forward.andThen(t); 326 } 327 for (final Converter<String, String> t : Lists.reverse(translationChain)) { 328 reverse = reverse.andThen(t.reverse()); 329 } 330 } 331 332 333 private static final List<Converter<String, String>> minimalTranslationChain = 334 of(new NamespaceConverter(), new HashConverter()); 335 336 protected List<Converter<String,String>> getTranslationChain() { 337 final ApplicationContext context = getApplicationContext(); 338 if (context != null) { 339 @SuppressWarnings("unchecked") 340 final List<Converter<String,String>> tchain = 341 getApplicationContext().getBean("translationChain", List.class); 342 return tchain; 343 } 344 return minimalTranslationChain; 345 } 346 347 protected ApplicationContext getApplicationContext() { 348 return getCurrentWebApplicationContext(); 349 } 350 351 /** 352 * Translate the current transaction into the identifier 353 */ 354 static class TransactionIdentifierConverter extends Converter<String, String> { 355 public static final String TX_PREFIX = "tx:"; 356 357 private final FedoraSession session; 358 private final boolean batch; 359 360 public TransactionIdentifierConverter(final FedoraSession session, final boolean batch) { 361 this.session = session; 362 this.batch = batch; 363 } 364 365 @Override 366 protected String doForward(final String path) { 367 368 if (path.contains(TX_PREFIX) && !path.contains(txSegment())) { 369 throw new RepositoryRuntimeException("Path " + path 370 + " is not in current transaction " + session.getId()); 371 } 372 373 return replaceOnce(path, txSegment(), EMPTY); 374 } 375 376 @Override 377 protected String doBackward(final String path) { 378 return txSegment() + path; 379 } 380 381 private String txSegment() { 382 return batch ? "/" + TX_PREFIX + session.getId() : EMPTY; 383 } 384 } 385 386 private boolean isRootWithoutTrailingSlash(final Resource resource) { 387 final Map<String, String> values = new HashMap<>(); 388 389 return uriTemplate.match(resource.getURI() + "/", values) && values.containsKey("path") && 390 values.get("path").isEmpty(); 391 } 392}