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.time.Instant;
020import java.time.temporal.ChronoUnit;
021import java.util.HashMap;
022import java.util.Map;
023
024import org.apache.avalon.framework.activity.Initializable;
025import org.apache.avalon.framework.component.Component;
026import org.apache.avalon.framework.service.ServiceException;
027import org.apache.avalon.framework.service.ServiceManager;
028import org.apache.avalon.framework.service.Serviceable;
029import org.apache.excalibur.source.Source;
030import org.apache.excalibur.source.SourceNotFoundException;
031import org.apache.excalibur.source.SourceResolver;
032
033import org.ametys.runtime.plugin.component.AbstractLogEnabled;
034
035/**
036 * a helper to track modification of a configuration file
037 */
038public class FileReloaderUtils extends AbstractLogEnabled implements Component, Serviceable, Initializable
039{
040    /** The avalon role */
041    public static final String ROLE = FileReloaderUtils.class.getName();
042    
043    /** The source resolver */
044    protected SourceResolver _resolver;
045    
046    private Map<String, FileMetadatas> _files;
047
048    public void initialize() throws Exception
049    {
050        this._files = new HashMap<>();
051    }
052    
053    @Override
054    public void service(ServiceManager manager) throws ServiceException
055    {
056        _resolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
057    }
058    
059    /**
060     * Request to read/update the file
061     * @param sourceUrl url of the file to read
062     * @param fileReloader FileReloader that will be called to get an ID and read the file
063     * @throws Exception the source can not be read
064     */
065    public void updateFile(String sourceUrl, FileReloader fileReloader) throws Exception
066    {
067        updateFile(sourceUrl,  null, fileReloader);
068    }
069    
070    /**
071     * Request to read/update the file
072     * @param sourceUrl url of the file to read
073     * @param forceRead true to force reload even if the file was not modified
074     * @param fileReloader FileReloader that will be called to get an ID and read the file
075     * @throws Exception the source can not be read
076     */
077    public void updateFile(String sourceUrl, boolean forceRead, FileReloader fileReloader) throws Exception
078    {
079        updateFile(sourceUrl, null, forceRead, fileReloader);
080    }
081    
082    /**
083     * Request to read/update the file
084     * @param sourceUrl url of the file to read
085     * @param parameters parameters passed to the resolver to get the file via it's url (can be null)
086     * @param fileReloader FileReloader that will be called to get an ID and read the file
087     * @throws Exception the source can not be read
088     */
089    public void updateFile(String sourceUrl, Map parameters, FileReloader fileReloader) throws Exception
090    {
091        updateFile(sourceUrl, parameters, false, fileReloader);
092    }
093
094    /**
095     * Request to read/update the file
096     * @param sourceUrl url of the file to read
097     * @param parameters parameters passed to the resolver to get the file via it's url (can be null)
098     * @param forceRead true to force reload even if the file was not modified
099     * @param fileReloader FileReloader that will be called to get an ID and read the file
100     * @throws Exception the source can not be read
101     */
102    public void updateFile(String sourceUrl, Map parameters, boolean forceRead, FileReloader fileReloader) throws Exception
103    {
104        String id = fileReloader.getId(sourceUrl);
105        if (!_files.containsKey(id))
106        {
107            _files.put(id, new FileMetadatas(sourceUrl, parameters));
108        }
109        
110        FileMetadatas fileMetadatas = _files.get(id);
111        
112        synchronized (fileMetadatas)
113        {
114            Source source = null;
115            try
116            {
117                source = _resolver.resolveURI(fileMetadatas.getSourceUrl(), null, fileMetadatas.getParameters());
118                if (!source.exists())
119                {
120                    throw new SourceNotFoundException("Source '" + fileMetadatas.getSourceUrl() + "' not found");
121                }
122                
123                long sourceLastModified = (source.getLastModified() / 1000) * 1000; // hack for file systems only returning seconds
124                if (forceRead || sourceLastModified >= fileMetadatas.getLastFileReading())
125                {
126                    getLogger().debug("Reading configuration file at {}", fileMetadatas.getSourceUrl());
127                    
128                    long lastFileReading = Instant.now().truncatedTo(ChronoUnit.SECONDS).toEpochMilli();
129                    
130                    try (InputStream is = source.getInputStream())
131                    {
132                        fileReloader.updateFile(fileMetadatas.getSourceUrl(), is);
133                        fileMetadatas.setLastFileReading(lastFileReading);
134                        getLogger().debug("Configuration file read. at {}", fileMetadatas.getSourceUrl());
135                    }
136                    
137                }
138                
139            }
140            catch (SourceNotFoundException e)
141            {
142                getLogger().debug("File at {} could not be found", fileMetadatas.getSourceUrl(), e);
143                if (forceRead || fileMetadatas.getLastFileReading() < 0)
144                {
145                    fileReloader.updateFile(fileMetadatas.getSourceUrl(), null);
146                }
147                fileMetadatas.setLastFileReading(0); // It was initialized with -1 so the update will be forced only once
148            }
149            finally 
150            {
151                _resolver.release(source);
152            }
153        }
154    }
155    
156    private static class FileMetadatas
157    {
158        private long _lastFileReading;
159        private String _sourceUrl;
160        private Map _parameters;
161
162        public FileMetadatas(String sourceUrl, Map parameters)
163        {
164            this._sourceUrl = sourceUrl;
165            this._lastFileReading = -1;
166            this._parameters = parameters;
167        }
168        
169        public long getLastFileReading()
170        {
171            return this._lastFileReading;
172        }
173        public void setLastFileReading(long lastFileReading)
174        {
175            this._lastFileReading = lastFileReading;
176        }
177        
178        public String getSourceUrl()
179        {
180            return this._sourceUrl;
181        }
182        
183        public Map getParameters()
184        {
185            return this._parameters;
186        }
187    }
188
189}