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.wicket.core.util.resource.locator;
018
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.Collections;
022import java.util.List;
023import java.util.Locale;
024
025import org.apache.wicket.Application;
026import org.apache.wicket.core.util.file.WebApplicationPath;
027import org.apache.wicket.util.file.IResourceFinder;
028import org.apache.wicket.util.file.Path;
029import org.apache.wicket.util.resource.IResourceStream;
030import org.apache.wicket.util.resource.ResourceUtils;
031import org.apache.wicket.util.resource.ResourceUtils.PathLocale;
032import org.apache.wicket.util.string.Strings;
033import org.slf4j.Logger;
034import org.slf4j.LoggerFactory;
035
036
037/**
038 * Locates Wicket resources.
039 *
040 * <p>
041 * Contains the logic to locate a resource based on a path, a variation, a style (see
042 * {@link org.apache.wicket.Session}), a locale and an extension string. The full filename will be
043 * built like: &lt;path&gt;_&lt;variation&gt;_&lt;style&gt;_&lt;locale&gt;.&lt;extension&gt;.
044 * <p>
045 * Resource matches will be attempted in the following order:
046 * <ol>
047 * <li>&lt;path&gt;_&lt;variation&gt;_&lt;style&gt;_&lt;locale&gt;.&lt;extension&gt;</li>
048 * <li>&lt;path&gt;_&lt;style&gt;_&lt;locale&gt;.&lt;extension&gt;</li>
049 * <li>&lt;path&gt;_&lt;locale&gt;.&lt;extension&gt;</li>
050 * <li>&lt;path&gt;_&lt;style&gt;.&lt;extension&gt;</li>
051 * <li>&lt;path&gt;.&lt;extension&gt;</li>
052 * </ol>
053 * <p>
054 * Locales may contain a language, a country and a region or variant. Combinations of these
055 * components will be attempted in the following order:
056 * <ol>
057 * <li>locale.toString() see javadoc for Locale for more details</li>
058 * <li>&lt;language&gt;_&lt;country&gt;</li>
059 * <li>&lt;language&gt;</li>
060 * </ol>
061 * <p>
062 * Resources will be actually loaded by the {@link IResourceFinder}s defined in the resource
063 * settings. By default there are finders that look in the classpath and in the classpath in
064 * META-INF/resources. You can add more by adding {@link WebApplicationPath}s or {@link Path}s to
065 * {@link org.apache.wicket.settings.ResourceSettings#getResourceFinders()}.
066 * 
067 * @author Juergen Donnerstag
068 * @author Jonathan Locke
069 */
070public class ResourceStreamLocator implements IResourceStreamLocator
071{
072        /** Logging */
073        private static final Logger log = LoggerFactory.getLogger(ResourceStreamLocator.class);
074
075        private static final Iterable<String> NO_EXTENSIONS = new ArrayList<>(0);
076
077        /** If null, the application registered finder will be used */
078        private List<IResourceFinder> finders;
079
080        /**
081         * Constructor
082         */
083        public ResourceStreamLocator()
084        {
085                this((List<IResourceFinder>)null);
086        }
087
088        /**
089         * Constructor
090         * 
091         * @param finders
092         *            resource finders. These will be tried in the given order.
093         */
094        public ResourceStreamLocator(final IResourceFinder... finders)
095        {
096                this(Arrays.asList(finders));
097        }
098
099        /**
100         * Constructor
101         * 
102         * @param finders
103         *            resource finders. These will be tried in the given order.
104         */
105        public ResourceStreamLocator(final List<IResourceFinder> finders)
106        {
107                this.finders = finders;
108        }
109
110        /**
111         * 
112         * @see org.apache.wicket.core.util.resource.locator.IResourceStreamLocator#locate(java.lang.Class,
113         *      java.lang.String)
114         */
115        @Override
116        public IResourceStream locate(final Class<?> clazz, final String path)
117        {
118                // First try with the resource finder registered with the application
119                // (allows for markup reloading)
120                if (finders == null)
121                {
122                        finders = Application.get().getResourceSettings().getResourceFinders();
123                }
124
125                IResourceStream result;
126                for (IResourceFinder finder : finders)
127                {
128                        log.debug("Attempting to locate resource '{}' using finder'{}'", path, finder);
129                        result = finder.find(clazz, path);
130                        if (result != null)
131                        {
132                                return result;
133                        }
134                }
135                return null;
136        }
137
138        /**
139         * 
140         * @see org.apache.wicket.core.util.resource.locator.IResourceStreamLocator#locate(java.lang.Class,
141         *      java.lang.String, java.lang.String, java.lang.String, java.util.Locale,
142         *      java.lang.String, boolean)
143         */
144        @Override
145        public IResourceStream locate(final Class<?> clazz, String path, final String style,
146                final String variation, Locale locale, final String extension, final boolean strict)
147        {
148                // If path contains a locale, then it'll replace the locale provided to this method
149                PathLocale data = ResourceUtils.getLocaleFromFilename(path);
150                if ((data != null) && (data.locale != null))
151                {
152                        path = data.path;
153                        locale = data.locale;
154                }
155
156                // Try the various combinations of style, locale and extension to find the resource.
157                IResourceNameIterator iter = newResourceNameIterator(path, locale, style, variation,
158                        extension, strict);
159                while (iter.hasNext())
160                {
161                        String newPath = iter.next();
162
163                        IResourceStream stream = locate(clazz, newPath);
164                        if (stream != null)
165                        {
166                                stream.setLocale(iter.getLocale());
167                                stream.setStyle(iter.getStyle());
168                                stream.setVariation(iter.getVariation());
169                                return stream;
170                        }
171                }
172
173                return null;
174        }
175
176        /**
177         * 
178         * @see org.apache.wicket.core.util.resource.locator.IResourceStreamLocator#newResourceNameIterator(java.lang.String,
179         *      java.util.Locale, java.lang.String, java.lang.String, java.lang.String, boolean)
180         */
181        @Override
182        public IResourceNameIterator newResourceNameIterator(final String path, final Locale locale,
183                final String style, final String variation, final String extension, final boolean strict)
184        {
185                final Iterable<String> extensions;
186
187                final String realPath;
188
189                if ((extension == null) && (path != null) && (path.indexOf('.') != -1))
190                {
191                        // extract the path and extension
192                        realPath = Strings.beforeLast(path, '.');
193                        String realExtension = Strings.afterLast(path, '.');
194                        if (realExtension.indexOf(',') > -1)
195                        {
196                                // multiple extensions are not allowed in the path parameter
197                                // it could be an attack, so ignore it and pretend there are no resources
198                                return new EmptyResourceNameIterator();
199                        }
200
201                        // add a minimized file to the resource lookup if necessary
202                        if (Application.exists() &&
203                                Application.get().getResourceSettings().getUseMinifiedResources())
204                        {
205                                extensions = Arrays.asList("min." + realExtension, realExtension);
206                        }
207                        else
208                        {
209                                extensions = Collections.singleton(realExtension);
210                        }
211                }
212                else
213                {
214                        realPath = path;
215                        if (extension == null)
216                        {
217                                extensions = NO_EXTENSIONS;
218                        }
219                        else
220                        {
221                                String[] commaSeparated = Strings.split(extension, ',');
222                                List<String> nonMinifiedExtensions = Arrays.asList(commaSeparated);
223
224                                // add a minimized file to the resource lookup if necessary
225                                if (Application.exists() &&
226                                        Application.get().getResourceSettings().getUseMinifiedResources())
227                                {
228                                        ArrayList<String> minifiedExtensions = new ArrayList<>();
229                                        for (String nonMinifiedExtension : nonMinifiedExtensions)
230                                        {
231                                                minifiedExtensions.add("min." + nonMinifiedExtension);
232                                                minifiedExtensions.add(nonMinifiedExtension);
233                                        }
234                                        extensions = minifiedExtensions;
235                                }
236                                else
237                                {
238                                        extensions = nonMinifiedExtensions;
239                                }
240                        }
241                }
242
243                return newResourceNameIterator(realPath, locale, style, variation, extensions, strict);
244        }
245
246        // TODO Wicket 7 Add this method to IResourceStreamLocator interface.
247        public IResourceNameIterator newResourceNameIterator(final String path, final Locale locale,
248                final String style, final String variation, final Iterable<String> extensions, final boolean strict)
249        {
250                return new ResourceNameIterator(path, style, variation, locale, extensions, strict);
251        }
252}