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 (rights = "ODF_Rights_Catalog_Handle") 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 (rights = "ODF_Rights_Catalog_Handle") 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 (rights = "ODF_Rights_Catalog_Handle") 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 result.put("code", catalog.getName()); 226 } 227 catch (UnknownAmetysObjectException e) 228 { 229 result.put("message", "not-found"); 230 } 231 232 return result; 233 } 234 235 /** 236 * Removes an ODF catalog. 237 * @param catalogId the catalog's id 238 * @param forceDeletion if true, will not check if the catalog is referenced by {@code ProgramItem}s before deleting the catalog 239 * @return The id of the deleted catalog, or an error 240 */ 241 @Callable (rights = "ODF_Rights_Catalog_Handle") 242 public Map<String, Object> removeCatalog (String catalogId, boolean forceDeletion) 243 { 244 Map<String, Object> result = new HashMap<>(); 245 result.put("id", catalogId); 246 Catalog catalog = null; 247 try 248 { 249 catalog = _resolver.resolveById(catalogId); 250 } 251 catch (UnknownAmetysObjectException e) 252 { 253 result.put("error", "unknown-catalog"); 254 return result; 255 } 256 257 if (!forceDeletion) 258 { 259 List<Content> contents = _catalogsManager.getContents(catalog.getName()); 260 if (!contents.isEmpty()) 261 { 262 // Still has programItems 263 // We provide a summary of the remaining items 264 result.put("error", "remaining-items"); 265 result.put("remainingItems", _summarizeRemainingItems(contents)); 266 return result; 267 } 268 } 269 String runnableId = _doRemoveCatalog(catalog); 270 if (runnableId != null) 271 { 272 result.put("jobId", runnableId); 273 } 274 else 275 { 276 result.put("error", "job-start-failed"); 277 } 278 return result; 279 } 280 281 private String _doRemoveCatalog(Catalog catalog) 282 { 283 String runnableId = DeleteCatalogSchedulable.SCHEDULABLE_ID + "$" + catalog.getId(); 284 try 285 { 286 JobKey jobKey = new JobKey(runnableId, Scheduler.JOB_GROUP); 287 // here we want to check if a deletion of the catalog is not already running or finished 288 // we rely on the job key which is not ideal, would be better to rely on the job param of the schedulable 289 if (_scheduler.getScheduler().checkExists(jobKey)) 290 { 291 Map<String, Object> runningMap = _scheduler.isRunning(jobKey.getName()); 292 boolean running = (boolean) runningMap.get("running"); 293 if (running) 294 { 295 // A removal of the catalog is already ongoing so we do nothing 296 return runnableId; 297 } 298 else 299 { 300 JobDetail jobDetail = _scheduler.getScheduler().getJobDetail(jobKey); 301 boolean success = jobDetail.getJobDataMap().getBoolean("success"); 302 if (success) 303 { 304 // The catalog was already removed with success we might better do nothing 305 return null; 306 } 307 else 308 { 309 // The job failed, so we restart it 310 _scheduler.remove(jobKey.getName()); 311 } 312 } 313 } 314 Map<String, I18nizableTextParameter> i18nParams = new HashMap<>(); 315 i18nParams.put("catalogName", new I18nizableText(catalog.getTitle())); 316 i18nParams.put("catalogId", new I18nizableText(catalog.getId())); 317 318 Map<String, Object> params = new HashMap<>(); 319 params.put(DeleteCatalogSchedulable.JOBDATAMAP_CATALOG_NAME_KEY, catalog.getName()); 320 321 Runnable runnable = new DefaultRunnable(runnableId, 322 new I18nizableText(_i18nUtils.translate(new I18nizableText("plugin.odf", "PLUGINS_ODF_SCHEDULABLE_DELETE_CATALOG_LABEL_WITH_DETAILS", i18nParams))), 323 new I18nizableText(_i18nUtils.translate(new I18nizableText("plugin.odf", "PLUGINS_ODF_SCHEDULABLE_DELETE_CATALOG_DESCRIPTION_WITH_DETAILS", i18nParams))), 324 FireProcess.NOW, 325 null /* cron*/, 326 DeleteCatalogSchedulable.SCHEDULABLE_ID, 327 true /* removable */, 328 false /* modifiable */, 329 false /* deactivatable */, 330 MisfirePolicy.FIRE_ONCE, 331 true /* isVolatile */, 332 _currentUserProvider.getUser(), 333 params 334 ); 335 336 _scheduler.scheduleJob(runnable); 337 return runnableId; 338 } 339 catch (SchedulerException e) 340 { 341 getLogger().error("An error occured when trying to schedule the deletion of the catalog '{}'", catalog.getTitle(), e); 342 return null; 343 } 344 } 345 346 private Map<String, AtomicInteger> _summarizeRemainingItems(List<Content> contents) 347 { 348 Map<String, AtomicInteger> summary = new HashMap<>(); 349 for (Content content : contents) 350 { 351 String key = content instanceof ProgramItem || content instanceof CoursePart 352 ? StringUtils.substringAfterLast(content.getTypes()[0], ".") 353 : "other"; 354 summary.computeIfAbsent(key, __ -> new AtomicInteger()).incrementAndGet(); 355 } 356 return summary; 357 } 358 359 /** 360 * Gets the properties of a catalog. 361 * @param id The catalog id 362 * @return The properties of the catalog in a map 363 */ 364 @Callable (rights = "ODF_Rights_Catalog_Handle") 365 public Map<String, Object> getCatalogProperties(String id) 366 { 367 Catalog catalog = _resolver.resolveById(id); 368 return getCatalogProperties(catalog); 369 } 370 371 /** 372 * Gets the properties of a set of catalogs. 373 * @param ids The catalogs' id 374 * @return The properties of the catalogs 375 */ 376 @Callable (rights = "ODF_Rights_Catalog_Handle") 377 public Map<String, Object> getCatalogsProperties(List<String> ids) 378 { 379 Map<String, Object> result = new HashMap<>(); 380 381 List<Map<String, Object>> catalogs = new LinkedList<>(); 382 Set<String> unknownCatalogs = new HashSet<>(); 383 384 for (String id : ids) 385 { 386 try 387 { 388 Catalog catalog = _resolver.resolveById(id); 389 catalogs.add(getCatalogProperties(catalog)); 390 } 391 catch (UnknownAmetysObjectException e) 392 { 393 unknownCatalogs.add(id); 394 } 395 } 396 397 result.put("catalogs", catalogs); 398 result.put("unknownCatalogs", unknownCatalogs); 399 400 return result; 401 } 402 403 /** 404 * Get the properties of a catalog as a Map 405 * @param catalog The catalog 406 * @return The properties into a map object 407 */ 408 public Map<String, Object> getCatalogProperties(Catalog catalog) 409 { 410 Map<String, Object> result = new HashMap<>(); 411 412 result.put("id", catalog.getId()); 413 result.put("title", catalog.getTitle()); 414 result.put("isDefault", catalog.isDefault()); 415 result.put("code", catalog.getName()); 416 417 AmetysObjectIterable<Program> programs = _catalogsManager.getPrograms(catalog.getName()); 418 result.put("nbPrograms", programs.getSize()); 419 420 return result; 421 } 422 423 /** 424 * List all the existing catalogs 425 * @return the catalogs 426 */ 427 @Callable (rights = Callable.NO_CHECK_REQUIRED) 428 public List<Object> getCatalogs() 429 { 430 List<Object> catalogs = new ArrayList<>(); 431 432 for (Catalog catalog : _catalogsManager.getCatalogs()) 433 { 434 catalogs.add(List.of(catalog.getName(), catalog.getTitle())); 435 } 436 437 return catalogs; 438 } 439}