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}