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