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