001/*
002 *  Copyright 2010 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.plugins.repository.lock;
017
018import java.time.LocalDateTime;
019import java.time.ZonedDateTime;
020import java.util.HashMap;
021import java.util.Map;
022import java.util.Map.Entry;
023import java.util.Optional;
024
025import org.apache.avalon.framework.activity.Disposable;
026import org.apache.avalon.framework.activity.Initializable;
027import org.apache.avalon.framework.component.Component;
028import org.apache.avalon.framework.service.ServiceException;
029import org.apache.avalon.framework.service.ServiceManager;
030import org.apache.avalon.framework.service.Serviceable;
031import org.apache.avalon.framework.thread.ThreadSafe;
032import org.quartz.JobKey;
033import org.quartz.SchedulerException;
034
035import org.ametys.core.schedule.Runnable;
036import org.ametys.core.user.UserIdentity;
037import org.ametys.core.user.population.UserPopulationDAO;
038import org.ametys.plugins.core.schedule.Scheduler;
039import org.ametys.plugins.repository.AmetysObject;
040import org.ametys.plugins.repository.AmetysObjectIterable;
041import org.ametys.plugins.repository.AmetysObjectResolver;
042import org.ametys.plugins.repository.UnknownAmetysObjectException;
043import org.ametys.runtime.config.Config;
044import org.ametys.runtime.plugin.PluginsManager;
045import org.ametys.runtime.plugin.component.AbstractLogEnabled;
046
047/**
048 * The helper for scheduling the unlocking of the contents.<br>
049 * At initialization all contents are scanned and locked ones will be automatically unlocked.
050 */
051public class UnlockHelper extends AbstractLogEnabled implements Initializable, ThreadSafe, Component, Serviceable, Disposable
052{
053    /** Avalon Role */
054    public static final String ROLE = UnlockHelper.class.getName();
055
056    // Deploy parameters
057    private static final String __ACTIVATE_PARAMETER = "content.unlocktimer.activate";
058
059    private static final String __PERIOD_PARAMETER = "content.unlocktimer.period";
060
061    /** The scheduler */
062    protected Scheduler _scheduler;
063    
064    // To know if the component is activated
065    private boolean _activated;
066
067    // The period before unlocking in hours
068    private long _period;
069
070    /** The map of locked contents id (String) and their locked time (value) */
071    private Map<String, LocalDateTime> _lockedObjects;
072    
073    private AmetysObjectResolver _resolver;
074    
075    public void service(ServiceManager manager) throws ServiceException
076    {
077        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
078        _scheduler = (Scheduler) manager.lookup(Scheduler.ROLE);
079    }
080
081    public void initialize() throws Exception
082    {
083        Config config = Config.getInstance();
084        if (config == null || PluginsManager.getInstance().isSafeMode())
085        {
086            // Désactiver le composant pour affecter les paramètres de
087            // déploiement
088            // Le composant doit pouvoir s'initialiser en safe mode, mais ne pas
089            // lancer le timer.
090            _activated = false;
091        }
092        else
093        {
094            // Récupérer les paramètres de déploiement
095            _activated = config.getValue(__ACTIVATE_PARAMETER);
096
097            if (_activated)
098            {
099                _period = config.getValue(__PERIOD_PARAMETER);
100
101                // Vérifier la validité de la période
102                if (_period <= 0)
103                {
104                    throw new RuntimeException("Invalid period (in hours) : '" + _period + "'");
105                }
106            }
107            else
108            {
109                _period = 0;
110            }
111        }
112        
113        _initializeLockedObjects();
114    }
115
116    /**
117     * Search and stores references to all locked {@link AmetysObject}s in the repository.<br>
118     * Should not be called by clients. 
119     */
120    private void _initializeLockedObjects()
121    {
122        _lockedObjects = new HashMap<>();
123        
124        // search for all locked AmetysObjects
125        try (AmetysObjectIterable<LockableAmetysObject> it = _resolver.query("//element(*, ametys:object)[@jcr:lockOwner]");)
126        {
127            for (LockableAmetysObject object : it)
128            {
129                if (object.isLocked() && _activated)
130                {
131                    scheduleUnlocking(object);
132                    getLogger().info("Scheduled unlocking in " + _period + " hour(s) for object '{}'", object.getName());
133                }
134                else if (object.isLocked() && !_activated)
135                {
136                    // If the unlocking was activated with remaining locked contents, and now it is disabled
137                    // there are stil remaining quartz job for unlocking those contents
138                    synchronized (_lockedObjects)
139                    {
140                        _lockedObjects.put(object.getId(), LocalDateTime.now());
141                    }
142                    _cancelUnlocking(object);
143                }
144            }
145        }
146    }
147
148    /**
149     * Check if the unlocking is enabled.
150     * @return true if the unlocking is activated, false otherwise.
151     */
152    public boolean isUnlockingActivated()
153    {
154        return _activated;
155    }
156
157    /**
158     * Get the period before unlocking a content.
159     * @return The period in hours.
160     */
161    public long getTimeBeforeUnlock()
162    {
163        return _period;
164    }
165
166    /**
167     * Schedule a task to unlock an object.
168     * @param object The {@link AmetysObject} to be unlocked.
169     */
170    public void scheduleUnlocking(AmetysObject object)
171    {
172        synchronized (_lockedObjects)
173        {
174            _lockedObjects.put(object.getId(), LocalDateTime.now());
175        }
176            
177        if (_activated)
178        {
179            try
180            {
181                ZonedDateTime unlockDate = ZonedDateTime.now().plusHours(_period);
182                
183                UserIdentity lockUser = Optional.ofNullable(object)
184                        .filter(LockableAmetysObject.class::isInstance)
185                        .map(LockableAmetysObject.class::cast)
186                        .map(LockableAmetysObject::getLockOwner)
187                        .orElse(UserPopulationDAO.SYSTEM_USER_IDENTITY);
188                Runnable unlockRunnable = new UnlockRunnable(object.getId(), object.getName(), unlockDate, lockUser);
189                JobKey jobKey = new JobKey(unlockRunnable.getId(), Scheduler.JOB_GROUP);
190                if (_scheduler.getScheduler().checkExists(jobKey))
191                {
192                    _scheduler.getScheduler().deleteJob(jobKey);
193                }
194                _scheduler.scheduleJob(unlockRunnable);
195                getLogger().info("Scheduled unlocking in " + _period + " hour(s) for object '{}'", object.getName());
196            }
197            catch (SchedulerException e)
198            {
199                getLogger().error("An error occured when trying to schedule the unlocking of the object " + object.getId(), e);
200            }
201        }
202    }
203
204    /**
205     * Cancel the unlocking of an {@link AmetysObject}.
206     * @param object The object to cancel the unlock scheduling.
207     * @return true if the unlocking was cancelled, false otherwise.
208     */
209    public boolean cancelUnlocking(AmetysObject object)
210    {
211        synchronized (_lockedObjects)
212        {
213            _lockedObjects.remove(object.getId());
214        }
215            
216        if (_activated)
217        {
218            return _cancelUnlocking(object);
219        }
220        else
221        {
222            return false;
223        }
224    }
225    
226    private boolean _cancelUnlocking(AmetysObject object)
227    {
228        try
229        {
230            String jobName = UnlockRunnable.class.getName() + "." + object.getId();
231            JobKey jobKey = new JobKey(jobName, Scheduler.JOB_GROUP);
232            if (_scheduler.getScheduler().checkExists(jobKey))
233            {
234                _scheduler.getScheduler().deleteJob(jobKey);
235                return true;
236            }
237        }
238        catch (SchedulerException e)
239        {
240            getLogger().error("An error occured when trying to schedule the unlocking of the object " + object.getId(), e);
241        }
242        return false;
243    }
244
245    /**
246     * Get all current locked {@link LockableAmetysObject}.
247     * @param <A> the generic type extending {@link LockableAmetysObject}.
248     * @return The contents as a Map of Content to LocalDateTime (the date time when they were locked).
249     */
250    @SuppressWarnings("unchecked")
251    public <A extends LockableAmetysObject> Map<A, LocalDateTime> getLockedObjects()
252    {
253        Map<A, LocalDateTime> result = new HashMap<>();
254        
255        synchronized (_lockedObjects)
256        {
257            for (Entry<String, LocalDateTime> entry : _lockedObjects.entrySet())
258            {
259                try
260                {
261                    LockableAmetysObject object = _resolver.resolveById(entry.getKey());
262                    
263                    if (object.isLocked())
264                    {
265                        // Add the current object and its locking time.
266                        // We test if it's still locked because it could have been manually unlocked in the meantime.
267                        result.put((A) object, entry.getValue());
268                    }
269                }
270                catch (UnknownAmetysObjectException e)
271                {
272                    // The object doesn't exist anymore: remove it from the map.
273                    if (getLogger().isWarnEnabled())
274                    {
275                        getLogger().warn("The object of ID " + entry.getKey() + " is referenced as locked but it doesn't exist anymore.", e);
276                    }
277                    _lockedObjects.remove(entry.getKey());
278                }
279                
280            }
281        }
282        
283        return result;
284    }
285    
286    @Override
287    public void dispose()
288    {
289        _activated = false;
290        
291        setLogger(null);
292        _resolver = null;
293        _lockedObjects = null;
294    }
295    
296}