001/*
002 * The contents of this file are subject to the license and copyright
003 * detailed in the LICENSE and NOTICE files at the root of the source
004 * tree.
005 */
006package org.fcrepo.http.commons.api.rdf;
007
008import static java.nio.charset.StandardCharsets.UTF_8;
009import static org.fcrepo.kernel.api.FedoraTypes.FEDORA_ID_PREFIX;
010import static org.slf4j.LoggerFactory.getLogger;
011
012import java.net.URLDecoder;
013import java.util.HashMap;
014import java.util.Map;
015
016import javax.ws.rs.core.UriBuilder;
017
018import org.fcrepo.kernel.api.identifiers.FedoraId;
019
020import org.glassfish.jersey.uri.UriTemplate;
021import org.slf4j.Logger;
022
023/**
024 * Convert between HTTP URIs (LDP paths) and internal Fedora ID using a
025 * JAX-RS UriBuilder to mediate the URI translation.
026 *
027 * @author whikloj
028 * @since 2019-09-26
029 */
030public class HttpIdentifierConverter {
031
032    private static final Logger LOGGER = getLogger(HttpIdentifierConverter.class);
033
034    private final UriBuilder uriBuilder;
035
036    private final UriTemplate uriTemplate;
037
038    private static String trimTrailingSlashes(final String string) {
039        return string.replaceAll("/+$", "");
040    }
041
042    /**
043     * Create a new identifier converter with the given URI template.
044     * @param uriBuilder the uri builder
045     */
046    public HttpIdentifierConverter(final UriBuilder uriBuilder) {
047        this.uriBuilder = uriBuilder;
048        this.uriTemplate = new UriTemplate(uriBuilder.toTemplate());
049    }
050
051    /**
052     * Convert an external URI to an internal ID.
053     *
054     * @param httpUri the external URI.
055     * @return the internal identifier.
056     */
057    public String toInternalId(final String httpUri) {
058        return toInternalId(httpUri, false);
059    }
060
061    /**
062     * Convert an external URI to an internal ID.
063     *
064     * @param httpUri the external URI.
065     * @param encoded whether the internal ID is encoded or not.
066     * @return the internal identifier.
067     */
068    public String toInternalId(final String httpUri, final boolean encoded) {
069        LOGGER.trace("Translating http URI {} to Fedora ID with encoded set to {}", httpUri, encoded);
070
071        final String path = getPath(httpUri);
072        if (path != null) {
073            final String decodedPath;
074            if (!encoded) {
075                decodedPath = URLDecoder.decode(path, UTF_8);
076            } else {
077                decodedPath = path;
078            }
079            final String fedoraId = trimTrailingSlashes(decodedPath);
080
081            return FEDORA_ID_PREFIX + fedoraId;
082        }
083        throw new IllegalArgumentException("Cannot translate NULL path extracted from URI " + httpUri);
084    }
085
086    /**
087     * Test if the provided external URI is in the domain of this repository.
088     *
089     * If it is not in the domain we can't convert it.
090     *
091     * @param httpUri the external URI.
092     * @return true if it is in domain.
093     */
094    public boolean inExternalDomain(final String httpUri) {
095        LOGGER.trace("Checking if http URI {} is in domain", httpUri);
096        return getPath(httpUri) != null;
097    }
098
099    /**
100     * Make a URI into an absolute URI (if relative), an internal encoded ID (if in repository domain) or leave it
101     * alone.
102     * @param httpUri the URI
103     * @return an absolute URI, the original URI or an internal ID.
104     */
105    public String translateUri(final String httpUri) {
106        if (inExternalDomain(httpUri)) {
107            return toInternalId(httpUri, true);
108        } else if (httpUri.startsWith("/")) {
109            // Is a relative URI.
110            // Build a fake URI using the hostname so we can resolve against it.
111            final var uri = uriBuilder.build("placeholder");
112            return uri.resolve(httpUri).toString();
113        }
114        return httpUri;
115    }
116
117    /**
118     * Convert an internal identifier to an external URI.
119     *
120     * @param fedoraId the internal identifier.
121     * @return the external URI.
122     */
123    public String toExternalId(final String fedoraId) {
124        LOGGER.trace("Translating Fedora ID {} to Http URI", fedoraId);
125        if (inInternalDomain(fedoraId)) {
126            // If it starts with our prefix, strip the prefix and any leading slashes and use it as the path
127            // part of the URI.
128            final String path = fedoraId.substring(FEDORA_ID_PREFIX.length()).replaceFirst("/", "");
129            return buildUri(path);
130        }
131        throw new IllegalArgumentException("Cannot translate IDs without our prefix");
132    }
133
134    /**
135     * Check if the provided internal identifier is in the domain of the repository.
136     *
137     * If it is not in the domain we can't convert it.
138     *
139     * @param fedoraId the internal identifier.
140     * @return true if it is in domain.
141     */
142    public boolean inInternalDomain(final String fedoraId) {
143        LOGGER.trace("Checking if fedora ID {} is in domain", fedoraId);
144        return (fedoraId.startsWith(FEDORA_ID_PREFIX));
145    }
146
147    /**
148     * Return a UriBuilder for the current template.
149     *
150     * @return the uri builder.
151     */
152    private UriBuilder uriBuilder() {
153        return UriBuilder.fromUri(uriBuilder.toTemplate());
154    }
155
156    /**
157     * Convert a path to a full url using the UriBuilder template.
158     * @param path the external path.
159     * @return the full url.
160     */
161    public String toDomain(final String path) {
162
163        final String realPath;
164        if (path == null) {
165            realPath = "";
166        } else if (path.startsWith("/")) {
167            realPath = path.substring(1);
168        } else {
169            realPath = path;
170        }
171
172        return buildUri(realPath);
173    }
174
175    /**
176     * Function to convert from the external path of a URI to an internal FedoraId.
177     * @param externalPath the path part of the external URI.
178     * @return the FedoraId.
179     */
180    public FedoraId pathToInternalId(final String externalPath) {
181        return FedoraId.create(externalPath);
182    }
183
184    /**
185     * Utility to build a URL.
186     * @param path the path from the internal Id.
187     * @return an external URI.
188     */
189    private String buildUri(final String path) {
190        final UriBuilder uri = uriBuilder();
191        if (path.contains("#")) {
192            final String[] split = path.split("#", 2);
193            uri.resolveTemplateFromEncoded("path", split[0]);
194            uri.fragment(split[1]);
195        } else {
196            uri.resolveTemplateFromEncoded("path", path);
197        }
198        return uri.build().toString();
199    }
200
201    /**
202     * Split the path off the URI.
203     *
204     * @param httpUri the incoming URI.
205     * @return the path of the URI.
206     */
207    private String getPath(final String httpUri) {
208        final Map<String, String> values = new HashMap<>();
209
210        if (uriTemplate.match(httpUri, values) && values.containsKey("path")) {
211            return "/" + values.get("path");
212        } else if (isRootWithoutTrailingSlash(httpUri)) {
213            return "/";
214        }
215        return null;
216    }
217
218    /**
219     * Test if the URI is the root but missing the trailing slash
220     *
221     * @param httpUri the incoming URI.
222     * @return whether or not it is the root minus trailing slash
223     */
224    private boolean isRootWithoutTrailingSlash(final String httpUri) {
225        final Map<String, String> values = new HashMap<>();
226
227        return uriTemplate.match(httpUri + "/", values) && values.containsKey("path") &&
228            values.get("path").isEmpty();
229    }
230
231}