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.core.user.UserIdentity; 045import org.ametys.plugins.explorer.ExplorerNode; 046import org.ametys.plugins.repository.AmetysObject; 047import org.ametys.plugins.repository.AmetysObjectIterable; 048import org.ametys.plugins.repository.AmetysRepositoryException; 049import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 050import org.ametys.plugins.repository.RepositoryConstants; 051import org.ametys.plugins.repository.TraversableAmetysObject; 052import org.ametys.plugins.repository.data.ametysobject.ModifiableModelLessDataAwareAmetysObject; 053import org.ametys.plugins.repository.data.holder.ModifiableModelLessDataHolder; 054import org.ametys.plugins.repository.data.holder.impl.DefaultModifiableModelLessDataHolder; 055import org.ametys.plugins.repository.data.repositorydata.ModifiableRepositoryData; 056import org.ametys.plugins.repository.data.repositorydata.impl.JCRRepositoryData; 057import org.ametys.plugins.repository.events.EventHolder; 058import org.ametys.plugins.repository.events.JCREventHelper; 059import org.ametys.plugins.repository.jcr.DefaultTraversableAmetysObject; 060import org.ametys.plugins.repository.tag.TaggableAmetysObjectHelper; 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 removeValue(__DATA_DESCRIPTION); 198 } 199 200 /** 201 * Retrieves the explorer nodes. 202 * @return the explorer nodes or an empty {@link AmetysObjectIterable}. 203 * @throws AmetysRepositoryException if an error occurs. 204 */ 205 public ExplorerNode getExplorerRootNode() throws AmetysRepositoryException 206 { 207 return (ExplorerNode) getChild(__EXPLORER_NODE_NAME); 208 } 209 210 /** 211 * Retrieves the explorer nodes. 212 * @return the explorer nodes or an empty {@link AmetysObjectIterable}. 213 * @throws AmetysRepositoryException if an error occurs. 214 */ 215 public AmetysObjectIterable<ExplorerNode> getExplorerNodes() throws AmetysRepositoryException 216 { 217 return ((TraversableAmetysObject) getChild(__EXPLORER_NODE_NAME)).getChildren(); 218 } 219 220 221 /** 222 * Retrieves the mailing list. 223 * @return the mailing list. 224 * @throws AmetysRepositoryException if an error occurs. 225 */ 226 public String getMailingList() throws AmetysRepositoryException 227 { 228 return getValue(__DATA_MAILING_LIST); 229 } 230 231 /** 232 * Set the mailing list. 233 * @param mailingList the mailing list. 234 * @throws AmetysRepositoryException if an error occurs. 235 */ 236 public void setMailingList(String mailingList) throws AmetysRepositoryException 237 { 238 setValue(__DATA_MAILING_LIST, mailingList); 239 } 240 241 /** 242 * Remove the mailing list. 243 * @throws AmetysRepositoryException if an error occurs. 244 */ 245 public void removeMailingList() throws AmetysRepositoryException 246 { 247 removeValue(__DATA_MAILING_LIST); 248 } 249 250 /** 251 * Retrieves the date of creation. 252 * @return the date of creation. 253 * @throws AmetysRepositoryException if an error occurs. 254 */ 255 public ZonedDateTime getCreationDate() throws AmetysRepositoryException 256 { 257 return getValue(__DATA_CREATION); 258 } 259 260 /** 261 * Set the date of creation. 262 * @param creationDate the date of creation 263 * @throws AmetysRepositoryException if an error occurs. 264 */ 265 public void setCreationDate(ZonedDateTime creationDate) throws AmetysRepositoryException 266 { 267 setValue(__DATA_CREATION, creationDate); 268 } 269 270 @Override 271 public Node getEventsRootNode() throws RepositoryException 272 { 273 return JCREventHelper.getEventsRootNode(getNode()); 274 } 275 276 @Override 277 public NodeIterator getEvents() throws RepositoryException 278 { 279 return JCREventHelper.getEvents(this); 280 } 281 282 /** 283 * Get the sites of the project 284 * @return The collection of sites 285 */ 286 public Collection<Site> getSites() 287 { 288 try 289 { 290 if (!hasValueOrEmpty(DATA_SITES)) 291 { 292 return new ArrayList<>(); 293 } 294 else 295 { 296 SiteManager siteManager = _getFactory()._getSiteManager(); 297 298 // Stream over the properties to retrieve the corresponding sites. 299 JCRRepositoryData sitesData = (JCRRepositoryData) new JCRRepositoryData(getNode()).getRepositoryData(DATA_SITES); 300 Node jcrSitesNode = sitesData.getNode(); 301 Iterator<Property> sitesIterator = jcrSitesNode.getProperties(); 302 Iterable<Property> sitesIterable = () -> sitesIterator; 303 304 return StreamSupport.stream(sitesIterable.spliterator(), false) 305 .map(p -> 306 { 307 try 308 { 309 return p.getNode().getName(); 310 } 311 catch (Exception e) 312 { 313 // site might not exist (anymore...) 314 return null; 315 } 316 }) 317 .filter(Objects::nonNull) 318 .map(siteName -> siteManager.getSite(siteName)) 319 .collect(Collectors.toList()); 320 } 321 } 322 catch (RepositoryException e) 323 { 324 return new ArrayList<>(); 325 } 326 } 327 328 /** 329 * Set the sites of the project 330 * @param sites The names of the site 331 */ 332 public void setSites(Collection<String> sites) 333 { 334 removeValue(DATA_SITES); 335 336 JCRRepositoryData sitesData = (JCRRepositoryData) new JCRRepositoryData(getNode()).addRepositoryData(DATA_SITES, RepositoryConstants.NAMESPACE_PREFIX + ":compositeMetadata"); 337 Node jcrSitesNode = sitesData.getNode(); 338 SiteManager siteManager = _getFactory()._getSiteManager(); 339 340 // create weak references to site nodes 341 int[] propIdx = {0}; 342 sites.forEach(siteName -> 343 { 344 if (siteManager.hasSite(siteName)) 345 { 346 Site site = siteManager.getSite(siteName); 347 348 try 349 { 350 ValueFactory valueFactory = jcrSitesNode.getSession().getValueFactory(); 351 Value weakRefValue = valueFactory.createValue(site.getNode(), true); 352 353 propIdx[0]++; // increment index 354 jcrSitesNode.setProperty(Integer.toString(propIdx[0]), weakRefValue); 355 } 356 catch (RepositoryException e) 357 { 358 throw new AmetysRepositoryException("Unexpected repository exception", e); 359 } 360 } 361 }); 362 363 if (needsSave()) 364 { 365 saveChanges(); 366 } 367 } 368 369// /** 370// * Get the project path of the project 371// * The project path is composed of the project category names and the project name separated by slashes. 372// * e.g. cat1/cat2/project-name 373// * @return he project path of the project 374// */ 375// public String getProjectPath() 376// { 377// Deque<String> path = new ArrayDeque<>(); 378// path.addFirst(getName()); 379// 380// try 381// { 382// Node parentNode = getNode().getParent(); 383// 384// while (NodeTypeHelper.isNodeType(parentNode, "ametys:projectCategory")) 385// { 386// path.addFirst(parentNode.getName()); 387// parentNode = parentNode.getParent(); 388// } 389// 390// return String.join("/", path); 391// } 392// catch (RepositoryException e) 393// { 394// throw new AmetysRepositoryException("Unexpected repository exception while retrieving project path", e); 395// } 396// } 397 398 /** 399 * Get the project managers user identities 400 * @return The managers 401 */ 402 public UserIdentity[] getManagers() 403 { 404 return getValue(__DATA_MANAGERS, new UserIdentity[0]); 405 } 406 407 /** 408 * Set the project managers 409 * @param user The managers 410 */ 411 public void setManagers(UserIdentity[] user) 412 { 413 setValue(__DATA_MANAGERS, user); 414 } 415 416 /** 417 * Retrieve the list of activated modules for the project 418 * @return The list of modules ids 419 */ 420 public String[] getModules() 421 { 422 return getValue(__DATA_MODULES, new String[0]); 423 } 424 425 /** 426 * Set the list of activated modules for the project 427 * @param modules The list of modules 428 */ 429 public void setModules(String[] modules) 430 { 431 setValue(__DATA_MODULES, modules); 432 } 433 434 /** 435 * Add a module to the list of activated modules 436 * @param moduleId The module id 437 */ 438 public void addModule(String moduleId) 439 { 440 String[] modules = getValue(__DATA_MODULES); 441 if (!ArrayUtils.contains(modules, moduleId)) 442 { 443 setValue(__DATA_MODULES, modules == null ? new String[]{moduleId} : ArrayUtils.add(modules, moduleId)); 444 } 445 } 446 447 /** 448 * Remove a module from the list of activated modules 449 * @param moduleId The module id 450 */ 451 public void removeModule(String moduleId) 452 { 453 String[] modules = getValue(__DATA_MODULES); 454 if (ArrayUtils.contains(modules, moduleId)) 455 { 456 setValue(__DATA_MODULES, ArrayUtils.removeElement(modules, moduleId)); 457 } 458 } 459 460 /** 461 * Get the inscription status of the project 462 * @return The inscription status 463 */ 464 public InscriptionStatus getInscriptionStatus() 465 { 466 if (hasValue(__DATA_INSCRIPTION_STATUS)) 467 { 468 return InscriptionStatus.createsFromString(getValue(__DATA_INSCRIPTION_STATUS)); 469 } 470 return InscriptionStatus.PRIVATE; 471 } 472 473 /** 474 * Set the inscription status of the project 475 * @param inscriptionStatus The inscription status 476 */ 477 public void setInscriptionStatus(String inscriptionStatus) 478 { 479 if (inscriptionStatus != null && InscriptionStatus.createsFromString(inscriptionStatus) != null) 480 { 481 setValue(__DATA_INSCRIPTION_STATUS, inscriptionStatus); 482 } 483 else 484 { 485 removeValue(__DATA_INSCRIPTION_STATUS); 486 } 487 } 488 489 /** 490 * Set the inscription status of the project 491 * @param inscriptionStatus The inscription status 492 */ 493 public void setInscriptionStatus(InscriptionStatus inscriptionStatus) 494 { 495 if (inscriptionStatus != null) 496 { 497 setValue(__DATA_INSCRIPTION_STATUS, inscriptionStatus.toString()); 498 } 499 else 500 { 501 removeValue(__DATA_INSCRIPTION_STATUS); 502 } 503 } 504 505 /** 506 * Get the default profile for new members of the project 507 * @return The default profile 508 */ 509 public String getDefaultProfile() 510 { 511 return getValue(__DATA_DEFAULT_PROFILE); 512 } 513 514 515 /** 516 * Set the default profile for the members of the project 517 * @param profileId The ID of the profile 518 */ 519 public void setDefaultProfile(String profileId) 520 { 521 if (profileId != null) 522 { 523 setValue(__DATA_DEFAULT_PROFILE, profileId); 524 } 525 else 526 { 527 removeValue(__DATA_DEFAULT_PROFILE); 528 } 529 } 530 531 /** 532 * Get the root for plugins 533 * @return the root for plugins 534 * @throws AmetysRepositoryException if an error occurs. 535 */ 536 public ModifiableTraversableAmetysObject getRootPlugins () throws AmetysRepositoryException 537 { 538 return (ModifiableTraversableAmetysObject) getChild(__PLUGINS_NODE_NAME); 539 } 540 541 /** 542 * Retrieve the list of tags 543 * @return The list of tags 544 * @throws AmetysRepositoryException if an error occurs 545 */ 546 public Set<String> getTags() throws AmetysRepositoryException 547 { 548 return TaggableAmetysObjectHelper.getTags(this); 549 } 550 551 /** 552 * Add a tag to the project 553 * @param tag The tag 554 * @throws AmetysRepositoryException if an error occurs 555 */ 556 public void tag(String tag) throws AmetysRepositoryException 557 { 558 TaggableAmetysObjectHelper.tag(this, tag); 559 } 560 561 /** 562 * Remove a tag from the project 563 * @param tag The tag 564 * @throws AmetysRepositoryException if an error occurs 565 */ 566 public void untag(String tag) throws AmetysRepositoryException 567 { 568 TaggableAmetysObjectHelper.untag(this, tag); 569 } 570 571 /** 572 * Set the project tags 573 * @param tags The list of tags 574 */ 575 public void setTags(List<String> tags) 576 { 577 Set<String> currentTags = getTags(); 578 // remove old tags not selected 579 currentTags.stream() 580 .filter(tag -> !tags.contains(tag)) 581 .forEach(tag -> untag(tag)); 582 583 // add new selected tags 584 tags.stream() 585 .filter(tag -> !currentTags.contains(tag)) 586 .forEach(tag -> tag(tag)); 587 } 588 589 590 /** 591 * Retrieve the list of categories 592 * @return The categories 593 * @throws AmetysRepositoryException if an error occurs 594 */ 595 public Set<String> getCategories() throws AmetysRepositoryException 596 { 597 return TaggableAmetysObjectHelper.getTags(this, __DATA_CATEGORIES); 598 } 599 600 /** 601 * Add a category to the project 602 * @param category The category 603 * @throws AmetysRepositoryException if an error occurs 604 */ 605 public void addCategory(String category) throws AmetysRepositoryException 606 { 607 TaggableAmetysObjectHelper.tag(this, category, __DATA_CATEGORIES); 608 } 609 610 /** 611 * Remove a category from the project 612 * @param category The category 613 * @throws AmetysRepositoryException if an error occurs 614 */ 615 public void removeCategory(String category) throws AmetysRepositoryException 616 { 617 TaggableAmetysObjectHelper.untag(this, category, __DATA_CATEGORIES); 618 } 619 620 /** 621 * Set the category tags of the project 622 * @param categoryTags The category tags 623 */ 624 public void setCategoryTags(List<String> categoryTags) 625 { 626 Set<String> currentCategories = getCategories(); 627 // remove old tags not selected 628 currentCategories.stream() 629 .filter(category -> !categoryTags.contains(category)) 630 .forEach(category -> removeCategory(category)); 631 632 // add new selected tags 633 categoryTags.stream() 634 .filter(category -> !currentCategories.contains(category)) 635 .forEach(category -> addCategory(category)); 636 } 637 638 /** 639 * Set the cover image of the site 640 * @param is The input stream of the cover image 641 * @param mimeType The mimetype of the cover image 642 * @param filename The filename of the cover image 643 * @param lastModificationDate The last modification date of the cover image 644 * @throws IOException if an error occurs while setting the cover image 645 */ 646 public void setCoverImage(InputStream is, String mimeType, String filename, ZonedDateTime lastModificationDate) throws IOException 647 { 648 if (is != null) 649 { 650 Binary coverImage = new Binary(); 651 coverImage.setInputStream(is); 652 Optional.ofNullable(mimeType).ifPresent(coverImage::setMimeType); 653 Optional.ofNullable(filename).ifPresent(coverImage::setFilename); 654 Optional.ofNullable(lastModificationDate).ifPresent(coverImage::setLastModificationDate); 655 setValue(__DATA_COVERIMAGE, coverImage); 656 } 657 else 658 { 659 removeValue(__DATA_COVERIMAGE); 660 } 661 } 662 663 /** 664 * Returns the cover image of the project 665 * @return the cover image of the project 666 */ 667 public Binary getCoverImage() 668 { 669 return getValue(__DATA_COVERIMAGE); 670 } 671 672 /** 673 * Generates SAX events for the project's cover image 674 * @param contentHandler the {@link ContentHandler} that will receive the SAX events 675 * @throws SAXException if error occurs during the SAX events generation 676 * @throws IOException if an I/O error occurs while reading the cover image 677 */ 678 public void coverImageToSAX(ContentHandler contentHandler) throws SAXException, IOException 679 { 680 dataToSAX(contentHandler, __DATA_COVERIMAGE); 681 } 682 683 /** 684 * Retrieve the list of keywords for the project 685 * @return The list of keywords 686 */ 687 public String[] getKeywords() 688 { 689 return getValue(__DATA_KEYWORDS, new String[0]); 690 } 691 692 /** 693 * Set the list of keywordss for the project 694 * @param keywords The list of keywords 695 */ 696 public void setKeywords(String[] keywords) 697 { 698 setValue(__DATA_KEYWORDS, keywords); 699 } 700}