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.caching;
018
019import java.util.Locale;
020import java.util.concurrent.ConcurrentHashMap;
021import java.util.concurrent.ConcurrentMap;
022
023import org.apache.wicket.core.util.resource.UrlResourceStream;
024import org.apache.wicket.core.util.resource.locator.IResourceNameIterator;
025import org.apache.wicket.core.util.resource.locator.IResourceStreamLocator;
026import org.apache.wicket.request.resource.ResourceReference.Key;
027import org.apache.wicket.util.lang.Args;
028import org.apache.wicket.util.resource.FileResourceStream;
029import org.apache.wicket.util.resource.IResourceStream;
030
031
032/**
033 * Locating resources can take a significant amount of time, especially since there are often
034 * several CSS, JavaScript and image resources on any given page. To facilitate localization and
035 * styling, Wicket will usually make several attempts at locating each resource (i.e. first with an
036 * "en_US" suffix, then "en", and so on); multiply these attempts by the number of resources on the
037 * page and this starts to add up.
038 * <p>
039 * This locator mitigates this problem by caching (indefinitely) references to
040 * {@link UrlResourceStream} and {@link FileResourceStream} objects as they are found, and
041 * {@link NullResourceStreamReference} for all which are missing so they are not looked up again and
042 * again.
043 */
044public class CachingResourceStreamLocator implements IResourceStreamLocator
045{
046        private final ConcurrentMap<CacheKey, IResourceStreamReference> cache;
047
048        private final IResourceStreamLocator delegate;
049
050        /**
051         * Construct.
052         * 
053         * @param resourceStreamLocator
054         *            the delegate
055         */
056        public CachingResourceStreamLocator(final IResourceStreamLocator resourceStreamLocator)
057        {
058                Args.notNull(resourceStreamLocator, "resourceStreamLocator");
059
060                delegate = resourceStreamLocator;
061
062                cache = new ConcurrentHashMap<>();
063        }
064
065        /**
066         * {@inheritDoc}
067         * 
068         * Checks for {@link IResourceStreamReference} in the cache and returns <code>null</code> if the
069         * result is {@link NullResourceStreamReference#INSTANCE}, or {@link FileResourceStream} /
070         * {@link UrlResourceStream} if there is an entry in the cache. Otherwise asks the delegate to
071         * find one and puts it in the cache.
072         */
073        @Override
074        public IResourceStream locate(Class<?> clazz, String path)
075        {
076                CacheKey key = new CacheKey(clazz.getName(), path, null, null, null, null, true);
077                IResourceStreamReference resourceStreamReference = cache.get(key);
078
079                final IResourceStream result;
080                if (resourceStreamReference == null)
081                {
082                        result = delegate.locate(clazz, path);
083
084                        updateCache(key, result);
085                }
086                else
087                {
088                        result = resourceStreamReference.getReference();
089                }
090
091                return result;
092        }
093
094        private void updateCache(CacheKey key, IResourceStream stream)
095        {
096                if (null == stream)
097                {
098                        cache.put(key, NullResourceStreamReference.INSTANCE);
099                }
100                else if (stream instanceof FileResourceStream)
101                {
102                        FileResourceStream fileResourceStream = (FileResourceStream)stream;
103                        cache.put(key, new FileResourceStreamReference(fileResourceStream));
104                }
105                else if (stream instanceof UrlResourceStream)
106                {
107                        UrlResourceStream urlResourceStream = (UrlResourceStream)stream;
108                        cache.put(key, new UrlResourceStreamReference(urlResourceStream));
109                }
110        }
111
112        @Override
113        public IResourceStream locate(Class<?> scope, String path, String style, String variation,
114                Locale locale, String extension, boolean strict)
115        {
116                CacheKey key = new CacheKey(scope.getName(), path, extension, locale, style, variation, strict);
117                IResourceStreamReference resourceStreamReference = cache.get(key);
118
119                final IResourceStream result;
120                if (resourceStreamReference == null)
121                {
122                        result = delegate.locate(scope, path, style, variation, locale, extension, strict);
123
124                        updateCache(key, result);
125                }
126                else
127                {
128                        result = resourceStreamReference.getReference();
129                }
130
131                return result;
132        }
133
134        @Override
135        public IResourceNameIterator newResourceNameIterator(String path, Locale locale, String style,
136                String variation, String extension, boolean strict)
137        {
138                return delegate.newResourceNameIterator(path, locale, style, variation, extension, strict);
139        }
140
141        /**
142         * Clears the resource cache.
143         * 
144         * @since 6.16.0
145         */
146        public void clearCache()
147        {
148                cache.clear();
149        }
150
151        /**
152         * A specialization of {@link org.apache.wicket.request.resource.ResourceReference.Key} that
153         * additionally takes the file extension into account
154         */
155        private static class CacheKey extends Key
156        {
157                private static final long serialVersionUID = 1L;
158
159                /**
160                 * The file extension
161                 */
162                private final String extension;
163
164                /** Whether the key was looked up using a strict matching search */
165                private final boolean strict;
166
167                private CacheKey(String scope, String name, String extension, Locale locale, String style, String variation, boolean strict)
168                {
169                        super(scope, name, locale, style, variation);
170
171                        this.extension = extension;
172                        this.strict = strict;
173                }
174
175                @Override
176                public int hashCode()
177                {
178                        final int prime = 31;
179                        int result = super.hashCode();
180                        result = prime * result + ((extension == null) ? 0 : extension.hashCode());
181                        result = prime * result + (strict ? 1231 : 1237);
182                        return result;
183                }
184
185                @Override
186                public boolean equals(Object obj)
187                {
188                        if (this == obj)
189                                return true;
190                        if (!super.equals(obj))
191                                return false;
192                        if (getClass() != obj.getClass())
193                                return false;
194                        CacheKey other = (CacheKey)obj;
195                        if (extension == null)
196                        {
197                                if (other.extension != null)
198                                        return false;
199                        }
200                        else if (!extension.equals(other.extension))
201                                return false;
202                        if (strict != other.strict)
203                                return false;
204                        return true;
205                }
206        }
207}