001/*
002 *  Copyright 2015 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 */
016
017package org.ametys.odf.catalog;
018
019import java.util.HashMap;
020import java.util.HashSet;
021import java.util.LinkedList;
022import java.util.List;
023import java.util.Map;
024import java.util.Set;
025import java.util.concurrent.atomic.AtomicInteger;
026
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.cocoon.ProcessingException;
032import org.apache.commons.lang.StringUtils;
033import org.quartz.JobDetail;
034import org.quartz.JobKey;
035import org.quartz.SchedulerException;
036
037import org.ametys.cms.repository.Content;
038import org.ametys.core.schedule.Runnable;
039import org.ametys.core.schedule.Runnable.FireProcess;
040import org.ametys.core.schedule.Runnable.MisfirePolicy;
041import org.ametys.core.ui.Callable;
042import org.ametys.core.user.CurrentUserProvider;
043import org.ametys.core.util.I18nUtils;
044import org.ametys.odf.ProgramItem;
045import org.ametys.odf.coursepart.CoursePart;
046import org.ametys.odf.program.Program;
047import org.ametys.odf.schedulable.CopyCatalogSchedulable;
048import org.ametys.odf.schedulable.DeleteCatalogSchedulable;
049import org.ametys.plugins.core.impl.schedule.DefaultRunnable;
050import org.ametys.plugins.core.schedule.Scheduler;
051import org.ametys.plugins.repository.AmetysObjectIterable;
052import org.ametys.plugins.repository.AmetysObjectResolver;
053import org.ametys.plugins.repository.UnknownAmetysObjectException;
054import org.ametys.runtime.i18n.I18nizableText;
055import org.ametys.runtime.i18n.I18nizableTextParameter;
056import org.ametys.runtime.plugin.component.AbstractLogEnabled;
057
058/**
059 * DAO for manipulating catalogs.
060 *
061 */
062public class CatalogDAO extends AbstractLogEnabled implements Serviceable, Component
063{
064    /** The Avalon role */
065    public static final String ROLE = CatalogDAO.class.getName();
066    
067    /** The catalog manager */
068    protected CatalogsManager _catalogsManager;
069    
070    /** The ametys object resolver */
071    protected AmetysObjectResolver _resolver;
072    
073    /** The current user provider */
074    protected CurrentUserProvider _currentUserProvider;
075
076    /** The scheduler */
077    protected Scheduler _scheduler;
078    
079    /** The I18N utils */
080    protected I18nUtils _i18nUtils;
081    
082    @Override
083    public void service(ServiceManager manager) throws ServiceException
084    {
085        _catalogsManager = (CatalogsManager) manager.lookup(CatalogsManager.ROLE);
086        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
087        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
088        _scheduler = (Scheduler) manager.lookup(Scheduler.ROLE);
089        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
090    }
091    
092    /**
093     * Creates a new ODF catalog.
094     * @param title The title of the catalog
095     * @param name The code of the catalog
096     * @param catalogNameToCopy The catalog name to copy or null
097     * @return The id and the title of the created catalog, or an error
098     * @throws ProcessingException if creation failed
099     */
100    @Callable
101    public Map<String, String> createCatalog (String title, String name, String catalogNameToCopy) throws ProcessingException
102    {
103        Map<String, String> result = new HashMap<>();
104        
105        // FIXME CMS-5758 FilterNameHelper.filterName do not authorized name with number (so name is computed from JS)
106        
107        Catalog catalog = _catalogsManager.getCatalog(name);
108        if (catalog != null)
109        {
110            result.put("message", "already-exist");
111            return result;
112        }
113        
114        Catalog newCatalog = _catalogsManager.createCatalog(name, title);
115        
116        if (StringUtils.isNotEmpty(catalogNameToCopy))
117        {
118            Catalog catalogToCopy = _catalogsManager.getCatalog(catalogNameToCopy);
119            
120            if (catalogToCopy == null)
121            {
122                result.put("message", "not-found");
123                return result;
124            }
125            
126            Map<String, I18nizableTextParameter> i18nParams = new HashMap<>();
127            i18nParams.put("srcCatalog", new I18nizableText(catalogToCopy.getTitle()));
128            i18nParams.put("destCatalog", new I18nizableText(newCatalog.getTitle()));
129            
130            Map<String, Object> params = new HashMap<>();
131            params.put(CopyCatalogSchedulable.JOBDATAMAP_SRC_CATALOG_KEY, catalogToCopy.getName());
132            params.put(CopyCatalogSchedulable.JOBDATAMAP_DEST_CATALOG_KEY, newCatalog.getName());
133            
134            Runnable runnable = new DefaultRunnable(CopyCatalogSchedulable.SCHEDULABLE_ID + "$" + newCatalog.getName(), 
135                new I18nizableText(_i18nUtils.translate(new I18nizableText("plugin.odf", "PLUGINS_ODF_SCHEDULABLE_COPY_CATALOG_LABEL_WITH_DETAILS", i18nParams))),
136                new I18nizableText(_i18nUtils.translate(new I18nizableText("plugin.odf", "PLUGINS_ODF_SCHEDULABLE_COPY_CATALOG_DESCRIPTION_WITH_DETAILS", i18nParams))),
137                FireProcess.NOW, 
138                null /* cron*/, 
139                CopyCatalogSchedulable.SCHEDULABLE_ID, 
140                false /* removable */, 
141                false /* modifiable */, 
142                false /* deactivatable */, 
143                MisfirePolicy.FIRE_ONCE, 
144                false /* isVolatile */, 
145                _currentUserProvider.getUser(), 
146                params
147            );
148            
149            try
150            {
151                JobKey jobKey = new JobKey(runnable.getId(), Scheduler.JOB_GROUP);
152                if (_scheduler.getScheduler().checkExists(jobKey))
153                {
154                    _scheduler.getScheduler().deleteJob(jobKey);
155                }
156                _scheduler.scheduleJob(runnable);
157            }
158            catch (SchedulerException e)
159            {
160                getLogger().error("An error occured when trying to schedule the copy of the catalog '{}' to '{}'", catalogToCopy.getTitle(), newCatalog.getTitle(), e);
161            }
162        }
163        
164        result.put("id", newCatalog.getId());
165        result.put("title", newCatalog.getTitle());
166        
167        return result;
168    }
169    
170    /**
171     * Edits an ODF catalog.
172     * @param id The id of the catalog to edit
173     * @param title The title of the catalog
174     * @return The id and the title of the edited catalog, or an error
175     */
176    @Callable
177    public Map<String, String> editCatalog (String id, String title)
178    {
179        Map<String, String> result = new HashMap<>();
180        
181        try
182        {
183            Catalog catalog = _resolver.resolveById(id);
184            
185            catalog.setTitle(title);
186            catalog.saveChanges();
187            
188            result.put("id", catalog.getId());
189        }
190        catch (UnknownAmetysObjectException e)
191        {
192            result.put("message", "not-found");
193        }
194        
195        return result;
196    }
197    
198    /**
199     * Set a catalog as default catalog
200     * @param id The id of catalog
201     * @return The id and the title of the edited catalog, or an error
202     */
203    @Callable
204    public synchronized Map<String, String> setDefaultCatalog(String id)
205    {
206        Map<String, String> result = new HashMap<>();
207        
208        try
209        {
210            Catalog catalog = _resolver.resolveById(id);
211            
212            Catalog defaultCatalog = _catalogsManager.getDefaultCatalog();
213            if (defaultCatalog != null)
214            {
215                defaultCatalog.setDefault(false);
216                defaultCatalog.saveChanges();
217            }
218            catalog.setDefault(true);
219            catalog.saveChanges();
220            
221            _catalogsManager.updateDefaultCatalog();
222            
223            result.put("id", catalog.getId());
224        }
225        catch (UnknownAmetysObjectException e)
226        {
227            result.put("message", "not-found");
228        }
229        
230        return result;
231    }
232    
233    /**
234     * Removes an ODF catalog.
235     * @param catalogId the catalog's id
236     * @param forceDeletion if true, will not check if the catalog is referenced by {@code ProgramItem}s before deleting the catalog
237     * @return The id of the deleted catalog, or an error
238     */
239    @Callable
240    public Map<String, Object> removeCatalog (String catalogId, boolean forceDeletion)
241    {
242        Map<String, Object> result = new HashMap<>();
243        result.put("id", catalogId);
244        Catalog catalog = null;
245        try
246        {
247            catalog = _resolver.resolveById(catalogId);
248        }
249        catch (UnknownAmetysObjectException e)
250        {
251            result.put("error", "unknown-catalog");
252            return result;
253        }
254        
255        if (!forceDeletion)
256        {
257            List<Content> contents = _catalogsManager.getContents(catalog.getName());
258            if (!contents.isEmpty())
259            {
260                // Still has programItems
261                // We provide a summary of the remaining items
262                result.put("error", "remaining-items");
263                result.put("remainingItems", _summarizeRemainingItems(contents));
264                return result;
265            }
266        }
267        String runnableId = _doRemoveCatalog(catalog);
268        if (runnableId != null)
269        {
270            result.put("jobId", runnableId);
271        }
272        else
273        {
274            result.put("error", "job-start-failed");
275        }
276        return result;
277    }
278    
279    private String _doRemoveCatalog(Catalog catalog)
280    {
281        String runnableId = DeleteCatalogSchedulable.SCHEDULABLE_ID + "$" + catalog.getId();
282        try
283        {
284            JobKey jobKey = new JobKey(runnableId, Scheduler.JOB_GROUP);
285            // here we want to check if a deletion of the catalog is not already running or finished
286            // we rely on the job key which is not ideal, would be better to rely on the job param of the schedulable
287            if (_scheduler.getScheduler().checkExists(jobKey))
288            {
289                Map<String, Object> runningMap = _scheduler.isRunning(jobKey.getName());
290                boolean running = (boolean) runningMap.get("running");
291                if (running)
292                {
293                    // A removal of the catalog is already ongoing so we do nothing
294                    return runnableId;
295                }
296                else
297                {
298                    JobDetail jobDetail = _scheduler.getScheduler().getJobDetail(jobKey);
299                    boolean success = jobDetail.getJobDataMap().getBoolean("success");
300                    if (success)
301                    {
302                        // The catalog was already removed with success we might better do nothing
303                        return null;
304                    }
305                    else
306                    {
307                        // The job failed, so we restart it
308                        _scheduler.remove(jobKey.getName());
309                    }
310                }
311            }
312            Map<String, I18nizableTextParameter> i18nParams = new HashMap<>();
313            i18nParams.put("catalogName", new I18nizableText(catalog.getTitle()));
314            i18nParams.put("catalogId", new I18nizableText(catalog.getId()));
315    
316            Map<String, Object> params = new HashMap<>();
317            params.put(DeleteCatalogSchedulable.JOBDATAMAP_CATALOG_NAME_KEY, catalog.getName());
318            
319            Runnable runnable = new DefaultRunnable(runnableId, 
320                new I18nizableText(_i18nUtils.translate(new I18nizableText("plugin.odf", "PLUGINS_ODF_SCHEDULABLE_DELETE_CATALOG_LABEL_WITH_DETAILS", i18nParams))),
321                new I18nizableText(_i18nUtils.translate(new I18nizableText("plugin.odf", "PLUGINS_ODF_SCHEDULABLE_DELETE_CATALOG_DESCRIPTION_WITH_DETAILS", i18nParams))),
322                FireProcess.NOW,
323                null /* cron*/, 
324                DeleteCatalogSchedulable.SCHEDULABLE_ID, 
325                true /* removable */, 
326                false /* modifiable */, 
327                false /* deactivatable */, 
328                MisfirePolicy.FIRE_ONCE, 
329                true /* isVolatile */, 
330                _currentUserProvider.getUser(), 
331                params
332            );
333            
334            _scheduler.scheduleJob(runnable);
335            return runnableId;
336        }
337        catch (SchedulerException e)
338        {
339            getLogger().error("An error occured when trying to schedule the deletion of the catalog '{}'", catalog.getTitle(), e);
340            return null;
341        }
342    }
343    
344    private Map<String, AtomicInteger> _summarizeRemainingItems(List<Content> contents)
345    {
346        Map<String, AtomicInteger> summary = new HashMap<>();
347        for (Content content : contents)
348        {
349            String key = content instanceof ProgramItem || content instanceof CoursePart
350                    ? StringUtils.substringAfterLast(content.getTypes()[0], ".")
351                    : "other";
352            summary.computeIfAbsent(key, __ -> new AtomicInteger()).incrementAndGet();
353        }
354        return summary;
355    }
356    
357    /**
358     * Gets the properties of a catalog.
359     * @param id The catalog id
360     * @return The properties of the catalog in a map
361     */
362    @Callable
363    public Map<String, Object> getCatalogProperties(String id)
364    {
365        Catalog catalog = _resolver.resolveById(id);
366        return getCatalogProperties(catalog);
367    }
368    
369    /**
370     * Gets the properties of a set of catalogs.
371     * @param ids The catalogs' id
372     * @return The properties of the catalogs
373     */
374    @Callable
375    public Map<String, Object> getCatalogsProperties(List<String> ids)
376    {
377        Map<String, Object> result = new HashMap<>();
378        
379        List<Map<String, Object>> catalogs = new LinkedList<>();
380        Set<String> unknownCatalogs = new HashSet<>();
381        
382        for (String id : ids)
383        {
384            try
385            {
386                Catalog catalog = _resolver.resolveById(id);
387                catalogs.add(getCatalogProperties(catalog));
388            }
389            catch (UnknownAmetysObjectException e)
390            {
391                unknownCatalogs.add(id);
392            }
393        }
394        
395        result.put("catalogs", catalogs);
396        result.put("unknownCatalogs", unknownCatalogs);
397        
398        return result;
399    }
400    
401    /**
402     * Get the properties of a catalog as a Map
403     * @param catalog The catalog
404     * @return The properties into a map object
405     */
406    public Map<String, Object> getCatalogProperties(Catalog catalog)
407    {
408        Map<String, Object> result = new HashMap<>();
409        
410        result.put("id", catalog.getId());
411        result.put("title", catalog.getTitle());
412        result.put("isDefault", catalog.isDefault());
413        result.put("code", catalog.getName());
414        
415        AmetysObjectIterable<Program> programs = _catalogsManager.getPrograms(catalog.getName());
416        result.put("nbPrograms", programs.getSize());
417        
418        return result;
419    }
420}