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