001/* 002 * Copyright 2016 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 */ 016package org.ametys.plugins.explorer.resources.actions; 017 018import java.io.File; 019import java.io.FileInputStream; 020import java.io.IOException; 021import java.io.InputStream; 022import java.text.Normalizer; 023import java.text.Normalizer.Form; 024import java.util.Collections; 025import java.util.Enumeration; 026import java.util.HashMap; 027import java.util.LinkedHashMap; 028import java.util.LinkedList; 029import java.util.List; 030import java.util.Map; 031import java.util.function.Function; 032import java.util.stream.Collectors; 033 034import org.apache.avalon.framework.component.Component; 035import org.apache.avalon.framework.service.ServiceException; 036import org.apache.avalon.framework.service.ServiceManager; 037import org.apache.avalon.framework.service.Serviceable; 038import org.apache.cocoon.servlet.multipart.Part; 039import org.apache.cocoon.servlet.multipart.PartOnDisk; 040import org.apache.cocoon.servlet.multipart.RejectedPart; 041import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; 042import org.apache.commons.compress.archivers.zip.ZipFile; 043import org.apache.commons.lang.IllegalClassException; 044 045import org.ametys.core.observation.Event; 046import org.ametys.core.observation.ObservationManager; 047import org.ametys.core.user.CurrentUserProvider; 048import org.ametys.plugins.explorer.ObservationConstants; 049import org.ametys.plugins.explorer.resources.ModifiableResource; 050import org.ametys.plugins.explorer.resources.ModifiableResourceCollection; 051import org.ametys.plugins.explorer.resources.Resource; 052import org.ametys.plugins.repository.AmetysObject; 053import org.ametys.plugins.repository.AmetysObjectResolver; 054import org.ametys.plugins.repository.UnknownAmetysObjectException; 055import org.ametys.runtime.plugin.component.AbstractLogEnabled; 056 057import com.google.common.collect.Ordering; 058 059/** 060 * Dedicated helper in order to add or update an explorer resource 061 */ 062public final class AddOrUpdateResourceHelper extends AbstractLogEnabled implements Component, Serviceable 063{ 064 /** The Avalon role name */ 065 public static final String ROLE = AddOrUpdateResourceHelper.class.getName(); 066 067 /** The resource DAO */ 068 protected ExplorerResourcesDAO _resourcesDAO; 069 /** The ametys resolver */ 070 protected AmetysObjectResolver _resolver; 071 072 /** The current user provider. */ 073 protected CurrentUserProvider _currentUserProvider; 074 075 /** Observer manager. */ 076 protected ObservationManager _observationManager; 077 078 public void service(ServiceManager serviceManager) throws ServiceException 079 { 080 _resourcesDAO = (ExplorerResourcesDAO) serviceManager.lookup(ExplorerResourcesDAO.ROLE); 081 _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE); 082 _currentUserProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE); 083 _observationManager = (ObservationManager) serviceManager.lookup(ObservationManager.ROLE); 084 } 085 086 /** 087 * Possible add and update modes 088 */ 089 public enum ResourceOperationMode 090 { 091 /** Add */ 092 ADD("add"), 093 /** Add with an unzip */ 094 ADD_UNZIP("add-unzip"), 095 /** Add and allow rename */ 096 ADD_RENAME("add-rename"), 097 /** Update */ 098 UPDATE("update"); 099 100 private String _mode; 101 102 private ResourceOperationMode(String mode) 103 { 104 _mode = mode; 105 } 106 107 @Override 108 public String toString() 109 { 110 return _mode; 111 } 112 113 /** 114 * Converts an raw input mode to the corresponding ResourceOperationMode 115 * @param mode The raw mode to convert 116 * @return the corresponding ResourceOperationMode or null if unknown 117 */ 118 public static ResourceOperationMode createsFromRawMode(String mode) 119 { 120 for (ResourceOperationMode entry : ResourceOperationMode.values()) 121 { 122 if (entry.toString().equals(mode)) 123 { 124 return entry; 125 } 126 } 127 return null; 128 } 129 } 130 131 /** 132 * Perform an add or update resource operation 133 * @param part The part representing the file for this operation 134 * @param parentId The identifier of the parent collection 135 * @param mode The operation mode 136 * @return the result of the operation 137 */ 138 public ResourceOperationResult performResourceOperation(Part part, String parentId, ResourceOperationMode mode) 139 { 140 try 141 { 142 AmetysObject object = _resolver.resolveById(parentId); 143 if (!(object instanceof ModifiableResourceCollection)) 144 { 145 throw new IllegalClassException(ModifiableResourceCollection.class, object.getClass()); 146 } 147 148 return performResourceOperation(part, (ModifiableResourceCollection) object, mode); 149 } 150 catch (UnknownAmetysObjectException e) 151 { 152 getLogger().error("Unable to add file : the collection of id '{}' does not exist anymore", parentId, e); 153 return new ResourceOperationResult("unknown-collection"); 154 } 155 } 156 157 /** 158 * Perform an add or update resource operation 159 * @param part The part representing the file for this operation 160 * @param parent The parent collection 161 * @param mode The operation mode 162 * @return the result of the operation 163 */ 164 public ResourceOperationResult performResourceOperation(Part part, ModifiableResourceCollection parent, ResourceOperationMode mode) 165 { 166 if (part instanceof RejectedPart || part == null) 167 { 168 return new ResourceOperationResult("rejected"); 169 } 170 171 PartOnDisk uploadedFilePart = (PartOnDisk) part; 172 return performResourceOperation(uploadedFilePart.getFile(), parent, mode); 173 } 174 175 /** 176 * Perform an add or update resource operation 177 * @param file The file for this operation 178 * @param parent The parent collection 179 * @param mode The operation mode 180 * @return the result of the operation 181 */ 182 public ResourceOperationResult performResourceOperation(File file, ModifiableResourceCollection parent, ResourceOperationMode mode) 183 { 184 // FIXME CMS-2297 185 // checkUserRight(parent, "Plugin_Explorer_File_Add"); 186 187 if (!_resourcesDAO.checkLock(parent)) 188 { 189 getLogger().warn("User '{}' is trying to modify the collection '{}' but it is locked by another user", _currentUserProvider.getUser(), parent.getName()); 190 return new ResourceOperationResult("locked"); 191 } 192 193 String fileName = file.getName(); 194 195 // Unzip 196 if (fileName.toLowerCase().endsWith(".zip") && ResourceOperationMode.ADD_UNZIP.equals(mode)) 197 { 198 return _unzip(parent, file); 199 } 200 else 201 { 202 try (InputStream is = new FileInputStream(file)) 203 { 204 return performResourceOperation(is, fileName, parent, mode); 205 } 206 catch (Exception e) 207 { 208 getLogger().error("Unable to add file to the collection of id '{}'", parent.getId(), e); 209 return new ResourceOperationResult("error"); 210 } 211 } 212 } 213 214 /** 215 * Perform an add or update resource operation 216 * @param inputStream The data for this operation 217 * @param fileName file name requested 218 * @param parent The parent collection 219 * @param mode The operation mode 220 * @return the result of the operation 221 */ 222 public ResourceOperationResult performResourceOperation(InputStream inputStream, String fileName, ModifiableResourceCollection parent, ResourceOperationMode mode) 223 { 224 // FIXME CMS-2297 225 // checkUserRight(parent, "Plugin_Explorer_File_Add"); 226 String usedFileName = fileName; 227 228 if (!Normalizer.isNormalized(usedFileName, Form.NFC)) 229 { 230 usedFileName = Normalizer.normalize(usedFileName, Form.NFC); 231 } 232 233 if (!_resourcesDAO.checkLock(parent)) 234 { 235 getLogger().warn("User '{}' is trying to modify the collection '{}' but it is locked by another user", _currentUserProvider.getUser(), parent.getName()); 236 return new ResourceOperationResult("locked"); 237 } 238 239 ModifiableResource resource = null; 240 241 // Rename existing 242 if (parent.hasChild(usedFileName)) 243 { 244 if (ResourceOperationMode.ADD_RENAME.equals(mode)) 245 { 246 // Find a new name 247 String[] f = usedFileName.split("\\."); 248 int index = 1; 249 while (parent.hasChild(usedFileName)) 250 { 251 usedFileName = f[0] + "-" + (index++) + '.' + f[1]; 252 } 253 resource = _resourcesDAO.createResource(parent, usedFileName); 254 } 255 else if (ResourceOperationMode.UPDATE.equals(mode)) 256 { 257 resource = parent.getChild(usedFileName); 258 } 259 else 260 { 261 return new ResourceOperationResult("already-exist"); 262 } 263 } 264 // Add 265 else 266 { 267 resource = _resourcesDAO.createResource(parent, usedFileName); 268 } 269 270 try 271 { 272 if (!_resourcesDAO.checkLock(resource)) 273 { 274 getLogger().warn("User '{}' is trying to modify the resource '{}' but it is locked by another user", _currentUserProvider.getUser(), parent.getName()); 275 return new ResourceOperationResult("locked-file"); 276 } 277 278 _resourcesDAO.updateResource(resource, inputStream, fileName); 279 parent.saveChanges(); 280 _resourcesDAO.checkpoint(resource); 281 } 282 catch (Exception e) 283 { 284 getLogger().error("Unable to add file to the collection of id '{}'", parent.getId(), e); 285 return new ResourceOperationResult("error"); 286 } 287 288 // Notify listeners 289 if (ResourceOperationMode.UPDATE.equals(mode)) 290 { 291 _notifyResourcesUpdated(parent, resource); 292 } 293 else 294 { 295 _notifyResourcesCreated(parent, Collections.singletonList(resource)); 296 } 297 298 return new ResourceOperationResult(resource); 299 } 300 301 /** 302 * Fire the {@link ObservationConstants#EVENT_RESOURCE_CREATED} event 303 * @param parent The parent collection of the resource 304 * @param resources The created resources 305 */ 306 protected void _notifyResourcesCreated(ModifiableResourceCollection parent, List<Resource> resources) 307 { 308 Map<String, Object> eventParams = new HashMap<>(); 309 310 // ARGS_RESOURCES (transform to a map while keeping iteration order) 311 Map<String, Resource> resourceMap = resources.stream() 312 .collect(Collectors.toMap( 313 Resource::getId, // key = id 314 Function.identity(), // value = resource 315 (u, v) -> u, // allow duplicates 316 LinkedHashMap::new // to respect iteration order 317 )); 318 319 eventParams.put(ObservationConstants.ARGS_RESOURCES, resourceMap); 320 321 eventParams.put(ObservationConstants.ARGS_PARENT_ID, parent.getId()); 322 eventParams.put(ObservationConstants.ARGS_PARENT_PATH, parent.getPath()); 323 324 _observationManager.notify(new Event(ObservationConstants.EVENT_RESOURCE_CREATED, _currentUserProvider.getUser(), eventParams)); 325 } 326 327 /** 328 * Fire the {@link ObservationConstants#EVENT_RESOURCE_UPDATED} event 329 * @param parent The parent collection of the resource 330 * @param resource The updated resource 331 */ 332 protected void _notifyResourcesUpdated(ModifiableResourceCollection parent, Resource resource) 333 { 334 Map<String, Object> eventParams = new HashMap<>(); 335 336 eventParams.put(ObservationConstants.ARGS_ID, resource.getId()); 337 eventParams.put(ObservationConstants.ARGS_NAME, resource.getName()); 338 eventParams.put(ObservationConstants.ARGS_PATH, resource.getPath()); 339 eventParams.put(ObservationConstants.ARGS_RESOURCE_PATH, resource.getResourcePath()); 340 341 eventParams.put(ObservationConstants.ARGS_PARENT_ID, parent.getId()); 342 343 _observationManager.notify(new Event(ObservationConstants.EVENT_RESOURCE_UPDATED, _currentUserProvider.getUser(), eventParams)); 344 } 345 346 /** 347 * Unzip a file 348 * @param collection The collection where to unzip 349 * @param file The zip file 350 * @return messages 351 */ 352 private ResourceOperationResult _unzip(ModifiableResourceCollection collection, File file) 353 { 354 ZipFile zipFile = null; 355 356 try 357 { 358 zipFile = new ZipFile(file, "cp437"); 359 List<Resource> extractedResources = _unzip(collection, zipFile); 360 361 // Notify listeners 362 _notifyResourcesCreated(collection, extractedResources); 363 364 return new ResourceOperationResult(extractedResources); 365 } 366 catch (IOException e) 367 { 368 getLogger().error("Unable to unzip file", e); 369 return new ResourceOperationResult("unzip-error"); 370 } 371 finally 372 { 373 ZipFile.closeQuietly(zipFile); 374 } 375 } 376 377 private List<Resource> _unzip(ModifiableResourceCollection collection, ZipFile zipFile) throws IOException 378 { 379 List<Resource> extractedResources = new LinkedList<>(); 380 381 Enumeration<ZipArchiveEntry> entries = zipFile.getEntries(); 382 while (entries.hasMoreElements()) 383 { 384 ZipArchiveEntry zipEntry = entries.nextElement(); 385 386 ModifiableResourceCollection parentCollection = collection; 387 388 String zipName = zipEntry.getName(); 389 String[] path = zipName.split("/"); 390 391 for (int i = 0; i < path.length - 1; i++) 392 { 393 String name = path[i]; 394 parentCollection = _addCollection(parentCollection, name); 395 } 396 397 String name = path[path.length - 1]; 398 if (zipEntry.isDirectory()) 399 { 400 parentCollection = _addCollection(parentCollection, name); 401 } 402 else 403 { 404 Resource resource = _addZipEntry(parentCollection, zipFile, zipEntry, name); 405 extractedResources.add(resource); 406 } 407 408 } 409 410 // sort by resource names 411 Ordering<Resource> resourceNameOrdering = Ordering.natural().onResultOf(Resource::getName); 412 extractedResources.sort(resourceNameOrdering); 413 414 return extractedResources; 415 } 416 417 private ModifiableResourceCollection _addCollection (ModifiableResourceCollection collection, String name) 418 { 419 if (collection.hasChild(name)) 420 { 421 return collection.getChild(name); 422 } 423 else 424 { 425 ModifiableResourceCollection child = collection.createChild(name, collection.getCollectionType()); 426 collection.saveChanges(); 427 return child; 428 } 429 } 430 431 private Resource _addZipEntry (ModifiableResourceCollection collection, ZipFile zipFile, ZipArchiveEntry zipEntry, String fileName) throws IOException 432 { 433 ModifiableResource resource; 434 435 try (InputStream is = zipFile.getInputStream(zipEntry)) 436 { 437 if (collection.hasChild(fileName)) 438 { 439 resource = collection.getChild(fileName); 440 } 441 else 442 { 443 resource = _resourcesDAO.createResource(collection, fileName); 444 } 445 446 _resourcesDAO.updateResource(resource, is, fileName); 447 448 collection.saveChanges(); 449 450 _resourcesDAO.checkpoint(resource); 451 } 452 453 return resource; 454 } 455 456 /** 457 * Class representing the result of a resource operation. 458 */ 459 public static class ResourceOperationResult 460 { 461 /** The created or updated resource(s) */ 462 private final List<Resource> _resources; 463 /** Indicates if an unzip operation was executed */ 464 private final boolean _unzip; 465 /** Indicates if the operation was successful */ 466 private final boolean _success; 467 /** Type of error in case of unsuccessful operation */ 468 private final String _errorMessage; 469 470 /** 471 * constructor in case of a successful operation 472 * @param resource The resource of this operation 473 */ 474 protected ResourceOperationResult(Resource resource) 475 { 476 _resources = Collections.singletonList(resource); 477 _unzip = false; 478 479 _success = true; 480 _errorMessage = null; 481 } 482 483 /** 484 * constructor in case of a successful unzip operation 485 * @param resources The list of resource for this operation 486 */ 487 protected ResourceOperationResult(List<Resource> resources) 488 { 489 _resources = resources; 490 _unzip = true; 491 492 _success = true; 493 _errorMessage = null; 494 } 495 496 /** 497 * constructor in case of an error 498 * @param errorMessage The error message. 499 */ 500 protected ResourceOperationResult(String errorMessage) 501 { 502 _errorMessage = errorMessage; 503 _success = false; 504 505 _resources = null; 506 _unzip = false; 507 } 508 509 /** 510 * Retrieves the resource 511 * Note that {@link #getResources()} should be used in case of an unzip. 512 * @return the resource 513 */ 514 public Resource getResource() 515 { 516 return _resources.get(0); 517 } 518 519 /** 520 * Retrieves the list of resources, in case of an unzip. 521 * @return the resource 522 */ 523 public List<Resource> getResources() 524 { 525 return _resources; 526 } 527 528 /** 529 * Retrieves the unzip 530 * @return the unzip 531 */ 532 public boolean isUnzip() 533 { 534 return _unzip; 535 } 536 537 /** 538 * Retrieves the success 539 * @return the success 540 */ 541 public boolean isSuccess() 542 { 543 return _success; 544 } 545 546 /** 547 * Retrieves the errorMessage 548 * @return the errorMessage 549 */ 550 public String getErrorMessage() 551 { 552 return _errorMessage; 553 } 554 } 555}