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 * Convert an external URI to an internal ID. 066 * 067 * @param httpUri the external URI. 068 * @return the internal identifier. 069 */ 070 public String toInternalId(final String httpUri) { 071 LOGGER.trace("Translating http URI {} to Fedora ID", httpUri); 072 073 final String path = getPath(httpUri); 074 if (path != null) { 075 final String decodedPath = URLDecoder.decode(path, UTF_8); 076 final String fedoraId = trimTrailingSlashes(decodedPath); 077 078 return FEDORA_ID_PREFIX + fedoraId; 079 } 080 throw new IllegalArgumentException("Cannot translate NULL path"); 081 } 082 083 /** 084 * Test if the provided external URI is in the domain of this repository. 085 * 086 * If it is not in the domain we can't convert it. 087 * 088 * @param httpUri the external URI. 089 * @return true if it is in domain. 090 */ 091 public boolean inExternalDomain(final String httpUri) { 092 LOGGER.trace("Checking if http URI {} is in domain", httpUri); 093 return getPath(httpUri) != null; 094 } 095 096 /** 097 * Convert an internal identifier to an external URI. 098 * 099 * @param fedoraId the internal identifier. 100 * @return the external URI. 101 */ 102 public String toExternalId(final String fedoraId) { 103 LOGGER.trace("Translating Fedora ID {} to Http URI", fedoraId); 104 if (inInternalDomain(fedoraId)) { 105 // If it starts with our prefix, strip the prefix and any leading slashes and use it as the path 106 // part of the URI. 107 final String path = fedoraId.substring(FEDORA_ID_PREFIX.length()).replaceFirst("\\/", ""); 108 return buildUri(path); 109 } 110 throw new IllegalArgumentException("Cannot translate IDs without our prefix"); 111 } 112 113 /** 114 * Check if the provided internal identifier is in the domain of the repository. 115 * 116 * If it is not in the domain we can't convert it. 117 * 118 * @param fedoraId the internal identifier. 119 * @return true if it is in domain. 120 */ 121 public boolean inInternalDomain(final String fedoraId) { 122 LOGGER.trace("Checking if fedora ID {} is in domain", fedoraId); 123 return (fedoraId.startsWith(FEDORA_ID_PREFIX)); 124 } 125 126 /** 127 * Return a UriBuilder for the current template. 128 * 129 * @return the uri builder. 130 */ 131 private UriBuilder uriBuilder() { 132 return UriBuilder.fromUri(uriBuilder.toTemplate()); 133 } 134 135 /** 136 * Convert a path to a full url using the UriBuilder template. 137 * @param path the external path. 138 * @return the full url. 139 */ 140 public String toDomain(final String path) { 141 142 final String realPath; 143 if (path == null) { 144 realPath = ""; 145 } else if (path.startsWith("/")) { 146 realPath = path.substring(1); 147 } else { 148 realPath = path; 149 } 150 151 return buildUri(realPath); 152 } 153 154 /** 155 * Function to convert from the external path of a URI to an internal FedoraId. 156 * @param externalPath the path part of the external URI. 157 * @return the FedoraId. 158 */ 159 public FedoraId pathToInternalId(final String externalPath) { 160 return FedoraId.create(externalPath); 161 } 162 163 /** 164 * Utility to build a URL. 165 * @param path the path from the internal Id. 166 * @return an external URI. 167 */ 168 private String buildUri(final String path) { 169 final UriBuilder uri = uriBuilder(); 170 if (path.contains("#")) { 171 final String[] split = path.split("#", 2); 172 uri.resolveTemplateFromEncoded("path", split[0]); 173 uri.fragment(split[1]); 174 } else { 175 uri.resolveTemplateFromEncoded("path", path); 176 } 177 return uri.build().toString(); 178 } 179 180 /** 181 * Split the path off the URI. 182 * 183 * @param httpUri the incoming URI. 184 * @return the path of the URI. 185 */ 186 private String getPath(final String httpUri) { 187 final Map<String, String> values = new HashMap<>(); 188 189 if (uriTemplate.match(httpUri, values) && values.containsKey("path")) { 190 return "/" + values.get("path"); 191 } else if (isRootWithoutTrailingSlash(httpUri)) { 192 return "/"; 193 } else if (httpUri.startsWith("info:/rest")) { 194 return mapInternalRestUri(httpUri); 195 } 196 return null; 197 } 198 199 /** 200 * Test if the URI is the root but missing the trailing slash 201 * 202 * @param httpUri the incoming URI. 203 * @return whether or not it is the root minus trailing slash 204 */ 205 private boolean isRootWithoutTrailingSlash(final String httpUri) { 206 final Map<String, String> values = new HashMap<>(); 207 208 return uriTemplate.match(httpUri + "/", values) && values.containsKey("path") && 209 values.get("path").isEmpty(); 210 } 211 212 /** 213 * Takes internal URIs starting with info:/rest and makes full URLs to convert. These URIs come when RDF contains 214 * a URI like </rest/someResource>. This gets converted to info:/rest/someResource as it is a URI but with no 215 * scheme. 216 * @param httpUri the partial URI 217 * @return the path part of the url 218 */ 219 private String mapInternalRestUri(final String httpUri) { 220 // This uri started with </rest...> and is an internal URI. 221 final String internalRestString = internalIdPrefix() + "/rest"; 222 if (httpUri.startsWith(internalRestString)) { 223 String realpath = httpUri.substring(internalRestString.length()); 224 if (realpath.startsWith("/")) { 225 realpath = realpath.substring(1); 226 } 227 final String fullUri = uriBuilder.build(realpath).toString(); 228 return getPath(fullUri); 229 } 230 return null; 231 } 232 233 /** 234 * Figure out what identifier you get when providing a absolute URL without hostname. 235 * @return the identifier. 236 */ 237 private String internalIdPrefix() { 238 String internalPrefix = FEDORA_ID_PREFIX; 239 if (internalPrefix.contains(":")) { 240 internalPrefix = internalPrefix.substring(0, internalPrefix.indexOf(":") + 1); 241 } 242 return internalPrefix; 243 } 244 245}