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