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}