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