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