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