001/* 002 * Copyright 2022-2025 Revetware LLC. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package com.soklet.core; 018 019import com.soklet.core.ResourcePathDeclaration.Component; 020import com.soklet.core.ResourcePathDeclaration.ComponentType; 021 022import javax.annotation.Nonnull; 023import javax.annotation.Nullable; 024import javax.annotation.concurrent.ThreadSafe; 025import java.util.Arrays; 026import java.util.Collections; 027import java.util.LinkedHashMap; 028import java.util.List; 029import java.util.Map; 030import java.util.Objects; 031 032import static java.lang.String.format; 033import static java.util.Collections.emptyList; 034import static java.util.Collections.unmodifiableList; 035import static java.util.Objects.requireNonNull; 036 037/** 038 * An HTTP URL path used to resolve a <em>Resource Method</em> at runtime, such as {@code /users/123}. 039 * <p> 040 * You may obtain instances via the {@link #of(String)} factory method. 041 * <p> 042 * <strong>Note: this type is not normally used by Soklet applications unless they support <a href="https://www.soklet.com/docs/server-sent-events">Server-Sent Events</a> or choose to implement a custom {@link ResourceMethodResolver}.</strong> 043 * <p> 044 * The corresponding compile-time type for {@link ResourcePath} is {@link ResourcePathDeclaration} and functionality is provided to check if the two "match" via {@link #matches(ResourcePathDeclaration)}. 045 * <p> 046 * For example, a {@link ResourcePath} {@code /users/123} would match {@link ResourcePathDeclaration} {@code /users/{userId}}. 047 * 048 * @author <a href="https://www.revetkn.com">Mark Allen</a> 049 */ 050@ThreadSafe 051public class ResourcePath { 052 @Nonnull 053 private final String path; 054 @Nonnull 055 private final List<String> components; 056 057 /** 058 * Vends an instance that represents a runtime representation of a resource path, for example {@code /users/123}. 059 * <p> 060 * This is in contrast to {@link ResourcePathDeclaration}, which represents compile-time path declarations 061 * that may include placeholders, e.g. {@code /users/{userId}}. 062 * 063 * @param path a runtime path which may not include placeholders 064 */ 065 @Nonnull 066 public static ResourcePath of(@Nonnull String path) { 067 requireNonNull(path); 068 return new ResourcePath(path); 069 } 070 071 protected ResourcePath(@Nonnull String path) { 072 requireNonNull(path); 073 this.path = ResourcePathDeclaration.normalizePath(path); 074 this.components = unmodifiableList(extractComponents(this.path)); 075 } 076 077 /** 078 * Does this resource path match the given resource path (taking placeholders into account, if present)? 079 * <p> 080 * For example, resource path {@code /users/123} would match the resource path declaration {@code /users/{userId}}. 081 * 082 * @param resourcePathDeclaration the resource path against which to match 083 * @return {@code true} if the paths match, {@code false} otherwise 084 */ 085 @Nonnull 086 public Boolean matches(@Nonnull ResourcePathDeclaration resourcePathDeclaration) { 087 requireNonNull(resourcePathDeclaration); 088 089 if (resourcePathDeclaration.getComponents().size() != getComponents().size()) 090 return false; 091 092 for (int i = 0; i < resourcePathDeclaration.getComponents().size(); ++i) { 093 Component resourcePathDeclarationComponent = resourcePathDeclaration.getComponents().get(i); 094 String resourcePathComponent = getComponents().get(i); 095 096 if (resourcePathDeclarationComponent.getType() == ComponentType.PLACEHOLDER) 097 continue; 098 099 if (!resourcePathDeclarationComponent.getValue().equals(resourcePathComponent)) 100 return false; 101 } 102 103 return true; 104 } 105 106 /** 107 * What is the mapping between this resource path's placeholder values to the given resource path declaration's placeholder names? 108 * <p> 109 * For example, placeholder extraction for resource path {@code /users/123} and resource path declaration {@code /users/{userId}} 110 * would result in a value equivalent to {@code Map.of("userId", "123")}. 111 * <p> 112 * Resource path placeholder values are automatically URL-decoded. For example, placeholder extraction for resource path declaration {@code /users/{userId}} 113 * and resource path {@code /users/ab%20c} would result in a value equivalent to {@code Map.of("userId", "ab c")}. 114 * 115 * @param resourcePathDeclaration compile-time resource path, used to provide placeholder names 116 * @return a mapping of placeholder names to values, or the empty map if there were no placeholders 117 * @throws IllegalArgumentException if the provided resource path declaration does not match this resource path, i.e. {@link #matches(ResourcePathDeclaration)} is {@code false} 118 */ 119 @Nonnull 120 public Map<String, String> extractPlaceholders(@Nonnull ResourcePathDeclaration resourcePathDeclaration) { 121 requireNonNull(resourcePathDeclaration); 122 123 if (!matches(resourcePathDeclaration)) 124 throw new IllegalArgumentException(format("%s is not a match for %s so we cannot extract placeholders", this, 125 resourcePathDeclaration)); 126 127 Map<String, String> placeholders = new LinkedHashMap<>(resourcePathDeclaration.getComponents().size()); 128 129 for (int i = 0; i < resourcePathDeclaration.getComponents().size(); ++i) { 130 Component resourcePathDeclarationComponent = resourcePathDeclaration.getComponents().get(i); 131 String resourcePathComponent = getComponents().get(i); 132 133 if (resourcePathDeclarationComponent.getType() == ComponentType.PLACEHOLDER) 134 placeholders.put(resourcePathDeclarationComponent.getValue(), resourcePathComponent); 135 } 136 137 return Collections.unmodifiableMap(placeholders); 138 } 139 140 /** 141 * What is the string representation of this resource path? 142 * 143 * @return the string representation of this resource path, which must start with {@code /} 144 */ 145 @Nonnull 146 public String getPath() { 147 return this.path; 148 } 149 150 /** 151 * What are the {@code /}-delimited components of this resource path? 152 * 153 * @return the components, or the empty list if this path is equal to {@code /} 154 */ 155 @Nonnull 156 public List<String> getComponents() { 157 return this.components; 158 } 159 160 /** 161 * Assumes {@code path} is already normalized via {@link ResourcePathDeclaration#normalizePath(String)}. 162 * 163 * @param path (nonnull) Path from which components are extracted 164 * @return Logical components of the supplied {@code path} 165 */ 166 @Nonnull 167 protected List<String> extractComponents(@Nonnull String path) { 168 requireNonNull(path); 169 170 if ("/".equals(path)) 171 return emptyList(); 172 173 // Strip off leading / 174 path = path.substring(1); 175 176 return Arrays.asList(path.split("/")); 177 } 178 179 @Override 180 public String toString() { 181 return format("%s{path=%s, components=%s}", getClass().getSimpleName(), getPath(), getComponents()); 182 } 183 184 @Override 185 public boolean equals(@Nullable Object object) { 186 if (this == object) 187 return true; 188 189 if (!(object instanceof ResourcePath resourcePath)) 190 return false; 191 192 return Objects.equals(getPath(), resourcePath.getPath()); 193 } 194 195 @Override 196 public int hashCode() { 197 return Objects.hash(getPath()); 198 } 199}