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.getValueAsBoolean(__ACTIVATE_PARAMETER).booleanValue();
095
096            if (_activated)
097            {
098                _period = config.getValueAsLong(__PERIOD_PARAMETER).longValue();
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                if (getLogger().isErrorEnabled())
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    /**
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            if (getLogger().isErrorEnabled())
242            {
243                getLogger().error("An error occured when trying to schedule the unlocking of the object " + object.getId(), e);
244            }
245        }
246        return false;
247    }
248
249    /**
250     * Get all current locked {@link LockableAmetysObject}.
251     * @param <A> the generic type extending {@link LockableAmetysObject}.
252     * @return The contents as a Map of Content to LocalDateTime (the date time when they were locked).
253     */
254    @SuppressWarnings("unchecked")
255    public <A extends LockableAmetysObject> Map<A, LocalDateTime> getLockedObjects()
256    {
257        Map<A, LocalDateTime> result = new HashMap<>();
258        
259        synchronized (_lockedObjects)
260        {
261            for (Entry<String, LocalDateTime> entry : _lockedObjects.entrySet())
262            {
263                try
264                {
265                    LockableAmetysObject object = _resolver.resolveById(entry.getKey());
266                    
267                    if (object.isLocked())
268                    {
269                        // Add the current object and its locking time.
270                        // We test if it's still locked because it could have been manually unlocked in the meantime.
271                        result.put((A) object, entry.getValue());
272                    }
273                }
274                catch (UnknownAmetysObjectException e)
275                {
276                    // The object doesn't exist anymore: remove it from the map.
277                    if (getLogger().isWarnEnabled())
278                    {
279                        getLogger().warn("The object of ID " + entry.getKey() + " is referenced as locked but it doesn't exist anymore.", e);
280                    }
281                    _lockedObjects.remove(entry.getKey());
282                }
283                
284            }
285        }
286        
287        return result;
288    }
289    
290    @Override
291    public void dispose()
292    {
293        _activated = false;
294        
295        setLogger(null);
296        _resolver = null;
297        _lockedObjects = null;
298    }
299    
300}