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