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}