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