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.ProgramItem; 045import org.ametys.odf.coursepart.CoursePart; 046import org.ametys.odf.program.Program; 047import org.ametys.odf.schedulable.CopyCatalogSchedulable; 048import org.ametys.odf.schedulable.DeleteCatalogSchedulable; 049import org.ametys.plugins.core.impl.schedule.DefaultRunnable; 050import org.ametys.plugins.core.schedule.Scheduler; 051import org.ametys.plugins.repository.AmetysObjectIterable; 052import org.ametys.plugins.repository.AmetysObjectResolver; 053import org.ametys.plugins.repository.UnknownAmetysObjectException; 054import org.ametys.runtime.i18n.I18nizableText; 055import org.ametys.runtime.i18n.I18nizableTextParameter; 056import org.ametys.runtime.plugin.component.AbstractLogEnabled; 057 058/** 059 * DAO for manipulating catalogs. 060 * 061 */ 062public class CatalogDAO extends AbstractLogEnabled implements Serviceable, Component 063{ 064 /** The Avalon role */ 065 public static final String ROLE = CatalogDAO.class.getName(); 066 067 /** The catalog manager */ 068 protected CatalogsManager _catalogsManager; 069 070 /** The ametys object resolver */ 071 protected AmetysObjectResolver _resolver; 072 073 /** The current user provider */ 074 protected CurrentUserProvider _currentUserProvider; 075 076 /** The scheduler */ 077 protected Scheduler _scheduler; 078 079 /** The I18N utils */ 080 protected I18nUtils _i18nUtils; 081 082 @Override 083 public void service(ServiceManager manager) throws ServiceException 084 { 085 _catalogsManager = (CatalogsManager) manager.lookup(CatalogsManager.ROLE); 086 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 087 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 088 _scheduler = (Scheduler) manager.lookup(Scheduler.ROLE); 089 _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE); 090 } 091 092 /** 093 * Creates a new ODF catalog. 094 * @param title The title of the catalog 095 * @param name The code of the catalog 096 * @param catalogNameToCopy The catalog name to copy or null 097 * @return The id and the title of the created catalog, or an error 098 * @throws ProcessingException if creation failed 099 */ 100 @Callable 101 public Map<String, String> createCatalog (String title, String name, String catalogNameToCopy) throws ProcessingException 102 { 103 Map<String, String> result = new HashMap<>(); 104 105 // FIXME CMS-5758 FilterNameHelper.filterName do not authorized name with number (so name is computed from JS) 106 107 Catalog catalog = _catalogsManager.getCatalog(name); 108 if (catalog != null) 109 { 110 result.put("message", "already-exist"); 111 return result; 112 } 113 114 Catalog newCatalog = _catalogsManager.createCatalog(name, title); 115 116 if (StringUtils.isNotEmpty(catalogNameToCopy)) 117 { 118 Catalog catalogToCopy = _catalogsManager.getCatalog(catalogNameToCopy); 119 120 if (catalogToCopy == null) 121 { 122 result.put("message", "not-found"); 123 return result; 124 } 125 126 Map<String, I18nizableTextParameter> i18nParams = new HashMap<>(); 127 i18nParams.put("srcCatalog", new I18nizableText(catalogToCopy.getTitle())); 128 i18nParams.put("destCatalog", new I18nizableText(newCatalog.getTitle())); 129 130 Map<String, Object> params = new HashMap<>(); 131 params.put(CopyCatalogSchedulable.JOBDATAMAP_SRC_CATALOG_KEY, catalogToCopy.getName()); 132 params.put(CopyCatalogSchedulable.JOBDATAMAP_DEST_CATALOG_KEY, newCatalog.getName()); 133 134 Runnable runnable = new DefaultRunnable(CopyCatalogSchedulable.SCHEDULABLE_ID + "$" + newCatalog.getName(), 135 new I18nizableText(_i18nUtils.translate(new I18nizableText("plugin.odf", "PLUGINS_ODF_SCHEDULABLE_COPY_CATALOG_LABEL_WITH_DETAILS", i18nParams))), 136 new I18nizableText(_i18nUtils.translate(new I18nizableText("plugin.odf", "PLUGINS_ODF_SCHEDULABLE_COPY_CATALOG_DESCRIPTION_WITH_DETAILS", i18nParams))), 137 FireProcess.NOW, 138 null /* cron*/, 139 CopyCatalogSchedulable.SCHEDULABLE_ID, 140 false /* removable */, 141 false /* modifiable */, 142 false /* deactivatable */, 143 MisfirePolicy.FIRE_ONCE, 144 false /* isVolatile */, 145 _currentUserProvider.getUser(), 146 params 147 ); 148 149 try 150 { 151 JobKey jobKey = new JobKey(runnable.getId(), Scheduler.JOB_GROUP); 152 if (_scheduler.getScheduler().checkExists(jobKey)) 153 { 154 _scheduler.getScheduler().deleteJob(jobKey); 155 } 156 _scheduler.scheduleJob(runnable); 157 } 158 catch (SchedulerException e) 159 { 160 getLogger().error("An error occured when trying to schedule the copy of the catalog '{}' to '{}'", catalogToCopy.getTitle(), newCatalog.getTitle(), e); 161 } 162 } 163 164 result.put("id", newCatalog.getId()); 165 result.put("title", newCatalog.getTitle()); 166 167 return result; 168 } 169 170 /** 171 * Edits an ODF catalog. 172 * @param id The id of the catalog to edit 173 * @param title The title of the catalog 174 * @return The id and the title of the edited catalog, or an error 175 */ 176 @Callable 177 public Map<String, String> editCatalog (String id, String title) 178 { 179 Map<String, String> result = new HashMap<>(); 180 181 try 182 { 183 Catalog catalog = _resolver.resolveById(id); 184 185 catalog.setTitle(title); 186 catalog.saveChanges(); 187 188 result.put("id", catalog.getId()); 189 } 190 catch (UnknownAmetysObjectException e) 191 { 192 result.put("message", "not-found"); 193 } 194 195 return result; 196 } 197 198 /** 199 * Set a catalog as default catalog 200 * @param id The id of catalog 201 * @return The id and the title of the edited catalog, or an error 202 */ 203 @Callable 204 public synchronized Map<String, String> setDefaultCatalog(String id) 205 { 206 Map<String, String> result = new HashMap<>(); 207 208 try 209 { 210 Catalog catalog = _resolver.resolveById(id); 211 212 Catalog defaultCatalog = _catalogsManager.getDefaultCatalog(); 213 if (defaultCatalog != null) 214 { 215 defaultCatalog.setDefault(false); 216 defaultCatalog.saveChanges(); 217 } 218 catalog.setDefault(true); 219 catalog.saveChanges(); 220 221 _catalogsManager.updateDefaultCatalog(); 222 223 result.put("id", catalog.getId()); 224 } 225 catch (UnknownAmetysObjectException e) 226 { 227 result.put("message", "not-found"); 228 } 229 230 return result; 231 } 232 233 /** 234 * Removes an ODF catalog. 235 * @param catalogId the catalog's id 236 * @param forceDeletion if true, will not check if the catalog is referenced by {@code ProgramItem}s before deleting the catalog 237 * @return The id of the deleted catalog, or an error 238 */ 239 @Callable 240 public Map<String, Object> removeCatalog (String catalogId, boolean forceDeletion) 241 { 242 Map<String, Object> result = new HashMap<>(); 243 result.put("id", catalogId); 244 Catalog catalog = null; 245 try 246 { 247 catalog = _resolver.resolveById(catalogId); 248 } 249 catch (UnknownAmetysObjectException e) 250 { 251 result.put("error", "unknown-catalog"); 252 return result; 253 } 254 255 if (!forceDeletion) 256 { 257 List<Content> contents = _catalogsManager.getContents(catalog.getName()); 258 if (!contents.isEmpty()) 259 { 260 // Still has programItems 261 // We provide a summary of the remaining items 262 result.put("error", "remaining-items"); 263 result.put("remainingItems", _summarizeRemainingItems(contents)); 264 return result; 265 } 266 } 267 String runnableId = _doRemoveCatalog(catalog); 268 if (runnableId != null) 269 { 270 result.put("jobId", runnableId); 271 } 272 else 273 { 274 result.put("error", "job-start-failed"); 275 } 276 return result; 277 } 278 279 private String _doRemoveCatalog(Catalog catalog) 280 { 281 String runnableId = DeleteCatalogSchedulable.SCHEDULABLE_ID + "$" + catalog.getId(); 282 try 283 { 284 JobKey jobKey = new JobKey(runnableId, Scheduler.JOB_GROUP); 285 // here we want to check if a deletion of the catalog is not already running or finished 286 // we rely on the job key which is not ideal, would be better to rely on the job param of the schedulable 287 if (_scheduler.getScheduler().checkExists(jobKey)) 288 { 289 Map<String, Object> runningMap = _scheduler.isRunning(jobKey.getName()); 290 boolean running = (boolean) runningMap.get("running"); 291 if (running) 292 { 293 // A removal of the catalog is already ongoing so we do nothing 294 return runnableId; 295 } 296 else 297 { 298 JobDetail jobDetail = _scheduler.getScheduler().getJobDetail(jobKey); 299 boolean success = jobDetail.getJobDataMap().getBoolean("success"); 300 if (success) 301 { 302 // The catalog was already removed with success we might better do nothing 303 return null; 304 } 305 else 306 { 307 // The job failed, so we restart it 308 _scheduler.remove(jobKey.getName()); 309 } 310 } 311 } 312 Map<String, I18nizableTextParameter> i18nParams = new HashMap<>(); 313 i18nParams.put("catalogName", new I18nizableText(catalog.getTitle())); 314 i18nParams.put("catalogId", new I18nizableText(catalog.getId())); 315 316 Map<String, Object> params = new HashMap<>(); 317 params.put(DeleteCatalogSchedulable.JOBDATAMAP_CATALOG_NAME_KEY, catalog.getName()); 318 319 Runnable runnable = new DefaultRunnable(runnableId, 320 new I18nizableText(_i18nUtils.translate(new I18nizableText("plugin.odf", "PLUGINS_ODF_SCHEDULABLE_DELETE_CATALOG_LABEL_WITH_DETAILS", i18nParams))), 321 new I18nizableText(_i18nUtils.translate(new I18nizableText("plugin.odf", "PLUGINS_ODF_SCHEDULABLE_DELETE_CATALOG_DESCRIPTION_WITH_DETAILS", i18nParams))), 322 FireProcess.NOW, 323 null /* cron*/, 324 DeleteCatalogSchedulable.SCHEDULABLE_ID, 325 true /* removable */, 326 false /* modifiable */, 327 false /* deactivatable */, 328 MisfirePolicy.FIRE_ONCE, 329 true /* isVolatile */, 330 _currentUserProvider.getUser(), 331 params 332 ); 333 334 _scheduler.scheduleJob(runnable); 335 return runnableId; 336 } 337 catch (SchedulerException e) 338 { 339 getLogger().error("An error occured when trying to schedule the deletion of the catalog '{}'", catalog.getTitle(), e); 340 return null; 341 } 342 } 343 344 private Map<String, AtomicInteger> _summarizeRemainingItems(List<Content> contents) 345 { 346 Map<String, AtomicInteger> summary = new HashMap<>(); 347 for (Content content : contents) 348 { 349 String key = content instanceof ProgramItem || content instanceof CoursePart 350 ? StringUtils.substringAfterLast(content.getTypes()[0], ".") 351 : "other"; 352 summary.computeIfAbsent(key, __ -> new AtomicInteger()).incrementAndGet(); 353 } 354 return summary; 355 } 356 357 /** 358 * Gets the properties of a catalog. 359 * @param id The catalog id 360 * @return The properties of the catalog in a map 361 */ 362 @Callable 363 public Map<String, Object> getCatalogProperties(String id) 364 { 365 Catalog catalog = _resolver.resolveById(id); 366 return getCatalogProperties(catalog); 367 } 368 369 /** 370 * Gets the properties of a set of catalogs. 371 * @param ids The catalogs' id 372 * @return The properties of the catalogs 373 */ 374 @Callable 375 public Map<String, Object> getCatalogsProperties(List<String> ids) 376 { 377 Map<String, Object> result = new HashMap<>(); 378 379 List<Map<String, Object>> catalogs = new LinkedList<>(); 380 Set<String> unknownCatalogs = new HashSet<>(); 381 382 for (String id : ids) 383 { 384 try 385 { 386 Catalog catalog = _resolver.resolveById(id); 387 catalogs.add(getCatalogProperties(catalog)); 388 } 389 catch (UnknownAmetysObjectException e) 390 { 391 unknownCatalogs.add(id); 392 } 393 } 394 395 result.put("catalogs", catalogs); 396 result.put("unknownCatalogs", unknownCatalogs); 397 398 return result; 399 } 400 401 /** 402 * Get the properties of a catalog as a Map 403 * @param catalog The catalog 404 * @return The properties into a map object 405 */ 406 public Map<String, Object> getCatalogProperties(Catalog catalog) 407 { 408 Map<String, Object> result = new HashMap<>(); 409 410 result.put("id", catalog.getId()); 411 result.put("title", catalog.getTitle()); 412 result.put("isDefault", catalog.isDefault()); 413 result.put("code", catalog.getName()); 414 415 AmetysObjectIterable<Program> programs = _catalogsManager.getPrograms(catalog.getName()); 416 result.put("nbPrograms", programs.getSize()); 417 418 return result; 419 } 420}