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 PartOnDisk)) 201 { 202 if (part instanceof RejectedPart rejectedPart && rejectedPart.getMaxContentLength() == 0) 203 { 204 return new ResourceOperationResult("infected"); 205 } 206 else // if (part == null || partUploaded instanceof RejectedPart) 207 { 208 return new ResourceOperationResult("rejected"); 209 } 210 } 211 212 PartOnDisk uploadedFilePart = (PartOnDisk) part; 213 return performResourceOperation(uploadedFilePart.getFile(), parent, mode); 214 } 215 216 /** 217 * Perform an add or update resource operation 218 * @param file The file for this operation 219 * @param parent The parent collection 220 * @param mode The operation mode 221 * @return the result of the operation 222 */ 223 public ResourceOperationResult performResourceOperation(File file, ModifiableResourceCollection parent, ResourceOperationMode mode) 224 { 225 if (!_resourcesDAO.checkLock(parent)) 226 { 227 getLogger().warn("User '{}' is trying to modify the collection '{}' but it is locked by another user", _currentUserProvider.getUser(), parent.getName()); 228 return new ResourceOperationResult("locked"); 229 } 230 231 String fileName = file.getName(); 232 233 try (InputStream is = new FileInputStream(file)) 234 { 235 return performResourceOperation(is, fileName, parent, mode); 236 } 237 catch (Exception e) 238 { 239 getLogger().error("Unable to add file to the collection of id '{}'", parent.getId(), e); 240 return new ResourceOperationResult("error"); 241 } 242 } 243 244 /** 245 * Perform an add or update resource operation 246 * @param inputStream The data for this operation 247 * @param fileName file name requested 248 * @param parent The parent collection 249 * @param mode The operation mode 250 * @return the result of the operation 251 */ 252 public ResourceOperationResult performResourceOperation(InputStream inputStream, String fileName, ModifiableResourceCollection parent, ResourceOperationMode mode) 253 { 254 String usedFileName = fileName; 255 256 if (!Normalizer.isNormalized(usedFileName, Form.NFC)) 257 { 258 usedFileName = Normalizer.normalize(usedFileName, Form.NFC); 259 } 260 261 if (!_resourcesDAO.checkLock(parent)) 262 { 263 getLogger().warn("User '{}' is trying to modify the collection '{}' but it is locked by another user", _currentUserProvider.getUser(), parent.getName()); 264 return new ResourceOperationResult("locked"); 265 } 266 267 if (fileName.toLowerCase().endsWith(".zip") && ResourceOperationMode.ADD_UNZIP.equals(mode)) 268 { 269 return _unzip(parent, inputStream); 270 } 271 272 ModifiableResource resource = null; 273 274 // Rename existing 275 if (parent.hasChild(usedFileName)) 276 { 277 if (ResourceOperationMode.ADD_RENAME.equals(mode)) 278 { 279 // Find a new name 280 String[] f = usedFileName.split("\\."); 281 int index = 1; 282 while (parent.hasChild(usedFileName)) 283 { 284 usedFileName = f[0] + "-" + (index++) + '.' + f[1]; 285 } 286 resource = _resourcesDAO.createResource(parent, usedFileName); 287 } 288 else if (ResourceOperationMode.UPDATE.equals(mode)) 289 { 290 resource = parent.getChild(usedFileName); 291 } 292 else 293 { 294 return new ResourceOperationResult("already-exist"); 295 } 296 } 297 // Add 298 else 299 { 300 resource = _resourcesDAO.createResource(parent, usedFileName); 301 } 302 303 try 304 { 305 if (!_resourcesDAO.checkLock(resource)) 306 { 307 getLogger().warn("User '{}' is trying to modify the resource '{}' but it is locked by another user", _currentUserProvider.getUser(), parent.getName()); 308 return new ResourceOperationResult("locked-file"); 309 } 310 311 _resourcesDAO.updateResource(resource, inputStream, fileName); 312 parent.saveChanges(); 313 _resourcesDAO.checkpoint(resource); 314 } 315 catch (Exception e) 316 { 317 getLogger().error("Unable to add file to the collection of id '{}'", parent.getId(), e); 318 return new ResourceOperationResult("error"); 319 } 320 321 // Notify listeners 322 if (ResourceOperationMode.UPDATE.equals(mode)) 323 { 324 _notifyResourcesUpdated(parent, resource); 325 } 326 else 327 { 328 _notifyResourcesCreated(parent, Collections.singletonList(resource)); 329 } 330 331 return new ResourceOperationResult(resource); 332 } 333 334 /** 335 * Fire the {@link ObservationConstants#EVENT_RESOURCE_CREATED} event 336 * @param parent The parent collection of the resource 337 * @param resources The created resources 338 */ 339 protected void _notifyResourcesCreated(ModifiableResourceCollection parent, List<Resource> resources) 340 { 341 Map<String, Object> eventParams = new HashMap<>(); 342 343 // ARGS_RESOURCES (transform to a map while keeping iteration order) 344 Map<String, Resource> resourceMap = resources.stream() 345 .collect(Collectors.toMap( 346 Resource::getId, // key = id 347 Function.identity(), // value = resource 348 (u, v) -> u, // allow duplicates 349 LinkedHashMap::new // to respect iteration order 350 )); 351 352 eventParams.put(ObservationConstants.ARGS_RESOURCES, resourceMap); 353 354 eventParams.put(ObservationConstants.ARGS_PARENT_ID, parent.getId()); 355 eventParams.put(ObservationConstants.ARGS_PARENT_PATH, parent.getPath()); 356 357 _observationManager.notify(new Event(ObservationConstants.EVENT_RESOURCE_CREATED, _currentUserProvider.getUser(), eventParams)); 358 } 359 360 /** 361 * Fire the {@link ObservationConstants#EVENT_RESOURCE_UPDATED} event 362 * @param parent The parent collection of the resource 363 * @param resource The updated resource 364 */ 365 protected void _notifyResourcesUpdated(ModifiableResourceCollection parent, Resource resource) 366 { 367 Map<String, Object> eventParams = new HashMap<>(); 368 369 eventParams.put(ObservationConstants.ARGS_ID, resource.getId()); 370 eventParams.put(ObservationConstants.ARGS_NAME, resource.getName()); 371 eventParams.put(ObservationConstants.ARGS_PATH, resource.getPath()); 372 eventParams.put(ObservationConstants.ARGS_RESOURCE_PATH, resource.getResourcePath()); 373 374 eventParams.put(ObservationConstants.ARGS_PARENT_ID, parent.getId()); 375 376 _observationManager.notify(new Event(ObservationConstants.EVENT_RESOURCE_UPDATED, _currentUserProvider.getUser(), eventParams)); 377 } 378 379 /** 380 * Unzip an inputStream and add the content to the resource collection 381 * @param collection The collection where to unzip 382 * @param inputStream the inputStream of data we want to unzip 383 * @return messages 384 */ 385 private ResourceOperationResult _unzip(ModifiableResourceCollection collection, InputStream inputStream) 386 { 387 try (ZipArchiveInputStream zipInputStream = new ZipArchiveInputStream(inputStream, "cp437")) 388 { 389 List<Resource> extractedResources = _unzip(collection, zipInputStream); 390 391 // Notify listeners 392 _notifyResourcesCreated(collection, extractedResources); 393 394 return new ResourceOperationResult(extractedResources); 395 } 396 catch (IOException e) 397 { 398 getLogger().error("Unable to unzip file", e); 399 return new ResourceOperationResult("unzip-error"); 400 } 401 } 402 403 private List<Resource> _unzip(ModifiableResourceCollection collection, ZipArchiveInputStream zipInputStream) throws IOException 404 { 405 List<Resource> extractedResources = new LinkedList<>(); 406 407 ArchiveEntry zipEntry; 408 while ((zipEntry = zipInputStream.getNextEntry()) != null) 409 { 410 ModifiableResourceCollection parentCollection = collection; 411 412 String zipName = zipEntry.getName(); 413 String[] path = zipName.split("/"); 414 415 for (int i = 0; i < path.length - 1; i++) 416 { 417 String name = path[i]; 418 parentCollection = _addCollection(parentCollection, name); 419 } 420 421 String name = path[path.length - 1]; 422 if (zipEntry.isDirectory()) 423 { 424 parentCollection = _addCollection(parentCollection, name); 425 } 426 else 427 { 428 // because of the getNextEntry() call, zipInputStream is restricted to the data of the entry 429 Resource resource = _addZipEntry(parentCollection, zipInputStream, name); 430 extractedResources.add(resource); 431 } 432 433 } 434 435 // sort by resource names 436 Ordering<Resource> resourceNameOrdering = Ordering.natural().onResultOf(Resource::getName); 437 extractedResources.sort(resourceNameOrdering); 438 439 return extractedResources; 440 } 441 442 private ModifiableResourceCollection _addCollection (ModifiableResourceCollection collection, String name) 443 { 444 if (collection.hasChild(name)) 445 { 446 return collection.getChild(name); 447 } 448 else 449 { 450 ModifiableResourceCollection child = collection.createChild(name, collection.getCollectionType()); 451 collection.saveChanges(); 452 return child; 453 } 454 } 455 456 private Resource _addZipEntry (ModifiableResourceCollection collection, InputStream zipInputStream, String fileName) 457 { 458 ModifiableResource resource; 459 460 if (collection.hasChild(fileName)) 461 { 462 resource = collection.getChild(fileName); 463 } 464 else 465 { 466 resource = _resourcesDAO.createResource(collection, fileName); 467 } 468 // the call to updateResource will close the InputStream. 469 // We don't want the InputStream to be closed because it's a zipInputStream 470 // containing all the zip data, not just this entry. 471 try (CloseShieldInputStream csis = new CloseShieldInputStream(zipInputStream)) 472 { 473 _resourcesDAO.updateResource(resource, csis, fileName); 474 } 475 476 collection.saveChanges(); 477 478 _resourcesDAO.checkpoint(resource); 479 480 return resource; 481 } 482 483 /** 484 * Class representing the result of a resource operation. 485 */ 486 public static class ResourceOperationResult 487 { 488 /** The created or updated resource(s) */ 489 private final List<Resource> _resources; 490 /** Indicates if an unzip operation was executed */ 491 private final boolean _unzip; 492 /** Indicates if the operation was successful */ 493 private final boolean _success; 494 /** Type of error in case of unsuccessful operation */ 495 private final String _errorMessage; 496 497 /** 498 * constructor in case of a successful operation 499 * @param resource The resource of this operation 500 */ 501 protected ResourceOperationResult(Resource resource) 502 { 503 _resources = Collections.singletonList(resource); 504 _unzip = false; 505 506 _success = true; 507 _errorMessage = null; 508 } 509 510 /** 511 * constructor in case of a successful unzip operation 512 * @param resources The list of resource for this operation 513 */ 514 protected ResourceOperationResult(List<Resource> resources) 515 { 516 _resources = resources; 517 _unzip = true; 518 519 _success = true; 520 _errorMessage = null; 521 } 522 523 /** 524 * constructor in case of an error 525 * @param errorMessage The error message. 526 */ 527 protected ResourceOperationResult(String errorMessage) 528 { 529 _errorMessage = errorMessage; 530 _success = false; 531 532 _resources = null; 533 _unzip = false; 534 } 535 536 /** 537 * Retrieves the resource 538 * Note that {@link #getResources()} should be used in case of an unzip. 539 * @return the resource 540 */ 541 public Resource getResource() 542 { 543 return _resources.get(0); 544 } 545 546 /** 547 * Retrieves the list of resources, in case of an unzip. 548 * @return the resource 549 */ 550 public List<Resource> getResources() 551 { 552 return _resources; 553 } 554 555 /** 556 * Retrieves the unzip 557 * @return the unzip 558 */ 559 public boolean isUnzip() 560 { 561 return _unzip; 562 } 563 564 /** 565 * Retrieves the success 566 * @return the success 567 */ 568 public boolean isSuccess() 569 { 570 return _success; 571 } 572 573 /** 574 * Retrieves the errorMessage 575 * @return the errorMessage 576 */ 577 public String getErrorMessage() 578 { 579 return _errorMessage; 580 } 581 } 582}