001/*
002 *  Copyright 2018 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.core.util.filereloader;
017
018import java.io.InputStream;
019import java.util.Map;
020import java.util.concurrent.ConcurrentHashMap;
021
022import org.apache.avalon.framework.activity.Initializable;
023import org.apache.avalon.framework.component.Component;
024import org.apache.avalon.framework.service.ServiceException;
025import org.apache.avalon.framework.service.ServiceManager;
026import org.apache.avalon.framework.service.Serviceable;
027import org.apache.excalibur.source.Source;
028import org.apache.excalibur.source.SourceNotFoundException;
029import org.apache.excalibur.source.SourceResolver;
030
031import org.ametys.core.cache.AbstractCacheManager;
032import org.ametys.core.cache.Cache;
033import org.ametys.runtime.i18n.I18nizableText;
034import org.ametys.runtime.plugin.component.AbstractLogEnabled;
035
036/**
037 * a helper to track modification of a configuration file
038 */
039public class FileReloaderUtils extends AbstractLogEnabled implements Component, Serviceable, Initializable
040{
041    /** The avalon role */
042    public static final String ROLE = FileReloaderUtils.class.getName();
043    
044    private static final String __FILERELOADER_CACHE = FileReloaderUtils.class.getName() + "$Cache";
045    
046    /** The source resolver */
047    protected SourceResolver _resolver;
048    
049    private AbstractCacheManager _cacheManager;
050    private Map<String, FileMetadatas> _files;
051
052    @Override
053    public void service(ServiceManager manager) throws ServiceException
054    {
055        _resolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
056        _cacheManager = (AbstractCacheManager) manager.lookup(AbstractCacheManager.ROLE);
057    }
058    
059    public void initialize() throws Exception
060    {
061        _files = new ConcurrentHashMap<>();
062        
063        _cacheManager.createRequestCache(__FILERELOADER_CACHE, 
064                new I18nizableText("plugin.core", "PLUGINS_CORE_FILERELOADER_CACHE_LABEL"),
065                new I18nizableText("plugin.core", "PLUGINS_CORE_FILERELOADER_CACHE_DESCRIPTION"),
066                true);
067    }
068    
069    /**
070     * Request to read/update the file
071     * @param sourceUrl url of the file to read
072     * @param fileReloader FileReloader that will be called to get an ID and read the file
073     * @throws Exception the source can not be read
074     */
075    public void updateFile(String sourceUrl, FileReloader fileReloader) throws Exception
076    {
077        updateFile(sourceUrl,  null, fileReloader);
078    }
079    
080    /**
081     * Request to read/update the file
082     * @param sourceUrl url of the file to read
083     * @param forceRead true to force reload even if the file was not modified
084     * @param fileReloader FileReloader that will be called to get an ID and read the file
085     * @throws Exception the source can not be read
086     */
087    public void updateFile(String sourceUrl, boolean forceRead, FileReloader fileReloader) throws Exception
088    {
089        updateFile(sourceUrl, null, forceRead, fileReloader);
090    }
091    
092    /**
093     * Request to read/update the file
094     * @param sourceUrl url of the file to read
095     * @param parameters parameters passed to the resolver to get the file via it's url (can be null)
096     * @param fileReloader FileReloader that will be called to get an ID and read the file
097     * @throws Exception the source can not be read
098     */
099    public void updateFile(String sourceUrl, Map parameters, FileReloader fileReloader) throws Exception
100    {
101        updateFile(sourceUrl, parameters, false, fileReloader);
102    }
103
104    /**
105     * Request to read/update the file
106     * @param sourceUrl url of the file to read
107     * @param parameters parameters passed to the resolver to get the file via it's url (can be null)
108     * @param forceRead true to force reload even if the file was not modified
109     * @param fileReloader FileReloader that will be called to get an ID and read the file
110     * @throws Exception the source can not be read
111     */
112    public void updateFile(String sourceUrl, Map parameters, boolean forceRead, FileReloader fileReloader) throws Exception
113    {
114        String id = fileReloader.getId(sourceUrl);
115        
116        Cache<String, Object> cache = _cacheManager.get(__FILERELOADER_CACHE);
117        if (!forceRead && cache.get(id) != null) // cache#get used here instead of cache#hasKey to trigger hit/miss stats
118        {
119            // already been handled in this request 
120            return;
121        }
122        
123        FileMetadatas fileMetadatas = _files.computeIfAbsent(id, i -> new FileMetadatas(sourceUrl, parameters));
124        synchronized (fileMetadatas)
125        {
126            Source source = null;
127            try
128            {
129                source = _resolver.resolveURI(fileMetadatas.getSourceUrl(), null, fileMetadatas.getParameters());
130                if (!source.exists())
131                {
132                    throw new SourceNotFoundException("Source '" + fileMetadatas.getSourceUrl() + "' not found");
133                }
134                
135                long sourceLastModified = (source.getLastModified() / 1000) * 1000; // hack for file systems only returning seconds
136                if (forceRead || sourceLastModified != fileMetadatas.getLastModified())
137                {
138                    getLogger().debug("Reading configuration file at {}", fileMetadatas.getSourceUrl());
139                    
140                    try (InputStream is = source.getInputStream())
141                    {
142                        fileReloader.updateFile(fileMetadatas.getSourceUrl(), source, is);
143                        fileMetadatas.setLastModified(sourceLastModified);
144                        getLogger().debug("Configuration file read. at {}", fileMetadatas.getSourceUrl());
145                    }
146                }
147            }
148            catch (SourceNotFoundException e)
149            {
150                if (forceRead || fileMetadatas.getLastModified() != FileMetadatas.FILE_DOES_NOT_EXIST)
151                {
152                    getLogger().debug("File at {} could not be found", fileMetadatas.getSourceUrl(), e);
153                    fileReloader.updateFile(fileMetadatas.getSourceUrl(), null, null);
154                }
155                
156                fileMetadatas.setLastModified(FileMetadatas.FILE_DOES_NOT_EXIST);
157            }
158            finally 
159            {
160                _resolver.release(source);
161                cache.put(id, id); // no matter the value, we only test the presence of the key
162            }
163        }
164    }
165    
166    private static class FileMetadatas
167    {
168        /** When the date was not set yet */
169        public static final long UNKNOWN = -1;
170        /** When there is no corresponding file */
171        public static final long FILE_DOES_NOT_EXIST = 0;
172        
173        private long _lastModified;
174        private String _sourceUrl;
175        private Map _parameters;
176
177        public FileMetadatas(String sourceUrl, Map parameters)
178        {
179            this._sourceUrl = sourceUrl;
180            this._lastModified = UNKNOWN;
181            this._parameters = parameters;
182        }
183        
184        public long getLastModified()
185        {
186            return this._lastModified;
187        }
188        public void setLastModified(long lastFileReading)
189        {
190            this._lastModified = lastFileReading;
191        }
192        
193        public String getSourceUrl()
194        {
195            return this._sourceUrl;
196        }
197        
198        public Map getParameters()
199        {
200            return this._parameters;
201        }
202    }
203
204}