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.resource;
018
019import java.io.BufferedInputStream;
020import java.io.IOException;
021import java.util.ArrayList;
022import java.util.Enumeration;
023import java.util.Iterator;
024import java.util.List;
025import java.util.Map;
026import java.util.concurrent.ConcurrentHashMap;
027
028import org.apache.wicket.core.util.resource.locator.IResourceStreamLocator;
029import org.apache.wicket.util.io.IOUtils;
030import org.apache.wicket.util.listener.IChangeListener;
031import org.apache.wicket.util.resource.IResourceStream;
032import org.apache.wicket.util.resource.ResourceStreamNotFoundException;
033import org.apache.wicket.util.value.ValueMap;
034import org.apache.wicket.util.watch.IModifiable;
035import org.apache.wicket.util.watch.IModificationWatcher;
036import org.apache.wicket.util.watch.ModificationWatcher;
037import org.slf4j.Logger;
038import org.slf4j.LoggerFactory;
039
040
041/**
042 * Default implementation of {@link IPropertiesFactory} which uses the
043 * {@link IResourceStreamLocator} as defined by
044 * {@link org.apache.wicket.settings.ResourceSettings#getResourceStreamLocator()}
045 * to load the {@link Properties} objects. Depending on the settings, it will assign
046 * {@link ModificationWatcher}s to the loaded resources to support reloading.
047 * 
048 * @see org.apache.wicket.settings.ResourceSettings#getPropertiesFactory()
049 * 
050 * @author Juergen Donnerstag
051 */
052public class PropertiesFactory implements IPropertiesFactory
053{
054        /** Log. */
055        private static final Logger log = LoggerFactory.getLogger(PropertiesFactory.class);
056
057        /** Listeners will be invoked after changes to property file have been detected */
058        private final List<IPropertiesChangeListener> afterReloadListeners = new ArrayList<>();
059
060        /** Cache for all property files loaded */
061        private final Map<String, Properties> propertiesCache = newPropertiesCache();
062
063        /** Provides the environment for properties factory */
064        private final IPropertiesFactoryContext context;
065
066        /** List of Properties Loader */
067        private final List<IPropertiesLoader> propertiesLoader;
068
069        /**
070         * Construct.
071         * 
072         * @param context
073         *            context for properties factory
074         */
075        public PropertiesFactory(final IPropertiesFactoryContext context)
076        {
077                this.context = context;
078                this.propertiesLoader = new ArrayList<>();
079                this.propertiesLoader.add(new IsoPropertiesFilePropertiesLoader("properties"));
080                this.propertiesLoader.add(new UtfPropertiesFilePropertiesLoader("utf8.properties", "utf-8"));
081                this.propertiesLoader.add(new XmlFilePropertiesLoader("properties.xml"));
082        }
083
084        /**
085         * Gets the {@link List} of properties loader. You may add or remove properties loaders at your
086         * will.
087         * 
088         * @return the {@link List} of properties loader
089         */
090        public List<IPropertiesLoader> getPropertiesLoaders()
091        {
092                return propertiesLoader;
093        }
094
095        /**
096         * @return new Cache implementation
097         */
098        protected Map<String, Properties> newPropertiesCache()
099        {
100                return new ConcurrentHashMap<>();
101        }
102
103        /**
104         * @see org.apache.wicket.resource.IPropertiesFactory#addListener(org.apache.wicket.resource.IPropertiesChangeListener)
105         */
106        @Override
107        public void addListener(final IPropertiesChangeListener listener)
108        {
109                // Make sure listeners are added only once
110                if (afterReloadListeners.contains(listener) == false)
111                {
112                        afterReloadListeners.add(listener);
113                }
114        }
115
116        /**
117         * @see org.apache.wicket.resource.IPropertiesFactory#clearCache()
118         */
119        @Override
120        public final void clearCache()
121        {
122                if (propertiesCache != null)
123                {
124                        propertiesCache.clear();
125                }
126
127                // clear the localizer cache as well
128                context.getLocalizer().clearCache();
129        }
130
131        @Override
132        public Properties load(final Class<?> clazz, final String path)
133        {
134                // Check the cache
135                Properties properties = null;
136                if (propertiesCache != null)
137                {
138                        properties = propertiesCache.get(path);
139                }
140
141                if (properties == null)
142                {
143                        Iterator<IPropertiesLoader> iter = propertiesLoader.iterator();
144                        while ((properties == null) && iter.hasNext())
145                        {
146                                IPropertiesLoader loader = iter.next();
147                                String fullPath = path + "." + loader.getFileExtension();
148
149                                // If not in the cache than try to load properties
150                                IResourceStream resourceStream = context.getResourceStreamLocator()
151                                        .locate(clazz, fullPath);
152                                if (resourceStream == null)
153                                {
154                                        continue;
155                                }
156
157                                // Watch file modifications
158                                final IModificationWatcher watcher = context.getResourceWatcher(true);
159                                if (watcher != null)
160                                {
161                                        addToWatcher(path, resourceStream, watcher);
162                                }
163
164                                ValueMap props = loadFromLoader(loader, resourceStream);
165                                if (props != null)
166                                {
167                                        properties = new Properties(path, props);
168                                }
169                        }
170
171                        // Cache the lookup
172                        if (propertiesCache != null)
173                        {
174                                if (properties == null)
175                                {
176                                        // Could not locate properties, store a placeholder
177                                        propertiesCache.put(path, Properties.EMPTY_PROPERTIES);
178                                }
179                                else
180                                {
181                                        propertiesCache.put(path, properties);
182                                }
183                        }
184                }
185
186                if (properties == Properties.EMPTY_PROPERTIES)
187                {
188                        // Translate empty properties placeholder to null prior to returning
189                        properties = null;
190                }
191
192                return properties;
193        }
194
195        /**
196         * 
197         * @param loader
198         * @param resourceStream
199         * @return properties
200         */
201        protected ValueMap loadFromLoader(final IPropertiesLoader loader,
202                final IResourceStream resourceStream)
203        {
204                if (log.isDebugEnabled())
205                {
206                        log.debug("Loading properties files from '{}' with loader '{}'", resourceStream, loader);
207                }
208
209                BufferedInputStream in = null;
210
211                try
212                {
213                        // Get the InputStream
214                        in = new BufferedInputStream(resourceStream.getInputStream());
215                        ValueMap data = loader.loadWicketProperties(in);
216                        if (data == null)
217                        {
218                                java.util.Properties props = loader.loadJavaProperties(in);
219                                if (props != null)
220                                {
221                                        // Copy the properties into the ValueMap
222                                        data = new ValueMap();
223                                        Enumeration<?> enumeration = props.propertyNames();
224                                        while (enumeration.hasMoreElements())
225                                        {
226                                                String property = (String)enumeration.nextElement();
227                                                data.put(property, props.getProperty(property));
228                                        }
229                                }
230                        }
231                        return data;
232                }
233                catch (ResourceStreamNotFoundException | IOException e)
234                {
235                        log.warn("Unable to find resource " + resourceStream, e);
236                }
237                finally
238                {
239                        IOUtils.closeQuietly(in);
240                        IOUtils.closeQuietly(resourceStream);
241                }
242
243                return null;
244        }
245
246        /**
247         * Add the resource stream to the file being watched
248         * 
249         * @param path
250         * @param resourceStream
251         * @param watcher
252         */
253        protected void addToWatcher(final String path, final IResourceStream resourceStream,
254                final IModificationWatcher watcher)
255        {
256                watcher.add(resourceStream, new IChangeListener<IModifiable>()
257                {
258                        @Override
259                        public void onChange(IModifiable modifiable)
260                        {
261                                log.info("A properties files has changed. Removing all entries " +
262                                        "from the cache. Resource: " + resourceStream);
263
264                                // Clear the whole cache as associated localized files may
265                                // be affected and may need reloading as well.
266                                clearCache();
267
268                                // Inform all listeners
269                                for (IPropertiesChangeListener listener : afterReloadListeners)
270                                {
271                                        try
272                                        {
273                                                listener.propertiesChanged(path);
274                                        }
275                                        catch (Exception ex)
276                                        {
277                                                PropertiesFactory.log.error("PropertiesReloadListener has thrown an exception: " +
278                                                        ex.getMessage());
279                                        }
280                                }
281                        }
282                });
283        }
284
285        /**
286         * For subclasses to get access to the cache
287         * 
288         * @return Map
289         */
290        protected final Map<String, Properties> getCache()
291        {
292                return propertiesCache;
293        }
294}