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