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.mcp.v20260728;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.net.*;
022import java.util.*;
023
024/**
025 * Pure, side-effect-free construction of the RFC 9728 / SEP-2351 well-known Protected Resource Metadata (PRM) URIs for a
026 * given canonical resource identifier.
027 *
028 * <p>
029 * Per <a class="doclink" href="https://datatracker.ietf.org/doc/html/rfc9728#section-3.1">RFC 9728 &sect;3.1</a> and
030 * SEP-2351, the well-known segment {@code /.well-known/oauth-protected-resource} is inserted between the host and the
031 * resource's path component:
032 *
033 * <ul>
034 *    <li>Resource {@code https://host/mcp} &rarr; PRM at {@code https://host/.well-known/oauth-protected-resource/mcp}
035 *       (path-insertion).
036 *    <li>Resource {@code https://host} (no path) &rarr; PRM at {@code https://host/.well-known/oauth-protected-resource}
037 *       (root form).
038 * </ul>
039 *
040 * <p>
041 * {@link #candidates(URI)} returns the ordered client lookup sequence &mdash; the path-inserted URI first, then the root
042 * fallback &mdash; matching the ordered fallback SEP-2351 specifies.  The server hosts the path-inserted form (and, for
043 * a rooted resource, that <i>is</i> the root form).
044 *
045 * @since 10.0.0
046 */
047public final class McpWellKnownRouting {
048
049   /** The RFC 9728 well-known path segment. */
050   public static final String WELL_KNOWN_PATH = "/.well-known/oauth-protected-resource";
051
052   private McpWellKnownRouting() {}
053
054   /**
055    * Returns the path-inserted PRM URI for the supplied resource identifier (RFC 9728 &sect;3.1).
056    *
057    * @param resource The canonical resource identifier.  Must not be <jk>null</jk> and must be absolute (scheme +
058    *    authority).
059    * @return The PRM document URI.
060    * @throws IllegalArgumentException If {@code resource} is <jk>null</jk> or is missing a scheme/authority.
061    */
062   public static URI metadataUri(URI resource) {
063      var base = baseOf(resource);
064      return URI.create(base + WELL_KNOWN_PATH + normalizePath(resource.getRawPath()));
065   }
066
067   /**
068    * Returns the root-form PRM URI for the supplied resource identifier &mdash; the ordered fallback location
069    * (SEP-2351).
070    *
071    * @param resource The canonical resource identifier.  Must not be <jk>null</jk> and must be absolute (scheme +
072    *    authority).
073    * @return The root PRM document URI.
074    * @throws IllegalArgumentException If {@code resource} is <jk>null</jk> or is missing a scheme/authority.
075    */
076   public static URI rootMetadataUri(URI resource) {
077      return URI.create(baseOf(resource) + WELL_KNOWN_PATH);
078   }
079
080   /**
081    * Returns the ordered PRM lookup candidates &mdash; the path-inserted URI first, then the root fallback (SEP-2351).
082    *
083    * <p>
084    * When the resource has no path component the two coincide and a single-element list is returned.
085    *
086    * @param resource The canonical resource identifier.  Must not be <jk>null</jk> and must be absolute (scheme +
087    *    authority).
088    * @return The ordered candidate list (1 or 2 entries).  Never <jk>null</jk>.
089    * @throws IllegalArgumentException If {@code resource} is <jk>null</jk> or is missing a scheme/authority.
090    */
091   public static List<URI> candidates(URI resource) {
092      var pathInserted = metadataUri(resource);
093      var root = rootMetadataUri(resource);
094      return pathInserted.equals(root) ? List.of(pathInserted) : List.of(pathInserted, root);
095   }
096
097   /**
098    * Returns the servlet-relative request path at which this server hosts its PRM document for the supplied resource
099    * identifier &mdash; i.e. {@link #metadataUri(URI)}'s path component.
100    *
101    * @param resource The canonical resource identifier.  Must not be <jk>null</jk>.
102    * @return The request path (e.g. {@code /.well-known/oauth-protected-resource/mcp}).
103    * @throws IllegalArgumentException If {@code resource} is <jk>null</jk>.
104    */
105   public static String wellKnownRequestPath(URI resource) {
106      reqnn("resource", resource);
107      return WELL_KNOWN_PATH + normalizePath(resource.getRawPath());
108   }
109
110   private static String baseOf(URI resource) {
111      reqnn("resource", resource);
112      var scheme = resource.getScheme();
113      var authority = resource.getRawAuthority();
114      if (scheme == null || authority == null)
115         throw iaex("resource %s must be an absolute URI with a scheme and authority", resource);
116      return scheme + "://" + authority;
117   }
118
119   private static String normalizePath(String rawPath) {
120      if (ie(rawPath))
121         return "";
122      var p = rawPath;
123      while (p.endsWith("/"))
124         p = p.substring(0, p.length() - 1);
125      if (p.isEmpty())
126         return "";
127      return p.startsWith("/") ? p : "/" + p;
128   }
129}