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.workspaces.project.objects; 017 018import java.io.IOException; 019import java.io.InputStream; 020import java.time.ZonedDateTime; 021import java.util.ArrayList; 022import java.util.Collection; 023import java.util.Iterator; 024import java.util.List; 025import java.util.Objects; 026import java.util.Optional; 027import java.util.Set; 028import java.util.stream.Collectors; 029import java.util.stream.StreamSupport; 030 031import javax.jcr.Node; 032import javax.jcr.NodeIterator; 033import javax.jcr.Property; 034import javax.jcr.RepositoryException; 035import javax.jcr.Value; 036import javax.jcr.ValueFactory; 037 038import org.apache.commons.lang3.ArrayUtils; 039import org.xml.sax.ContentHandler; 040import org.xml.sax.SAXException; 041 042import org.ametys.cms.data.Binary; 043import org.ametys.cms.indexing.solr.SolrAclCacheUninfluentialObject; 044import org.ametys.cms.tag.jcr.TaggableAmetysObjectHelper; 045import org.ametys.core.user.UserIdentity; 046import org.ametys.plugins.explorer.ExplorerNode; 047import org.ametys.plugins.repository.AmetysObject; 048import org.ametys.plugins.repository.AmetysObjectIterable; 049import org.ametys.plugins.repository.AmetysRepositoryException; 050import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 051import org.ametys.plugins.repository.RepositoryConstants; 052import org.ametys.plugins.repository.TraversableAmetysObject; 053import org.ametys.plugins.repository.data.ametysobject.ModifiableModelLessDataAwareAmetysObject; 054import org.ametys.plugins.repository.data.holder.ModifiableModelLessDataHolder; 055import org.ametys.plugins.repository.data.holder.impl.DefaultModifiableModelLessDataHolder; 056import org.ametys.plugins.repository.data.repositorydata.ModifiableRepositoryData; 057import org.ametys.plugins.repository.data.repositorydata.impl.JCRRepositoryData; 058import org.ametys.plugins.repository.events.EventHolder; 059import org.ametys.plugins.repository.events.JCREventHelper; 060import org.ametys.plugins.repository.jcr.DefaultTraversableAmetysObject; 061import org.ametys.web.repository.site.Site; 062import org.ametys.web.repository.site.SiteManager; 063 064/** 065 * {@link AmetysObject} for storing project informations. 066 */ 067@SolrAclCacheUninfluentialObject 068public class Project extends DefaultTraversableAmetysObject<ProjectFactory> implements ModifiableModelLessDataAwareAmetysObject, EventHolder 069{ 070 /** Project node type name. */ 071 public static final String NODE_TYPE = RepositoryConstants.NAMESPACE_PREFIX + ":project"; 072 073 /** Metadata name for project 's sites */ 074 public static final String DATA_SITES = "sites"; 075 076 private static final String __EXPLORER_NODE_NAME = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":resources"; 077 private static final String __DATA_TITLE = "title"; 078 private static final String __DATA_DESCRIPTION = "description"; 079 080 private static final String __DATA_MAILING_LIST = "mailingList"; 081 private static final String __DATA_CREATION = "creationDate"; 082 private static final String __DATA_MANAGERS = "managers"; 083 private static final String __DATA_MODULES = "modules"; 084 private static final String __DATA_INSCRIPTION_STATUS = "inscriptionStatus"; 085 private static final String __DATA_DEFAULT_PROFILE = "defaultProfile"; 086 private static final String __DATA_COVERIMAGE = "coverImage"; 087 private static final String __DATA_KEYWORDS = "keywords"; 088 private static final String __PLUGINS_NODE_NAME = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":plugins"; 089 private static final String __DATA_CATEGORIES = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":categories"; 090 091 /** 092 * The inscription status of the project 093 */ 094 public enum InscriptionStatus 095 { 096 /** Inscriptions are opened to anyone */ 097 OPEN("open"), 098 /** Inscriptions are moderated */ 099 MODERATED("moderated"), 100 /** Inscriptions are private */ 101 PRIVATE("private"); 102 103 private String _value; 104 105 private InscriptionStatus(String value) 106 { 107 this._value = value; 108 } 109 110 @Override 111 public String toString() 112 { 113 return _value; 114 } 115 116 /** 117 * Converts a string to an Inscription 118 * @param status The status to convert 119 * @return The status corresponding to the string or null if unknown 120 */ 121 public static InscriptionStatus createsFromString(String status) 122 { 123 for (InscriptionStatus v : InscriptionStatus.values()) 124 { 125 if (v.toString().equals(status)) 126 { 127 return v; 128 } 129 } 130 return null; 131 } 132 } 133 134 /** 135 * Creates a {@link Project}. 136 * @param node the node backing this {@link AmetysObject}. 137 * @param parentPath the parent path in the Ametys hierarchy. 138 * @param factory the {@link ProjectFactory} which creates the AmetysObject. 139 */ 140 public Project(Node node, String parentPath, ProjectFactory factory) 141 { 142 super(node, parentPath, factory); 143 } 144 145 public ModifiableModelLessDataHolder getDataHolder() 146 { 147 ModifiableRepositoryData repositoryData = new JCRRepositoryData(getNode()); 148 return new DefaultModifiableModelLessDataHolder(_getFactory().getProjectDataTypeExtensionPoint(), repositoryData); 149 } 150 151 /** 152 * Retrieves the title. 153 * @return the title. 154 * @throws AmetysRepositoryException if an error occurs. 155 */ 156 public String getTitle() throws AmetysRepositoryException 157 { 158 return getValue(__DATA_TITLE); 159 } 160 161 /** 162 * Set the title. 163 * @param title the title. 164 * @throws AmetysRepositoryException if an error occurs. 165 */ 166 public void setTitle(String title) throws AmetysRepositoryException 167 { 168 setValue(__DATA_TITLE, title); 169 } 170 171 /** 172 * Retrieves the description. 173 * @return the description. 174 * @throws AmetysRepositoryException if an error occurs. 175 */ 176 public String getDescription() throws AmetysRepositoryException 177 { 178 return getValue(__DATA_DESCRIPTION); 179 } 180 181 /** 182 * Set the description. 183 * @param description the description. 184 * @throws AmetysRepositoryException if an error occurs. 185 */ 186 public void setDescription(String description) throws AmetysRepositoryException 187 { 188 setValue(__DATA_DESCRIPTION, description); 189 } 190 191 /** 192 * Remove the description. 193 * @throws AmetysRepositoryException if an error occurs. 194 */ 195 public void removeDescription() throws AmetysRepositoryException 196 { 197 if (hasValue(__DATA_DESCRIPTION)) 198 { 199 removeValue(__DATA_DESCRIPTION); 200 } 201 } 202 203 /** 204 * Retrieves the explorer nodes. 205 * @return the explorer nodes or an empty {@link AmetysObjectIterable}. 206 * @throws AmetysRepositoryException if an error occurs. 207 */ 208 public ExplorerNode getExplorerRootNode() throws AmetysRepositoryException 209 { 210 return (ExplorerNode) getChild(__EXPLORER_NODE_NAME); 211 } 212 213 /** 214 * Retrieves the explorer nodes. 215 * @return the explorer nodes or an empty {@link AmetysObjectIterable}. 216 * @throws AmetysRepositoryException if an error occurs. 217 */ 218 public AmetysObjectIterable<ExplorerNode> getExplorerNodes() throws AmetysRepositoryException 219 { 220 return ((TraversableAmetysObject) getChild(__EXPLORER_NODE_NAME)).getChildren(); 221 } 222 223 224 /** 225 * Retrieves the mailing list. 226 * @return the mailing list. 227 * @throws AmetysRepositoryException if an error occurs. 228 */ 229 public String getMailingList() throws AmetysRepositoryException 230 { 231 return getValue(__DATA_MAILING_LIST); 232 } 233 234 /** 235 * Set the mailing list. 236 * @param mailingList the mailing list. 237 * @throws AmetysRepositoryException if an error occurs. 238 */ 239 public void setMailingList(String mailingList) throws AmetysRepositoryException 240 { 241 setValue(__DATA_MAILING_LIST, mailingList); 242 } 243 244 /** 245 * Remove the mailing list. 246 * @throws AmetysRepositoryException if an error occurs. 247 */ 248 public void removeMailingList() throws AmetysRepositoryException 249 { 250 if (hasValue(__DATA_MAILING_LIST)) 251 { 252 removeValue(__DATA_MAILING_LIST); 253 } 254 } 255 256 /** 257 * Retrieves the date of creation. 258 * @return the date of creation. 259 * @throws AmetysRepositoryException if an error occurs. 260 */ 261 public ZonedDateTime getCreationDate() throws AmetysRepositoryException 262 { 263 return getValue(__DATA_CREATION); 264 } 265 266 /** 267 * Set the date of creation. 268 * @param creationDate the date of creation 269 * @throws AmetysRepositoryException if an error occurs. 270 */ 271 public void setCreationDate(ZonedDateTime creationDate) throws AmetysRepositoryException 272 { 273 setValue(__DATA_CREATION, creationDate); 274 } 275 276 @Override 277 public Node getEventsRootNode() throws RepositoryException 278 { 279 return JCREventHelper.getEventsRootNode(getNode()); 280 } 281 282 @Override 283 public NodeIterator getEvents() throws RepositoryException 284 { 285 return JCREventHelper.getEvents(this); 286 } 287 288 /** 289 * Get the sites of the project 290 * @return The collection of sites 291 */ 292 public Collection<Site> getSites() 293 { 294 try 295 { 296 if (!hasValue(DATA_SITES)) 297 { 298 return new ArrayList<>(); 299 } 300 else 301 { 302 SiteManager siteManager = _getFactory()._getSiteManager(); 303 304 // Stream over the properties to retrieve the corresponding sites. 305 JCRRepositoryData sitesData = (JCRRepositoryData) new JCRRepositoryData(getNode()).getRepositoryData(DATA_SITES); 306 Node jcrSitesNode = sitesData.getNode(); 307 Iterator<Property> sitesIterator = jcrSitesNode.getProperties(); 308 Iterable<Property> sitesIterable = () -> sitesIterator; 309 310 return StreamSupport.stream(sitesIterable.spliterator(), false) 311 .map(p -> 312 { 313 try 314 { 315 return p.getNode().getName(); 316 } 317 catch (Exception e) 318 { 319 // site might not exist (anymore...) 320 return null; 321 } 322 }) 323 .filter(Objects::nonNull) 324 .map(siteName -> siteManager.getSite(siteName)) 325 .collect(Collectors.toList()); 326 } 327 } 328 catch (RepositoryException e) 329 { 330 return new ArrayList<>(); 331 } 332 } 333 334 /** 335 * Set the sites of the project 336 * @param sites The names of the site 337 */ 338 public void setSites(Collection<String> sites) 339 { 340 if (hasValue(DATA_SITES)) 341 { 342 removeValue(DATA_SITES); 343 } 344 345 JCRRepositoryData sitesData = (JCRRepositoryData) new JCRRepositoryData(getNode()).addRepositoryData(DATA_SITES, RepositoryConstants.NAMESPACE_PREFIX + ":compositeMetadata"); 346 Node jcrSitesNode = sitesData.getNode(); 347 SiteManager siteManager = _getFactory()._getSiteManager(); 348 349 // create weak references to site nodes 350 int[] propIdx = {0}; 351 sites.forEach(siteName -> 352 { 353 if (siteManager.hasSite(siteName)) 354 { 355 Site site = siteManager.getSite(siteName); 356 357 try 358 { 359 ValueFactory valueFactory = jcrSitesNode.getSession().getValueFactory(); 360 Value weakRefValue = valueFactory.createValue(site.getNode(), true); 361 362 propIdx[0]++; // increment index 363 jcrSitesNode.setProperty(Integer.toString(propIdx[0]), weakRefValue); 364 } 365 catch (RepositoryException e) 366 { 367 throw new AmetysRepositoryException("Unexpected repository exception", e); 368 } 369 } 370 }); 371 372 if (needsSave()) 373 { 374 saveChanges(); 375 } 376 } 377 378// /** 379// * Get the project path of the project 380// * The project path is composed of the project category names and the project name separated by slashes. 381// * e.g. cat1/cat2/project-name 382// * @return he project path of the project 383// */ 384// public String getProjectPath() 385// { 386// Deque<String> path = new ArrayDeque<>(); 387// path.addFirst(getName()); 388// 389// try 390// { 391// Node parentNode = getNode().getParent(); 392// 393// while (NodeTypeHelper.isNodeType(parentNode, "ametys:projectCategory")) 394// { 395// path.addFirst(parentNode.getName()); 396// parentNode = parentNode.getParent(); 397// } 398// 399// return String.join("/", path); 400// } 401// catch (RepositoryException e) 402// { 403// throw new AmetysRepositoryException("Unexpected repository exception while retrieving project path", e); 404// } 405// } 406 407 /** 408 * Get the project managers user identities 409 * @return The managers 410 */ 411 public UserIdentity[] getManagers() 412 { 413 return getValue(__DATA_MANAGERS, new UserIdentity[0]); 414 } 415 416 /** 417 * Set the project managers 418 * @param user The managers 419 */ 420 public void setManagers(UserIdentity[] user) 421 { 422 setValue(__DATA_MANAGERS, user); 423 } 424 425 /** 426 * Retrieve the list of activated modules for the project 427 * @return The list of modules ids 428 */ 429 public String[] getModules() 430 { 431 return getValue(__DATA_MODULES, new String[0]); 432 } 433 434 /** 435 * Set the list of activated modules for the project 436 * @param modules The list of modules 437 */ 438 public void setModules(String[] modules) 439 { 440 setValue(__DATA_MODULES, modules); 441 } 442 443 /** 444 * Add a module to the list of activated modules 445 * @param moduleId The module id 446 */ 447 public void addModule(String moduleId) 448 { 449 String[] modules = getValue(__DATA_MODULES); 450 if (!ArrayUtils.contains(modules, moduleId)) 451 { 452 setValue(__DATA_MODULES, modules == null ? new String[]{moduleId} : ArrayUtils.add(modules, moduleId)); 453 } 454 } 455 456 /** 457 * Remove a module from the list of activated modules 458 * @param moduleId The module id 459 */ 460 public void removeModule(String moduleId) 461 { 462 String[] modules = getValue(__DATA_MODULES); 463 if (ArrayUtils.contains(modules, moduleId)) 464 { 465 setValue(__DATA_MODULES, ArrayUtils.removeElement(modules, moduleId)); 466 } 467 } 468 469 /** 470 * Get the inscription status of the project 471 * @return The inscription status 472 */ 473 public InscriptionStatus getInscriptionStatus() 474 { 475 if (hasValue(__DATA_INSCRIPTION_STATUS)) 476 { 477 return InscriptionStatus.createsFromString(getValue(__DATA_INSCRIPTION_STATUS)); 478 } 479 return InscriptionStatus.PRIVATE; 480 } 481 482 /** 483 * Set the inscription status of the project 484 * @param inscriptionStatus The inscription status 485 */ 486 public void setInscriptionStatus(String inscriptionStatus) 487 { 488 if (inscriptionStatus != null && InscriptionStatus.createsFromString(inscriptionStatus) != null) 489 { 490 setValue(__DATA_INSCRIPTION_STATUS, inscriptionStatus); 491 } 492 else if (hasValue(__DATA_INSCRIPTION_STATUS)) 493 { 494 removeValue(__DATA_INSCRIPTION_STATUS); 495 } 496 } 497 498 /** 499 * Set the inscription status of the project 500 * @param inscriptionStatus The inscription status 501 */ 502 public void setInscriptionStatus(InscriptionStatus inscriptionStatus) 503 { 504 if (inscriptionStatus != null) 505 { 506 setValue(__DATA_INSCRIPTION_STATUS, inscriptionStatus.toString()); 507 } 508 else if (hasValue(__DATA_INSCRIPTION_STATUS)) 509 { 510 removeValue(__DATA_INSCRIPTION_STATUS); 511 } 512 } 513 514 /** 515 * Get the default profile for new members of the project 516 * @return The default profile 517 */ 518 public String getDefaultProfile() 519 { 520 return getValue(__DATA_DEFAULT_PROFILE); 521 } 522 523 524 /** 525 * Set the default profile for the members of the project 526 * @param profileId The ID of the profile 527 */ 528 public void setDefaultProfile(String profileId) 529 { 530 if (profileId != null) 531 { 532 setValue(__DATA_DEFAULT_PROFILE, profileId); 533 } 534 else if (hasValue(__DATA_DEFAULT_PROFILE)) 535 { 536 removeValue(__DATA_DEFAULT_PROFILE); 537 } 538 } 539 540 /** 541 * Get the root for plugins 542 * @return the root for plugins 543 * @throws AmetysRepositoryException if an error occurs. 544 */ 545 public ModifiableTraversableAmetysObject getRootPlugins () throws AmetysRepositoryException 546 { 547 return (ModifiableTraversableAmetysObject) getChild(__PLUGINS_NODE_NAME); 548 } 549 550 /** 551 * Retrieve the list of tags 552 * @return The list of tags 553 * @throws AmetysRepositoryException if an error occurs 554 */ 555 public Set<String> getTags() throws AmetysRepositoryException 556 { 557 return TaggableAmetysObjectHelper.getTags(this); 558 } 559 560 /** 561 * Add a tag to the project 562 * @param tag The tag 563 * @throws AmetysRepositoryException if an error occurs 564 */ 565 public void tag(String tag) throws AmetysRepositoryException 566 { 567 TaggableAmetysObjectHelper.tag(this, tag); 568 } 569 570 /** 571 * Remove a tag from the project 572 * @param tag The tag 573 * @throws AmetysRepositoryException if an error occurs 574 */ 575 public void untag(String tag) throws AmetysRepositoryException 576 { 577 TaggableAmetysObjectHelper.untag(this, tag); 578 } 579 580 /** 581 * Set the project tags 582 * @param tags The list of tags 583 */ 584 public void setTags(List<String> tags) 585 { 586 Set<String> currentTags = getTags(); 587 // remove old tags not selected 588 currentTags.stream() 589 .filter(tag -> !tags.contains(tag)) 590 .forEach(tag -> untag(tag)); 591 592 // add new selected tags 593 tags.stream() 594 .filter(tag -> !currentTags.contains(tag)) 595 .forEach(tag -> tag(tag)); 596 } 597 598 599 /** 600 * Retrieve the list of categories 601 * @return The categories 602 * @throws AmetysRepositoryException if an error occurs 603 */ 604 public Set<String> getCategories() throws AmetysRepositoryException 605 { 606 return TaggableAmetysObjectHelper.getTags(this, __DATA_CATEGORIES); 607 } 608 609 /** 610 * Add a category to the project 611 * @param category The category 612 * @throws AmetysRepositoryException if an error occurs 613 */ 614 public void addCategory(String category) throws AmetysRepositoryException 615 { 616 TaggableAmetysObjectHelper.tag(this, category, __DATA_CATEGORIES); 617 } 618 619 /** 620 * Remove a category from the project 621 * @param category The category 622 * @throws AmetysRepositoryException if an error occurs 623 */ 624 public void removeCategory(String category) throws AmetysRepositoryException 625 { 626 TaggableAmetysObjectHelper.untag(this, category, __DATA_CATEGORIES); 627 } 628 629 /** 630 * Set the category tags of the project 631 * @param categoryTags The category tags 632 */ 633 public void setCategoryTags(List<String> categoryTags) 634 { 635 Set<String> currentCategories = getCategories(); 636 // remove old tags not selected 637 currentCategories.stream() 638 .filter(category -> !categoryTags.contains(category)) 639 .forEach(category -> removeCategory(category)); 640 641 // add new selected tags 642 categoryTags.stream() 643 .filter(category -> !currentCategories.contains(category)) 644 .forEach(category -> addCategory(category)); 645 } 646 647 /** 648 * Set the cover image of the site 649 * @param is The input stream of the cover image 650 * @param mimeType The mimetype of the cover image 651 * @param filename The filename of the cover image 652 * @param lastModificationDate The last modification date of the cover image 653 * @throws IOException if an error occurs while setting the cover image 654 */ 655 public void setCoverImage(InputStream is, String mimeType, String filename, ZonedDateTime lastModificationDate) throws IOException 656 { 657 if (is != null) 658 { 659 Binary coverImage = new Binary(); 660 coverImage.setInputStream(is); 661 Optional.ofNullable(mimeType).ifPresent(coverImage::setMimeType); 662 Optional.ofNullable(filename).ifPresent(coverImage::setFilename); 663 Optional.ofNullable(lastModificationDate).ifPresent(coverImage::setLastModificationDate); 664 setValue(__DATA_COVERIMAGE, coverImage); 665 } 666 else if (hasValue(__DATA_COVERIMAGE)) 667 { 668 removeValue(__DATA_COVERIMAGE); 669 } 670 } 671 672 /** 673 * Returns the cover image of the project 674 * @return the cover image of the project 675 */ 676 public Binary getCoverImage() 677 { 678 return getValue(__DATA_COVERIMAGE); 679 } 680 681 /** 682 * Generates SAX events for the project's cover image 683 * @param contentHandler the {@link ContentHandler} that will receive the SAX events 684 * @throws SAXException if error occurs during the SAX events generation 685 * @throws IOException if an I/O error occurs while reading the cover image 686 */ 687 public void coverImageToSAX(ContentHandler contentHandler) throws SAXException, IOException 688 { 689 dataToSAX(contentHandler, __DATA_COVERIMAGE); 690 } 691 692 /** 693 * Retrieve the list of keywords for the project 694 * @return The list of keywords 695 */ 696 public String[] getKeywords() 697 { 698 return getValue(__DATA_KEYWORDS, new String[0]); 699 } 700 701 /** 702 * Set the list of keywordss for the project 703 * @param keywords The list of keywords 704 */ 705 public void setKeywords(String[] keywords) 706 { 707 setValue(__DATA_KEYWORDS, keywords); 708 } 709}