001/*
002 * SPDX-FileCopyrightText: none
003 * SPDX-License-Identifier: CC0-1.0
004 */
005
006package dev.metaschema.core.util;
007
008import java.net.URI;
009import java.net.URISyntaxException;
010import java.nio.file.InvalidPathException;
011import java.nio.file.Path;
012import java.nio.file.Paths;
013import java.util.Arrays;
014import java.util.Objects;
015import java.util.regex.Pattern;
016
017import edu.umd.cs.findbugs.annotations.NonNull;
018
019/**
020 * A collection of methods for manipulating uniform resource identifiers (URIs),
021 * providing functionality for URI resolution, relativization, and path
022 * manipulation.
023 * <p>
024 * This utility class supports both local file paths and remote URIs.
025 */
026public final class UriUtils {
027  private static final Pattern URI_SEPERATOR_PATTERN = Pattern.compile("\\/");
028  private static final String URI_SEPERATOR = "/";
029
030  private UriUtils() {
031    // disable construction
032  }
033
034  /**
035   * Process a string to a local file path or remote location. If the location is
036   * convertible to a URI, return the {@link URI}. Normalize the resulting URI
037   * with the base URI, if provided.
038   *
039   * @param location
040   *          a string defining a remote or local file-based location
041   * @param baseUri
042   *          the base URI to use for URI normalization
043   * @return a new URI
044   * @throws URISyntaxException
045   *           if the location string is not convertible to URI
046   */
047  @SuppressWarnings("PMD.PreserveStackTrace")
048  @NonNull
049  public static URI toUri(@NonNull String location, @NonNull URI baseUri) throws URISyntaxException {
050    URI asUri;
051    try {
052      asUri = new URI(location);
053    } catch (URISyntaxException ex) {
054      // the location is not a valid URI
055      try {
056        // try to parse the location as a local file path
057        Path path = Paths.get(location);
058        asUri = path.toUri();
059      } catch (@SuppressWarnings("unused") InvalidPathException ex2) {
060        // not a local file path, so rethrow the original URI exception
061        throw ex;
062      }
063    }
064    return ObjectUtils.notNull(baseUri.resolve(asUri.normalize()));
065  }
066
067  /**
068   * This function extends the functionality of {@link URI#relativize(URI)} by
069   * supporting relative reference pathing (e.g., ..), when the {@code prepend}
070   * parameter is set to {@code true}.
071   *
072   * @param base
073   *          the URI to relativize against
074   * @param other
075   *          the URI to make relative
076   * @param prepend
077   *          if {@code true}, then prepend relative pathing
078   * @return a new relative URI
079   * @throws URISyntaxException
080   *           if any of the URIs are malformed
081   */
082  @NonNull
083  public static URI relativize(URI base, URI other, boolean prepend) throws URISyntaxException {
084    URI normBase = Objects.requireNonNull(base).normalize();
085    URI normOther = Objects.requireNonNull(other).normalize();
086    URI retval = ObjectUtils.notNull(normBase.relativize(normOther));
087
088    if (prepend && !normBase.isOpaque() && !retval.isOpaque() && hasSameSchemeAndAuthority(normBase, retval)) {
089      // the URIs are not opaque and they share the same scheme and authority
090      String basePath = normBase.getPath();
091      String targetPath = normOther.getPath();
092      String newPath = prependRelativePath(basePath, targetPath);
093
094      retval = new URI(null, null, newPath, normOther.getQuery(), normOther.getFragment());
095    }
096    return retval;
097  }
098
099  private static boolean hasSameSchemeAndAuthority(URI base, URI other) {
100    String baseScheme = base.getScheme();
101    boolean retval = baseScheme == null && other.getScheme() == null
102        || baseScheme != null && baseScheme.equals(other.getScheme());
103    String baseAuthority = base.getAuthority();
104    return retval && (baseAuthority == null && other.getAuthority() == null
105        || baseAuthority != null && baseAuthority.equals(other.getAuthority()));
106  }
107
108  /**
109   * Get the path of the provided target relative to the path of the provided
110   * base.
111   *
112   * @param base
113   *          the base path to resolve against
114   * @param target
115   *          the URI to relativize against the base
116   * @return the relativized URI
117   */
118  @SuppressWarnings("PMD.CyclomaticComplexity")
119  public static String prependRelativePath(String base, String target) {
120    // based on code from
121    // http://stackoverflow.com/questions/10801283/get-relative-path-of-two-uris-in-java
122
123    // Split paths into segments
124    String[] baseSegments = URI_SEPERATOR_PATTERN.split(base);
125    String[] targetSegments = URI_SEPERATOR_PATTERN.split(target, -1);
126
127    // Discard trailing segment of base path, since this resource doesn't matter
128    if (baseSegments.length > 0 && !base.endsWith(URI_SEPERATOR)) {
129      baseSegments = Arrays.copyOf(baseSegments, baseSegments.length - 1);
130    }
131
132    // Remove common prefix segments
133    int segmentIndex = 0;
134    while (segmentIndex < baseSegments.length && segmentIndex < targetSegments.length
135        && baseSegments[segmentIndex].equals(targetSegments[segmentIndex])) {
136      segmentIndex++;
137    }
138
139    // Construct the relative path
140    StringBuilder retval = new StringBuilder();
141    for (int j = 0; j < baseSegments.length - segmentIndex; j++) {
142      retval.append("..");
143      if (retval.length() != 0) {
144        retval.append(URI_SEPERATOR);
145      }
146    }
147
148    for (int j = segmentIndex; j < targetSegments.length; j++) {
149      retval.append(targetSegments[j]);
150      if (retval.length() != 0 && j < targetSegments.length - 1) {
151        retval.append(URI_SEPERATOR);
152      }
153    }
154    return retval.toString();
155  }
156}