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 org.fcrepo.kernel.api.FedoraTypes.FEDORA_ID_PREFIX; 021import static org.slf4j.LoggerFactory.getLogger; 022 023import static java.nio.charset.StandardCharsets.UTF_8; 024 025import javax.ws.rs.core.UriBuilder; 026 027import java.net.URLDecoder; 028import java.util.HashMap; 029import java.util.Map; 030 031import org.fcrepo.kernel.api.identifiers.FedoraId; 032import org.glassfish.jersey.uri.UriTemplate; 033import org.slf4j.Logger; 034 035/** 036 * Convert between HTTP URIs (LDP paths) and internal Fedora ID using a 037 * JAX-RS UriBuilder to mediate the URI translation. 038 * 039 * @author whikloj 040 * @since 2019-09-26 041 */ 042public class HttpIdentifierConverter { 043 044 private static final Logger LOGGER = getLogger(HttpIdentifierConverter.class); 045 046 private final UriBuilder uriBuilder; 047 048 private final UriTemplate uriTemplate; 049 050 private static String trimTrailingSlashes(final String string) { 051 return string.replaceAll("/+$", ""); 052 } 053 054 /** 055 * Create a new identifier converter within the given session with the given URI template 056 * @param uriBuilder the uri builder 057 */ 058 public HttpIdentifierConverter(final UriBuilder uriBuilder) { 059 060 this.uriBuilder = uriBuilder; 061 this.uriTemplate = new UriTemplate(uriBuilder.toTemplate()); 062 } 063 064 /** 065 * TODO: This constructor should be removed! 066 * ..it is only used so that Spring can create this class... since it is unclear where the `UriBuilder` will come 067 * ..from when this class is expected to be injected into `ContentExposingResource.java` 068 */ 069 public HttpIdentifierConverter() { 070 // nothing :( 071 this.uriBuilder = null; 072 this.uriTemplate = null; 073 } 074 075 /** 076 * Convert an external URI to an internal ID. 077 * 078 * @param httpUri the external URI. 079 * @return the internal identifier. 080 */ 081 public String toInternalId(final String httpUri) { 082 LOGGER.trace("Translating http URI {} to Fedora ID", httpUri); 083 084 final String path = getPath(httpUri); 085 if (path != null) { 086 final String decodedPath = URLDecoder.decode(path, UTF_8); 087 final String fedoraId = trimTrailingSlashes(decodedPath); 088 089 return FEDORA_ID_PREFIX + fedoraId; 090 } 091 throw new IllegalArgumentException("Cannot translate NULL path"); 092 } 093 094 /** 095 * Test if the provided external URI is in the domain of this repository. 096 * 097 * If it is not in the domain we can't convert it. 098 * 099 * @param httpUri the external URI. 100 * @return true if it is in domain. 101 */ 102 public boolean inExternalDomain(final String httpUri) { 103 LOGGER.trace("Checking if http URI {} is in domain", httpUri); 104 return getPath(httpUri) != null; 105 } 106 107 /** 108 * Convert an internal identifier to an external URI. 109 * 110 * @param fedoraId the internal identifier. 111 * @return the external URI. 112 */ 113 public String toExternalId(final String fedoraId) { 114 LOGGER.trace("Translating Fedora ID {} to Http URI", fedoraId); 115 if (inInternalDomain(fedoraId)) { 116 // If it starts with our prefix, strip the prefix and any leading slashes and use it as the path 117 // part of the URI. 118 final String path = fedoraId.substring(FEDORA_ID_PREFIX.length()).replaceFirst("\\/", ""); 119 final UriBuilder uri = uriBuilder(); 120 if (path.contains("#")) { 121 final String[] split = path.split("#", 2); 122 uri.resolveTemplate("path", split[0], false); 123 uri.fragment(split[1]); 124 } else { 125 uri.resolveTemplate("path", path, false); 126 } 127 return uri.build().toString(); 128 } 129 throw new IllegalArgumentException("Cannot translate IDs without our prefix"); 130 } 131 132 /** 133 * Check if the provided internal identifier is in the domain of the repository. 134 * 135 * If it is not in the domain we can't convert it. 136 * 137 * @param fedoraId the internal identifier. 138 * @return true if it is in domain. 139 */ 140 public boolean inInternalDomain(final String fedoraId) { 141 LOGGER.trace("Checking if fedora ID {} is in domain", fedoraId); 142 return (fedoraId.startsWith(FEDORA_ID_PREFIX)); 143 } 144 145 /** 146 * Return a UriBuilder for the current template. 147 * 148 * @return the uri builder. 149 */ 150 private UriBuilder uriBuilder() { 151 return UriBuilder.fromUri(uriBuilder.toTemplate()); 152 } 153 154 /** 155 * Convert a path to a full url using the UriBuilder template. 156 * @param path the external path. 157 * @return the full url. 158 */ 159 public String toDomain(final String path) { 160 161 final String realPath; 162 if (path == null) { 163 realPath = ""; 164 } else if (path.startsWith("/")) { 165 realPath = path.substring(1); 166 } else { 167 realPath = path; 168 } 169 170 final UriBuilder uri = uriBuilder(); 171 172 if (realPath.contains("#")) { 173 174 final String[] split = realPath.split("#", 2); 175 176 uri.resolveTemplate("path", split[0], false); 177 uri.fragment(split[1]); 178 } else { 179 uri.resolveTemplate("path", realPath, false); 180 181 } 182 return uri.build().toString(); 183 } 184 185 /** 186 * Function to convert from the external path of a URI to an internal FedoraId. 187 * @param externalPath the path part of the external URI. 188 * @return the FedoraId. 189 */ 190 public FedoraId pathToInternalId(final String externalPath) { 191 return FedoraId.create(toInternalId(toDomain(externalPath))); 192 } 193 194 /** 195 * Split the path off the URI. 196 * 197 * @param httpUri the incoming URI. 198 * @return the path of the URI. 199 */ 200 private String getPath(final String httpUri) { 201 final Map<String, String> values = new HashMap<>(); 202 203 if (uriTemplate.match(httpUri, values) && values.containsKey("path")) { 204 return "/" + values.get("path"); 205 } else if (isRootWithoutTrailingSlash(httpUri)) { 206 return "/"; 207 } else if (httpUri.startsWith("info:/rest")) { 208 return mapInternalRestUri(httpUri); 209 } 210 return null; 211 } 212 213 /** 214 * Test if the URI is the root but missing the trailing slash 215 * 216 * @param httpUri the incoming URI. 217 * @return whether or not it is the root minus trailing slash 218 */ 219 private boolean isRootWithoutTrailingSlash(final String httpUri) { 220 final Map<String, String> values = new HashMap<>(); 221 222 return uriTemplate.match(httpUri + "/", values) && values.containsKey("path") && 223 values.get("path").isEmpty(); 224 } 225 226 /** 227 * Takes internal URIs starting with info:/rest and makes full URLs to convert. These URIs come when RDF contains 228 * a URI like </rest/someResource>. This gets converted to info:/rest/someResource as it is a URI but with no 229 * scheme. 230 * @param httpUri the partial URI 231 * @return the path part of the url 232 */ 233 private String mapInternalRestUri(final String httpUri) { 234 // This uri started with </rest...> and is an internal URI. 235 final String internalRestString = internalIdPrefix() + "/rest"; 236 if (httpUri.startsWith(internalRestString)) { 237 String realpath = httpUri.substring(internalRestString.length()); 238 if (realpath.startsWith("/")) { 239 realpath = realpath.substring(1); 240 } 241 final String fullUri = uriBuilder.build(realpath).toString(); 242 return getPath(fullUri); 243 } 244 return null; 245 } 246 247 /** 248 * Figure out what identifier you get when providing a absolute URL without hostname. 249 * @return the identifier. 250 */ 251 private String internalIdPrefix() { 252 String internalPrefix = FEDORA_ID_PREFIX; 253 if (internalPrefix.contains(":")) { 254 internalPrefix = internalPrefix.substring(0, internalPrefix.indexOf(":") + 1); 255 } 256 return internalPrefix; 257 } 258 259}