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 * Get the default profile for new members of the project 500 * @return The default profile 501 */ 502 public String getDefaultProfile() 503 { 504 return getValue(__DATA_DEFAULT_PROFILE); 505 } 506 507 508 /** 509 * Set the default profile for the members of the project 510 * @param profileId The ID of the profile 511 */ 512 public void setDefaultProfile(String profileId) 513 { 514 if (profileId != null) 515 { 516 setValue(__DATA_DEFAULT_PROFILE, profileId); 517 } 518 else if (hasValue(__DATA_DEFAULT_PROFILE)) 519 { 520 removeValue(__DATA_DEFAULT_PROFILE); 521 } 522 } 523 524 /** 525 * Get the root for plugins 526 * @return the root for plugins 527 * @throws AmetysRepositoryException if an error occurs. 528 */ 529 public ModifiableTraversableAmetysObject getRootPlugins () throws AmetysRepositoryException 530 { 531 return (ModifiableTraversableAmetysObject) getChild(__PLUGINS_NODE_NAME); 532 } 533 534 /** 535 * Retrieve the list of tags 536 * @return The list of tags 537 * @throws AmetysRepositoryException if an error occurs 538 */ 539 public Set<String> getTags() throws AmetysRepositoryException 540 { 541 return TaggableAmetysObjectHelper.getTags(this); 542 } 543 544 /** 545 * Add a tag to the project 546 * @param tag The tag 547 * @throws AmetysRepositoryException if an error occurs 548 */ 549 public void tag(String tag) throws AmetysRepositoryException 550 { 551 TaggableAmetysObjectHelper.tag(this, tag); 552 } 553 554 /** 555 * Remove a tag from the project 556 * @param tag The tag 557 * @throws AmetysRepositoryException if an error occurs 558 */ 559 public void untag(String tag) throws AmetysRepositoryException 560 { 561 TaggableAmetysObjectHelper.untag(this, tag); 562 } 563 564 /** 565 * Set the project tags 566 * @param tags The list of tags 567 */ 568 public void setTags(List<String> tags) 569 { 570 Set<String> currentTags = getTags(); 571 // remove old tags not selected 572 currentTags.stream() 573 .filter(tag -> !tags.contains(tag)) 574 .forEach(tag -> untag(tag)); 575 576 // add new selected tags 577 tags.stream() 578 .filter(tag -> !currentTags.contains(tag)) 579 .forEach(tag -> tag(tag)); 580 } 581 582 583 /** 584 * Retrieve the list of categories 585 * @return The categories 586 * @throws AmetysRepositoryException if an error occurs 587 */ 588 public Set<String> getCategories() throws AmetysRepositoryException 589 { 590 return TaggableAmetysObjectHelper.getTags(this, __DATA_CATEGORIES); 591 } 592 593 /** 594 * Add a category to the project 595 * @param category The category 596 * @throws AmetysRepositoryException if an error occurs 597 */ 598 public void addCategory(String category) throws AmetysRepositoryException 599 { 600 TaggableAmetysObjectHelper.tag(this, category, __DATA_CATEGORIES); 601 } 602 603 /** 604 * Remove a category from the project 605 * @param category The category 606 * @throws AmetysRepositoryException if an error occurs 607 */ 608 public void removeCategory(String category) throws AmetysRepositoryException 609 { 610 TaggableAmetysObjectHelper.untag(this, category, __DATA_CATEGORIES); 611 } 612 613 /** 614 * Set the category tags of the project 615 * @param categoryTags The category tags 616 */ 617 public void setCategoryTags(List<String> categoryTags) 618 { 619 Set<String> currentCategories = getCategories(); 620 // remove old tags not selected 621 currentCategories.stream() 622 .filter(category -> !categoryTags.contains(category)) 623 .forEach(category -> removeCategory(category)); 624 625 // add new selected tags 626 categoryTags.stream() 627 .filter(category -> !currentCategories.contains(category)) 628 .forEach(category -> addCategory(category)); 629 } 630 631 /** 632 * Set the cover image of the site 633 * @param is The input stream of the cover image 634 * @param mimeType The mimetype of the cover image 635 * @param filename The filename of the cover image 636 * @param lastModificationDate The last modification date of the cover image 637 * @throws IOException if an error occurs while setting the cover image 638 */ 639 public void setCoverImage(InputStream is, String mimeType, String filename, ZonedDateTime lastModificationDate) throws IOException 640 { 641 if (is != null) 642 { 643 Binary coverImage = new Binary(); 644 coverImage.setInputStream(is); 645 Optional.ofNullable(mimeType).ifPresent(coverImage::setMimeType); 646 Optional.ofNullable(filename).ifPresent(coverImage::setFilename); 647 Optional.ofNullable(lastModificationDate).ifPresent(coverImage::setLastModificationDate); 648 setValue(__DATA_COVERIMAGE, coverImage); 649 } 650 else if (hasValue(__DATA_COVERIMAGE)) 651 { 652 removeValue(__DATA_COVERIMAGE); 653 } 654 } 655 656 /** 657 * Returns the cover image of the project 658 * @return the cover image of the project 659 */ 660 public Binary getCoverImage() 661 { 662 return getValue(__DATA_COVERIMAGE); 663 } 664 665 /** 666 * Generates SAX events for the project's cover image 667 * @param contentHandler the {@link ContentHandler} that will receive the SAX events 668 * @throws SAXException if error occurs during the SAX events generation 669 * @throws IOException if an I/O error occurs while reading the cover image 670 */ 671 public void coverImageToSAX(ContentHandler contentHandler) throws SAXException, IOException 672 { 673 dataToSAX(contentHandler, __DATA_COVERIMAGE); 674 } 675 676 /** 677 * Retrieve the list of keywords for the project 678 * @return The list of keywords 679 */ 680 public String[] getKeywords() 681 { 682 return getValue(__DATA_KEYWORDS, new String[0]); 683 } 684 685 /** 686 * Set the list of keywordss for the project 687 * @param keywords The list of keywords 688 */ 689 public void setKeywords(String[] keywords) 690 { 691 setValue(__DATA_KEYWORDS, keywords); 692 } 693}