001// ***************************************************************************************************************************
002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
003// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
005// * with the License.  You may obtain a copy of the License at                                                              *
006// *                                                                                                                         *
007// *  http://www.apache.org/licenses/LICENSE-2.0                                                                             *
008// *                                                                                                                         *
009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an  *
010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
011// * specific language governing permissions and limitations under the License.                                              *
012// ***************************************************************************************************************************
013package org.apache.juneau.rest;
014
015import static org.apache.juneau.internal.CollectionUtils.*;
016import static org.apache.juneau.internal.StringUtils.*;
017import static org.apache.juneau.internal.StateMachineState.*;
018import static java.lang.Character.*;
019
020import java.util.*;
021
022import org.apache.juneau.*;
023import org.apache.juneau.internal.*;
024import org.apache.juneau.json.*;
025import org.apache.juneau.parser.*;
026import org.apache.juneau.rest.annotation.*;
027import org.apache.juneau.utils.*;
028
029/**
030 * Static file mapping.
031 *
032 * <p>
033 * Used to define paths and locations of statically-served files such as images or HTML documents.
034 *
035 * <p>
036 * An example where this class is used is in the {@link Rest#staticFiles} annotation:
037 * <p class='bcode w800'>
038 * <jk>package</jk> com.foo.mypackage;
039 *
040 * <ja>@Rest</ja>(
041 *    path=<js>"/myresource"</js>,
042 *    staticFiles={<js>"htdocs:docs"</js>}
043 * )
044 * <jk>public class</jk> MyResource <jk>extends</jk> BasicRestServlet {...}
045 * </p>
046 *
047 * <p>
048 * Static files are found by using the {@link ClasspathResourceFinder} defined on the resource.
049 *
050 * <p>
051 * In the example above, given a GET request to <l>/myresource/htdocs/foobar.html</l>, the servlet will attempt to find
052 * the <l>foobar.html</l> file in the following ordered locations:
053 * <ol>
054 *    <li><l>com.foo.mypackage.docs</l> package.
055 *    <li><l>org.apache.juneau.rest.docs</l> package (since <l>BasicRestServlet</l> is in <l>org.apache.juneau.rest</l>).
056 *    <li><l>[working-dir]/docs</l> directory.
057 * </ol>
058 *
059 * <ul class='notes'>
060 *    <li>
061 *       Mappings are cumulative from parent to child.  Child resources can override mappings made on parent resources.
062 *    <li>
063 *       The media type on the response is determined by the {@link org.apache.juneau.rest.RestContext#getMediaTypeForName(String)} method.
064 * </ul>
065 *
066 * <ul class='seealso'>
067 *    <li class='link'>{@doc juneau-rest-server.StaticFiles}
068 * </ul>
069 */
070public class StaticFileMapping {
071
072   final Class<?> resourceClass;
073   final String path, location;
074   final Map<String,Object> responseHeaders;
075
076   /**
077    * Constructor.
078    *
079    * @param resourceClass
080    *    The resource/servlet class which serves as the base location of the location below.
081    * @param path
082    *    The mapped URI path.
083    *    <br>Leading and trailing slashes are trimmed.
084    * @param location
085    *    The location relative to the resource class.
086    *    <br>Leading and trailing slashes are trimmed.
087    * @param responseHeaders
088    *    The response headers.
089    *    Can be <jk>null</jk>.
090    */
091   public StaticFileMapping(Class<?> resourceClass, String path, String location, Map<String,Object> responseHeaders) {
092      this.resourceClass = resourceClass;
093      this.path = trimSlashes(path);
094      this.location = trimTrailingSlashes(location);
095      this.responseHeaders = immutableMap(responseHeaders);
096   }
097
098   /**
099    * Create one or more <c>StaticFileMappings</c> from the specified comma-delimited list of mappings.
100    *
101    * <p>
102    * Mapping string must be one of these formats:
103    * <ul>
104    *    <li><js>"path:location"</js> (e.g. <js>"foodocs:docs/foo"</js>)
105    *    <li><js>"path:location:headers-json"</js> (e.g. <js>"foodocs:docs/foo:{'Cache-Control':'max-age=86400, public'}"</js>)
106    * </ul>
107    *
108    * @param resourceClass
109    *    The resource/servlet class which serves as the base location of the location below.
110    * @param mapping
111    *    The mapping string that represents the path/location mapping.
112    *    <br>Leading and trailing slashes and whitespace are trimmed from path and location.
113    * @return A list of parsed mappings.  Never <jk>null</jk>.
114    * @throws ParseException If mapping was malformed.
115    */
116   public static List<StaticFileMapping> parse(Class<?> resourceClass, String mapping) throws ParseException {
117
118      mapping = trim(mapping);
119      if (isEmpty(mapping))
120         return Collections.emptyList();
121
122      // States:
123      // S01 = In path, looking for :
124      // S02 = Found path:, looking for : or , or end
125      // S03 = Found path:location:, looking for {
126      // S04 = Found path:location:{, looking for }
127      // S05 = Found path:location:{headers}, looking for , or end
128      // S06 = Found path:location:{headers} , looking for start of path
129
130      StateMachineState state = S01;
131      int mark = 0;
132
133      String path = null, location = null;
134      List<StaticFileMapping> l = new ArrayList<>();
135      String s = mapping;
136      int jsonDepth = 0;
137      for (int i = 0; i < s.length(); i++) {
138         char c = s.charAt(i);
139         if (state == S01) {
140            if (c == ':') {
141               path = trim(s.substring(mark, i));
142               mark = i+1;
143               state = S02;
144            }
145         } else if (state == S02) {
146            if (c == ':') {
147               location = trim(s.substring(mark, i));
148               mark = i+1;
149               state = S03;
150            } else if (c == ',') {
151               location = trim(s.substring(mark, i));
152               l.add(new StaticFileMapping(resourceClass, path, location, null));
153               mark = i+1;
154               state = S01;
155               path = null;
156               location = null;
157            }
158         } else if (state == S03) {
159            if (c == '{') {
160               mark = i;
161               state = S04;
162            } else if (! isWhitespace(c)) {
163               throw new ParseException("Invalid staticFiles mapping.  Expected '{' at beginning of headers.  mapping=[{0}]", mapping);
164            }
165         } else if (state == S04) {
166            if (c == '{') {
167               jsonDepth++;
168            } else if (c == '}') {
169               if (jsonDepth > 0) {
170                  jsonDepth--;
171               } else {
172                  String json = s.substring(mark, i+1);
173                  l.add(new StaticFileMapping(resourceClass, path, location, new ObjectMap(json)));
174                  state = S05;
175                  path = null;
176                  location = null;
177               }
178            }
179         } else if (state == S05) {
180            if (c == ',') {
181               state = S06;
182            } else if (! isWhitespace(c)) {
183               throw new ParseException("Invalid staticFiles mapping.  Invalid text following headers.  mapping=[{0}]", mapping);
184            }
185         } else /* state == S06 */ {
186            if (! isWhitespace(c)) {
187               mark = i;
188               state = S01;
189            }
190         }
191      }
192
193      if (state == S01) {
194         throw new ParseException("Invalid staticFiles mapping.  Couldn''t find '':'' following path.  mapping=[{0}]", mapping);
195      } else if (state == S02) {
196         location = trim(s.substring(mark, s.length()));
197         l.add(new StaticFileMapping(resourceClass, path, location, null));
198      } else if (state == S03) {
199         throw new ParseException("Invalid staticFiles mapping.  Found extra '':'' following location.  mapping=[{0}]", mapping);
200      } else if (state == S04) {
201         throw new ParseException("Invalid staticFiles mapping.  Malformed headers.  mapping=[{0}]", mapping);
202      }
203
204      return l;
205   }
206
207   //-----------------------------------------------------------------------------------------------------------------
208   // Other methods
209   //-----------------------------------------------------------------------------------------------------------------
210
211   @Override /* Object */
212   public String toString() {
213      return SimpleJsonSerializer.DEFAULT_READABLE.toString(toMap());
214   }
215
216   /**
217    * Returns the properties defined on this bean as a simple map for debugging purposes.
218    *
219    * @return A new map containing the properties defined on this bean.
220    */
221   public ObjectMap toMap() {
222      return new DefaultFilteringObjectMap()
223         .append("resourceClass", resourceClass)
224         .append("path", path)
225         .append("location", location)
226         .append("responseHeaders", responseHeaders)
227      ;
228   }
229}