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}