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}