11/*
2- * Copyright (c) 2005, 2025 , Oracle and/or its affiliates. All rights reserved.
2+ * Copyright (c) 2005, 2026 , Oracle and/or its affiliates. All rights reserved.
33 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44 *
55 * This code is free software; you can redistribute it and/or modify it
2626package sun .net .httpserver ;
2727
2828import java .util .*;
29+ import java .util .function .BiPredicate ;
2930
3031class ContextList {
3132
33+ private static final System .Logger LOGGER = System .getLogger (ContextList .class .getName ());
34+
3235 private final LinkedList <HttpContextImpl > list = new LinkedList <>();
3336
3437 public synchronized void add (HttpContextImpl ctx ) {
38+ assert ctx != null ;
39+ // `findContext(String protocol, String path, ContextPathMatcher matcher)`
40+ // expects the protocol to be lower-cased using ROOT locale, hence:
41+ assert ctx .getProtocol ().equals (ctx .getProtocol ().toLowerCase (Locale .ROOT ));
3542 assert ctx .getPath () != null ;
43+ // `ContextPathMatcher` expects context paths to be non-empty:
44+ assert !ctx .getPath ().isEmpty ();
3645 if (contains (ctx )) {
3746 throw new IllegalArgumentException ("cannot add context to list" );
3847 }
3948 list .add (ctx );
4049 }
4150
4251 boolean contains (HttpContextImpl ctx ) {
43- return findContext (ctx .getProtocol (), ctx .getPath (), true ) != null ;
52+ return findContext (ctx .getProtocol (), ctx .getPath (), ContextPathMatcher . EXACT ) != null ;
4453 }
4554
4655 public synchronized int size () {
4756 return list .size ();
4857 }
4958
50- /* initially contexts are located only by protocol:path.
51- * Context with longest prefix matches (currently case-sensitive)
59+ /**
60+ * {@return the context with the longest case-sensitive prefix match}
61+ *
62+ * @param protocol the request protocol
63+ * @param path the request path
5264 */
53- synchronized HttpContextImpl findContext (String protocol , String path ) {
54- return findContext (protocol , path , false );
65+ HttpContextImpl findContext (String protocol , String path ) {
66+ var matcher = ContextPathMatcher .ofConfiguredPrefixPathMatcher ();
67+ return findContext (protocol , path , matcher );
5568 }
5669
57- synchronized HttpContextImpl findContext (String protocol , String path , boolean exact ) {
70+ private synchronized HttpContextImpl findContext (String protocol , String path , ContextPathMatcher matcher ) {
5871 protocol = protocol .toLowerCase (Locale .ROOT );
5972 String longest = "" ;
6073 HttpContextImpl lc = null ;
@@ -63,9 +76,7 @@ synchronized HttpContextImpl findContext(String protocol, String path, boolean e
6376 continue ;
6477 }
6578 String cpath = ctx .getPath ();
66- if (exact && !cpath .equals (path )) {
67- continue ;
68- } else if (!exact && !path .startsWith (cpath )) {
79+ if (!matcher .test (cpath , path )) {
6980 continue ;
7081 }
7182 if (cpath .length () > longest .length ()) {
@@ -76,10 +87,174 @@ synchronized HttpContextImpl findContext(String protocol, String path, boolean e
7687 return lc ;
7788 }
7889
90+ private enum ContextPathMatcher implements BiPredicate <String , String > {
91+
92+ /**
93+ * Tests if both the request path and the context path are identical.
94+ */
95+ EXACT (String ::equals ),
96+
97+ /**
98+ * Tests <em>string prefix matches</em> where the request path string
99+ * starts with the context path string.
100+ *
101+ * <h3>Examples</h3>
102+ *
103+ * <table>
104+ * <thead>
105+ * <tr>
106+ * <th rowspan="2">Context path</th>
107+ * <th colspan="4">Request path</th>
108+ * </tr>
109+ * <tr>
110+ * <th>/foo</th>
111+ * <th>/foo/</th>
112+ * <th>/foo/bar</th>
113+ * <th>/foobar</th>
114+ * </tr>
115+ * </thead>
116+ * <tbody>
117+ * <tr>
118+ * <td>/</td>
119+ * <td>Y</td>
120+ * <td>Y</td>
121+ * <td>Y</td>
122+ * <td>Y</td>
123+ * </tr>
124+ * <tr>
125+ * <td>/foo</td>
126+ * <td>Y</td>
127+ * <td>Y</td>
128+ * <td>Y</td>
129+ * <td>Y</td>
130+ * </tr>
131+ * <tr>
132+ * <td>/foo/</td>
133+ * <td>N</td>
134+ * <td>Y</td>
135+ * <td>Y</td>
136+ * <td>N</td>
137+ * </tr>
138+ * </tbody>
139+ * </table>
140+ */
141+ STRING_PREFIX ((contextPath , requestPath ) -> requestPath .startsWith (contextPath )),
142+
143+ /**
144+ * Tests <em>path prefix matches</em> where path segments must have an
145+ * exact match.
146+ *
147+ * <h3>Examples</h3>
148+ *
149+ * <table>
150+ * <thead>
151+ * <tr>
152+ * <th rowspan="2">Context path</th>
153+ * <th colspan="4">Request path</th>
154+ * </tr>
155+ * <tr>
156+ * <th>/foo</th>
157+ * <th>/foo/</th>
158+ * <th>/foo/bar</th>
159+ * <th>/foobar</th>
160+ * </tr>
161+ * </thead>
162+ * <tbody>
163+ * <tr>
164+ * <td>/</td>
165+ * <td>Y</td>
166+ * <td>Y</td>
167+ * <td>Y</td>
168+ * <td>Y</td>
169+ * </tr>
170+ * <tr>
171+ * <td>/foo</td>
172+ * <td>Y</td>
173+ * <td>Y</td>
174+ * <td>Y</td>
175+ * <td>N</td>
176+ * </tr>
177+ * <tr>
178+ * <td>/foo/</td>
179+ * <td>N</td>
180+ * <td>Y</td>
181+ * <td>Y</td>
182+ * <td>N</td>
183+ * </tr>
184+ * </tbody>
185+ * </table>
186+ */
187+ PATH_PREFIX ((contextPath , requestPath ) -> {
188+
189+ // Fast-path for `/`
190+ if ("/" .equals (contextPath )) {
191+ return true ;
192+ }
193+
194+ // Does the request path prefix match?
195+ if (requestPath .startsWith (contextPath )) {
196+
197+ // Is it an exact match?
198+ int contextPathLength = contextPath .length ();
199+ if (requestPath .length () == contextPathLength ) {
200+ return true ;
201+ }
202+
203+ // Is it a path-prefix match?
204+ assert contextPathLength > 0 ;
205+ return
206+ // Case 1: The request path starts with the context
207+ // path, but the context path has an extra path
208+ // separator suffix. For instance, the context path is
209+ // `/foo/` and the request path is `/foo/bar`.
210+ contextPath .charAt (contextPathLength - 1 ) == '/' ||
211+ // Case 2: The request path starts with the
212+ // context path, but the request path has an
213+ // extra path separator suffix. For instance,
214+ // context path is `/foo` and the request path
215+ // is `/foo/` or `/foo/bar`.
216+ requestPath .charAt (contextPathLength ) == '/' ;
217+
218+ }
219+
220+ return false ;
221+
222+ });
223+
224+ private final BiPredicate <String , String > predicate ;
225+
226+ ContextPathMatcher (BiPredicate <String , String > predicate ) {
227+ this .predicate = predicate ;
228+ }
229+
230+ @ Override
231+ public boolean test (String contextPath , String requestPath ) {
232+ return predicate .test (contextPath , requestPath );
233+ }
234+
235+ private static ContextPathMatcher ofConfiguredPrefixPathMatcher () {
236+ var propertyName = "sun.net.httpserver.pathMatcher" ;
237+ var propertyValueDefault = "pathPrefix" ;
238+ var propertyValue = System .getProperty (propertyName , propertyValueDefault );
239+ return switch (propertyValue ) {
240+ case "pathPrefix" -> ContextPathMatcher .PATH_PREFIX ;
241+ case "stringPrefix" -> ContextPathMatcher .STRING_PREFIX ;
242+ default -> {
243+ LOGGER .log (
244+ System .Logger .Level .WARNING ,
245+ "System property \" {}\" contains an invalid value: \" {}\" . Falling back to the default: \" {}\" " ,
246+ propertyName , propertyValue , propertyValueDefault );
247+ yield ContextPathMatcher .PATH_PREFIX ;
248+ }
249+ };
250+ }
251+
252+ }
253+
79254 public synchronized void remove (String protocol , String path )
80255 throws IllegalArgumentException
81256 {
82- HttpContextImpl ctx = findContext (protocol , path , true );
257+ HttpContextImpl ctx = findContext (protocol , path , ContextPathMatcher . EXACT );
83258 if (ctx == null ) {
84259 throw new IllegalArgumentException ("cannot remove element from list" );
85260 }
0 commit comments