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 java.nio.charset.StandardCharsets.UTF_8;
021import static org.fcrepo.kernel.api.FedoraTypes.FEDORA_ID_PREFIX;
022import static org.slf4j.LoggerFactory.getLogger;
023
024import java.net.URLDecoder;
025import java.util.HashMap;
026import java.util.Map;
027
028import javax.ws.rs.core.UriBuilder;
029
030import org.fcrepo.kernel.api.identifiers.FedoraId;
031
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 with the given URI template.
056     * @param uriBuilder the uri builder
057     */
058    public HttpIdentifierConverter(final UriBuilder uriBuilder) {
059        this.uriBuilder = uriBuilder;
060        this.uriTemplate = new UriTemplate(uriBuilder.toTemplate());
061    }
062
063    /**
064     * Convert an external URI to an internal ID.
065     *
066     * @param httpUri the external URI.
067     * @return the internal identifier.
068     */
069    public String toInternalId(final String httpUri) {
070        return toInternalId(httpUri, false);
071    }
072
073    /**
074     * Convert an external URI to an internal ID.
075     *
076     * @param httpUri the external URI.
077     * @param encoded whether the internal ID is encoded or not.
078     * @return the internal identifier.
079     */
080    public String toInternalId(final String httpUri, final boolean encoded) {
081        LOGGER.trace("Translating http URI {} to Fedora ID with encoded set to {}", httpUri, encoded);
082
083        final String path = getPath(httpUri);
084        if (path != null) {
085            final String decodedPath;
086            if (!encoded) {
087                decodedPath = URLDecoder.decode(path, UTF_8);
088            } else {
089                decodedPath = path;
090            }
091            final String fedoraId = trimTrailingSlashes(decodedPath);
092
093            return FEDORA_ID_PREFIX + fedoraId;
094        }
095        throw new IllegalArgumentException("Cannot translate NULL path");
096    }
097
098    /**
099     * Test if the provided external URI is in the domain of this repository.
100     *
101     * If it is not in the domain we can't convert it.
102     *
103     * @param httpUri the external URI.
104     * @return true if it is in domain.
105     */
106    public boolean inExternalDomain(final String httpUri) {
107        LOGGER.trace("Checking if http URI {} is in domain", httpUri);
108        return getPath(httpUri) != null;
109    }
110
111    /**
112     * Make a URI into an absolute URI (if relative), an internal encoded ID (if in repository domain) or leave it
113     * alone.
114     * @param httpUri the URI
115     * @return an absolute URI, the original URI or an internal ID.
116     */
117    public String translateUri(final String httpUri) {
118        if (inExternalDomain(httpUri)) {
119            return toInternalId(httpUri, true);
120        } else if (httpUri.startsWith("/")) {
121            // Is a relative URI.
122            // Build a fake URI using the hostname so we can resolve against it.
123            final var uri = uriBuilder.build("placeholder");
124            return uri.resolve(httpUri).toString();
125        }
126        return httpUri;
127    }
128
129    /**
130     * Convert an internal identifier to an external URI.
131     *
132     * @param fedoraId the internal identifier.
133     * @return the external URI.
134     */
135    public String toExternalId(final String fedoraId) {
136        LOGGER.trace("Translating Fedora ID {} to Http URI", fedoraId);
137        if (inInternalDomain(fedoraId)) {
138            // If it starts with our prefix, strip the prefix and any leading slashes and use it as the path
139            // part of the URI.
140            final String path = fedoraId.substring(FEDORA_ID_PREFIX.length()).replaceFirst("/", "");
141            return buildUri(path);
142        }
143        throw new IllegalArgumentException("Cannot translate IDs without our prefix");
144    }
145
146    /**
147     * Check if the provided internal identifier is in the domain of the repository.
148     *
149     * If it is not in the domain we can't convert it.
150     *
151     * @param fedoraId the internal identifier.
152     * @return true if it is in domain.
153     */
154    public boolean inInternalDomain(final String fedoraId) {
155        LOGGER.trace("Checking if fedora ID {} is in domain", fedoraId);
156        return (fedoraId.startsWith(FEDORA_ID_PREFIX));
157    }
158
159    /**
160     * Return a UriBuilder for the current template.
161     *
162     * @return the uri builder.
163     */
164    private UriBuilder uriBuilder() {
165        return UriBuilder.fromUri(uriBuilder.toTemplate());
166    }
167
168    /**
169     * Convert a path to a full url using the UriBuilder template.
170     * @param path the external path.
171     * @return the full url.
172     */
173    public String toDomain(final String path) {
174
175        final String realPath;
176        if (path == null) {
177            realPath = "";
178        } else if (path.startsWith("/")) {
179            realPath = path.substring(1);
180        } else {
181            realPath = path;
182        }
183
184        return buildUri(realPath);
185    }
186
187    /**
188     * Function to convert from the external path of a URI to an internal FedoraId.
189     * @param externalPath the path part of the external URI.
190     * @return the FedoraId.
191     */
192    public FedoraId pathToInternalId(final String externalPath) {
193        return FedoraId.create(externalPath);
194    }
195
196    /**
197     * Utility to build a URL.
198     * @param path the path from the internal Id.
199     * @return an external URI.
200     */
201    private String buildUri(final String path) {
202        final UriBuilder uri = uriBuilder();
203        if (path.contains("#")) {
204            final String[] split = path.split("#", 2);
205            uri.resolveTemplateFromEncoded("path", split[0]);
206            uri.fragment(split[1]);
207        } else {
208            uri.resolveTemplateFromEncoded("path", path);
209        }
210        return uri.build().toString();
211    }
212
213    /**
214     * Split the path off the URI.
215     *
216     * @param httpUri the incoming URI.
217     * @return the path of the URI.
218     */
219    private String getPath(final String httpUri) {
220        final Map<String, String> values = new HashMap<>();
221
222        if (uriTemplate.match(httpUri, values) && values.containsKey("path")) {
223            return "/" + values.get("path");
224        } else if (isRootWithoutTrailingSlash(httpUri)) {
225            return "/";
226        }
227        return null;
228    }
229
230    /**
231     * Test if the URI is the root but missing the trailing slash
232     *
233     * @param httpUri the incoming URI.
234     * @return whether or not it is the root minus trailing slash
235     */
236    private boolean isRootWithoutTrailingSlash(final String httpUri) {
237        final Map<String, String> values = new HashMap<>();
238
239        return uriTemplate.match(httpUri + "/", values) && values.containsKey("path") &&
240            values.get("path").isEmpty();
241    }
242
243}