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