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}