001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.rest.server; 018 019import static org.apache.juneau.commons.utils.CollectionUtils.*; 020import static org.apache.juneau.commons.utils.Shorts.*; 021 022import java.util.*; 023import java.util.stream.*; 024 025import org.apache.juneau.commons.inject.*; 026import org.apache.juneau.http.response.*; 027 028/** 029 * Encapsulates the set of {@link RestOp}-annotated methods within a single {@link Rest}-annotated object. 030 */ 031public class RestOperations { 032 033 /** 034 * Builder class. 035 */ 036 public static class Builder { 037 038 private final BeanStore beanStore; 039 TreeMap<String,TreeSet<RestOpContext>> map; 040 Set<RestOpContext> set; 041 042 /** 043 * Constructor. 044 * 045 * @param beanStore The bean store to use for creating beans. 046 */ 047 protected Builder(BeanStore beanStore) { 048 this.beanStore = beanStore; 049 map = new TreeMap<>(); 050 set = set(); 051 } 052 053 /** 054 * Returns the bean store used by this builder. 055 * 056 * @return The bean store used by this builder. 057 */ 058 public BeanStore beanStore() { 059 return beanStore; 060 } 061 062 /** 063 * Adds a method context to this builder. 064 * 065 * @param value The REST method context to add. Must not be <jk>null</jk>. 066 * @return This object. 067 */ 068 public Builder add(RestOpContext value) { 069 return add(value.getHttpMethod(), value); 070 } 071 072 /** 073 * Adds a method context to this builder. 074 * 075 * @param httpMethodName The HTTP method name. Must not be <jk>null</jk>. 076 * @param value The REST method context to add. Must not be <jk>null</jk>. 077 * @return This object. 078 */ 079 public Builder add(String httpMethodName, RestOpContext value) { 080 httpMethodName = ucr(httpMethodName); 081 map.computeIfAbsent(httpMethodName, k -> new TreeSet<>()).add(value); 082 set.add(value); 083 return this; 084 } 085 086 /** 087 * Builds the operations. 088 * 089 * @return A new {@link RestOperations}. 090 */ 091 public RestOperations build() { 092 return new RestOperations(this); 093 } 094 } 095 096 /** 097 * Represents a null value for the RestOperations class. 098 * 099 * <p> 100 * This is used internally when no custom RestOperations implementation is specified. 101 * The {@code Void} class is used as a placeholder when the {@link Rest} annotation 102 * does not specify a custom {@code RestOperations} class. 103 */ 104 @SuppressWarnings({ 105 "java:S112" // throws Exception intentional - callback/lifecycle method 106 }) 107 public final class Void extends RestOperations { 108 109 /** 110 * Constructor. 111 * 112 * @param builder The builder. Must not be <jk>null</jk>. 113 * @throws Exception If initialization fails. 114 */ 115 public Void(Builder builder) throws Exception { 116 super(builder); 117 } 118 } 119 120 /** 121 * Static creator. 122 * 123 * @param beanStore The bean store to use for creating beans. 124 * @return A new builder for this object. 125 */ 126 public static Builder create(BeanStore beanStore) { 127 return new Builder(beanStore); 128 } 129 130 private final Map<String,List<RestOpContext>> map; 131 private final RestOpContext[] list; 132 133 /** 134 * Constructor. 135 * 136 * @param builder The builder containing the settings for this object. Must not be <jk>null</jk>. 137 */ 138 public RestOperations(Builder builder) { 139 Map<String,List<RestOpContext>> m = map(); 140 for (var e : builder.map.entrySet()) 141 m.put(e.getKey(), toList(e.getValue())); 142 this.map = m; 143 this.list = array(builder.set, RestOpContext.class); 144 145 // Fail the resource if an operation declared @Mutating is bound to a method defined as safe. Done here 146 // because this constructor is the one path every operation table passes through, so the check cannot be 147 // skipped by omission -- and it runs before findOperation can dispatch anything. A resource with no 148 // @Mutating anywhere is unaffected. 149 MethodSafety.check(getOpContexts()); 150 } 151 152 /** 153 * Finds the method that should handle the specified call. 154 * 155 * @param session The HTTP call. Must not be <jk>null</jk>. 156 * @return The method that should handle the specified call. 157 * @throws MethodNotAllowed If no methods implement the requested HTTP method. 158 * @throws PreconditionFailed At least one method was found but it didn't match one or more matchers. 159 * @throws NotFound HTTP method match was found but matching path was not. 160 */ 161 @SuppressWarnings({ 162 "java:S3776" // Cognitive complexity acceptable for REST operation lookup/matching logic 163 }) 164 public RestOpContext findOperation(RestSession session) throws MethodNotAllowed, PreconditionFailed, NotFound { 165 String m = session.getMethod(); 166 167 int rc = 0; 168 if (map.containsKey(m)) { 169 for (var oc : map.get(m)) { 170 int mrc = oc.match(session); 171 if (mrc == 2) 172 return oc; 173 rc = Math.max(rc, mrc); 174 } 175 } 176 177 if (map.containsKey("*")) { 178 for (var oc : map.get("*")) { 179 int mrc = oc.match(session); 180 if (mrc == 2) 181 return oc; 182 rc = Math.max(rc, mrc); 183 } 184 } 185 186 // If no paths matched, see if the path matches any other methods. 187 // Note that we don't want to match against "/*" patterns such as getOptions(). 188 if (rc == 0) { 189 for (var oc : list) { 190 if (! oc.getPathPattern().endsWith("/*")) { 191 int orc = oc.match(session); 192 if (orc == 2) 193 throw methodNotAllowed(session); 194 } 195 } 196 } 197 198 if (rc == 1) 199 throw new PreconditionFailed("Method '%s' not found on resource on path '%s' with matching matcher.", m, session.getPathInfo()); 200 201 throw new NotFound("Java method matching path '%s' not found on resource '%s'.", session.getPathInfo(), cn(session.getResource())); 202 } 203 204 /** 205 * Returns the value of the <c>Allow</c> header for a <c>405 Method Not Allowed</c> response to the specified call. 206 * 207 * <p> 208 * The methods this resource answers on the call's path: every key of the operation table, other than 209 * <js>"*"</js>, with at least one operation whose path pattern matches, plus the request-line method when a 210 * <js>"*"</js> operation's path matches (the un-overridden request is always accepted there). The keys are what 211 * dispatch looks up, so a remote-proxy operation contributes <c>GET</c> and <c>POST</c>, never <c>RRPC</c>. 212 * 213 * <p> 214 * Only path patterns are tested. Matchers are not run and the session's cached path match is not touched, so the 215 * call can be made for any request without disturbing the operation that handles it. 216 * 217 * @param session The HTTP call. Must not be <jk>null</jk>. 218 * @return The sorted, comma-separated method names. Empty when nothing answers on the path. Never <jk>null</jk>. 219 * @since 10.0.0 220 */ 221 public String allowedMethods(RestSession session) { 222 var methods = map.entrySet().stream() 223 .filter(e -> e.getValue().stream().anyMatch(x -> x.matchesPath(session))) 224 .map(e -> eq(e.getKey(), "*") ? ucr(session.getRequest().getMethod()) : e.getKey()) 225 .collect(Collectors.toCollection(TreeSet::new)); 226 return String.join(", ", methods); 227 } 228 229 /** 230 * Creates the <c>405 Method Not Allowed</c> thrown when the call's path matches only operations under other 231 * methods, carrying the <c>Allow</c> header. 232 * 233 * @param session The HTTP call. Must not be <jk>null</jk>. 234 * @return A new exception. Never <jk>null</jk>. 235 */ 236 private MethodNotAllowed methodNotAllowed(RestSession session) { 237 var e = new MethodNotAllowed(); 238 e.setHeader("Allow", allowedMethods(session)); 239 return e; 240 } 241 242 /** 243 * Returns the list of method contexts in this object. 244 * 245 * @return An unmodifiable list of method contexts in this object. 246 */ 247 public List<RestOpContext> getOpContexts() { return u(l(list)); } 248}