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}