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.List; 022import java.util.Optional; 023import java.util.Set; 024 025import javax.jcr.ItemNotFoundException; 026import javax.jcr.Node; 027import javax.jcr.PathNotFoundException; 028import javax.jcr.Property; 029import javax.jcr.RepositoryException; 030import javax.jcr.Value; 031import javax.jcr.ValueFactory; 032 033import org.apache.commons.lang3.ArrayUtils; 034import org.xml.sax.ContentHandler; 035import org.xml.sax.SAXException; 036 037import org.ametys.cms.data.Binary; 038import org.ametys.cms.indexing.solr.SolrAclCacheUninfluentialObject; 039import org.ametys.core.user.UserIdentity; 040import org.ametys.plugins.explorer.ExplorerNode; 041import org.ametys.plugins.repository.AmetysObject; 042import org.ametys.plugins.repository.AmetysObjectIterable; 043import org.ametys.plugins.repository.AmetysRepositoryException; 044import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 045import org.ametys.plugins.repository.RepositoryConstants; 046import org.ametys.plugins.repository.TraversableAmetysObject; 047import org.ametys.plugins.repository.activities.ActivityHolder; 048import org.ametys.plugins.repository.activities.ActivityHolderAmetysObject; 049import org.ametys.plugins.repository.activities.DefaultActivityHolder; 050import org.ametys.plugins.repository.data.ametysobject.ModifiableModelLessDataAwareAmetysObject; 051import org.ametys.plugins.repository.data.holder.ModifiableModelLessDataHolder; 052import org.ametys.plugins.repository.data.holder.impl.DefaultModifiableModelLessDataHolder; 053import org.ametys.plugins.repository.data.repositorydata.ModifiableRepositoryData; 054import org.ametys.plugins.repository.data.repositorydata.impl.JCRRepositoryData; 055import org.ametys.plugins.repository.jcr.DefaultTraversableAmetysObject; 056import org.ametys.plugins.repository.tag.TaggableAmetysObjectHelper; 057import org.ametys.web.repository.site.Site; 058import org.ametys.web.repository.site.SiteManager; 059 060/** 061 * {@link AmetysObject} for storing project informations. 062 */ 063@SolrAclCacheUninfluentialObject 064public class Project extends DefaultTraversableAmetysObject<ProjectFactory> implements ModifiableModelLessDataAwareAmetysObject, ActivityHolderAmetysObject 065{ 066 /** Project node type name. */ 067 public static final String NODE_TYPE = RepositoryConstants.NAMESPACE_PREFIX + ":project"; 068 069 /** Attribute name for project 's site */ 070 public static final String DATA_SITE = "site"; 071 072 private static final String __EXPLORER_NODE_NAME = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":resources"; 073 private static final String __DATA_TITLE = "title"; 074 private static final String __DATA_DESCRIPTION = "description"; 075 076 private static final String __DATA_MAILING_LIST = "mailingList"; 077 private static final String __DATA_CREATION = "creationDate"; 078 private static final String __DATA_MANAGERS = "managers"; 079 private static final String __DATA_MODULES = "modules"; 080 private static final String __DATA_INSCRIPTION_STATUS = "inscriptionStatus"; 081 private static final String __DATA_DEFAULT_PROFILE = "defaultProfile"; 082 private static final String __DATA_COVERIMAGE = "coverImage"; 083 private static final String __DATA_KEYWORDS = "keywords"; 084 private static final String __PLUGINS_NODE_NAME = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":plugins"; 085 private static final String __DATA_CATEGORIES = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":categories"; 086 087 /** 088 * The inscription status of the project 089 */ 090 public enum InscriptionStatus 091 { 092 /** Inscriptions are opened to anyone */ 093 OPEN("open"), 094 /** Inscriptions are moderated */ 095 MODERATED("moderated"), 096 /** Inscriptions are private */ 097 PRIVATE("private"); 098 099 private String _value; 100 101 private InscriptionStatus(String value) 102 { 103 this._value = value; 104 } 105 106 @Override 107 public String toString() 108 { 109 return _value; 110 } 111 112 /** 113 * Converts a string to an Inscription 114 * @param status The status to convert 115 * @return The status corresponding to the string or null if unknown 116 */ 117 public static InscriptionStatus createsFromString(String status) 118 { 119 for (InscriptionStatus v : InscriptionStatus.values()) 120 { 121 if (v.toString().equals(status)) 122 { 123 return v; 124 } 125 } 126 return null; 127 } 128 } 129 130 /** 131 * Creates a {@link Project}. 132 * @param node the node backing this {@link AmetysObject}. 133 * @param parentPath the parent path in the Ametys hierarchy. 134 * @param factory the {@link ProjectFactory} which creates the AmetysObject. 135 */ 136 public Project(Node node, String parentPath, ProjectFactory factory) 137 { 138 super(node, parentPath, factory); 139 } 140 141 public ModifiableModelLessDataHolder getDataHolder() 142 { 143 ModifiableRepositoryData repositoryData = new JCRRepositoryData(getNode()); 144 return new DefaultModifiableModelLessDataHolder(_getFactory().getProjectDataTypeExtensionPoint(), repositoryData); 145 } 146 147 public ActivityHolder getActivityHolder() throws RepositoryException 148 { 149 // The child node is provided by the activity-holder nodetype so we are sure it is there 150 return new DefaultActivityHolder(getChild(ACTIVITIES_ROOT_NODE_NAME), _getFactory()); 151 } 152 153 154 /** 155 * Retrieves the title. 156 * @return the title. 157 * @throws AmetysRepositoryException if an error occurs. 158 */ 159 public String getTitle() throws AmetysRepositoryException 160 { 161 return getValue(__DATA_TITLE); 162 } 163 164 /** 165 * Set the title. 166 * @param title the title. 167 * @throws AmetysRepositoryException if an error occurs. 168 */ 169 public void setTitle(String title) throws AmetysRepositoryException 170 { 171 setValue(__DATA_TITLE, title); 172 } 173 174 /** 175 * Retrieves the description. 176 * @return the description. 177 * @throws AmetysRepositoryException if an error occurs. 178 */ 179 public String getDescription() throws AmetysRepositoryException 180 { 181 return getValue(__DATA_DESCRIPTION); 182 } 183 184 /** 185 * Set the description. 186 * @param description the description. 187 * @throws AmetysRepositoryException if an error occurs. 188 */ 189 public void setDescription(String description) throws AmetysRepositoryException 190 { 191 setValue(__DATA_DESCRIPTION, description); 192 } 193 194 /** 195 * Remove the description. 196 * @throws AmetysRepositoryException if an error occurs. 197 */ 198 public void removeDescription() throws AmetysRepositoryException 199 { 200 removeValue(__DATA_DESCRIPTION); 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 removeValue(__DATA_MAILING_LIST); 251 } 252 253 /** 254 * Retrieves the date of creation. 255 * @return the date of creation. 256 * @throws AmetysRepositoryException if an error occurs. 257 */ 258 public ZonedDateTime getCreationDate() throws AmetysRepositoryException 259 { 260 return getValue(__DATA_CREATION); 261 } 262 263 /** 264 * Set the date of creation. 265 * @param creationDate the date of creation 266 * @throws AmetysRepositoryException if an error occurs. 267 */ 268 public void setCreationDate(ZonedDateTime creationDate) throws AmetysRepositoryException 269 { 270 setValue(__DATA_CREATION, creationDate); 271 } 272 273 /** 274 * Get the site of the project 275 * @return The site. Can be null if the reference is broken. 276 */ 277 public Site getSite() 278 { 279 try 280 { 281 SiteManager siteManager = _getFactory()._getSiteManager(); 282 Property weakReference = getNode().getProperty(RepositoryConstants.NAMESPACE_PREFIX + ":" + DATA_SITE); 283 String siteName = weakReference.getNode().getName(); 284 return siteManager.getSite(siteName); 285 } 286 catch (PathNotFoundException e) 287 { 288 _getFactory().getFactoryLogger().debug("Can not found site attribute for project " + getName(), e); 289 return null; 290 } 291 catch (ItemNotFoundException e) 292 { 293 _getFactory().getFactoryLogger().debug("Can not found site reference for project " + getName(), e); 294 return null; 295 } 296 catch (RepositoryException e) 297 { 298 throw new AmetysRepositoryException("Unexpected repository exception", e); 299 } 300 } 301 302 /** 303 * Get the site of the project 304 * @param site the site to set 305 */ 306 public void setSite(Site site) 307 { 308 try 309 { 310 Node projectNode = getNode(); 311 if (projectNode.hasProperty(RepositoryConstants.NAMESPACE_PREFIX + ":" + DATA_SITE)) 312 { 313 removeValue(DATA_SITE); 314 } 315 ValueFactory valueFactory = projectNode.getSession().getValueFactory(); 316 Value weakRefValue = valueFactory.createValue(site.getNode(), true); 317 projectNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":" + DATA_SITE, weakRefValue); 318 } 319 catch (RepositoryException e) 320 { 321 throw new AmetysRepositoryException("Unexpected repository exception", e); 322 } 323 324 if (needsSave()) 325 { 326 saveChanges(); 327 } 328 } 329 330 /** 331 * Get the project managers user identities 332 * @return The managers 333 */ 334 public UserIdentity[] getManagers() 335 { 336 return getValue(__DATA_MANAGERS, new UserIdentity[0]); 337 } 338 339 /** 340 * Set the project managers 341 * @param user The managers 342 */ 343 public void setManagers(UserIdentity[] user) 344 { 345 setValue(__DATA_MANAGERS, user); 346 } 347 348 /** 349 * Retrieve the list of activated modules for the project 350 * @return The list of modules ids 351 */ 352 public String[] getModules() 353 { 354 return getValue(__DATA_MODULES, new String[0]); 355 } 356 357 /** 358 * Set the list of activated modules for the project 359 * @param modules The list of modules 360 */ 361 public void setModules(String[] modules) 362 { 363 setValue(__DATA_MODULES, modules); 364 } 365 366 /** 367 * Add a module to the list of activated modules 368 * @param moduleId The module id 369 */ 370 public void addModule(String moduleId) 371 { 372 String[] modules = getValue(__DATA_MODULES); 373 if (!ArrayUtils.contains(modules, moduleId)) 374 { 375 setValue(__DATA_MODULES, modules == null ? new String[]{moduleId} : ArrayUtils.add(modules, moduleId)); 376 } 377 } 378 379 /** 380 * Remove a module from the list of activated modules 381 * @param moduleId The module id 382 */ 383 public void removeModule(String moduleId) 384 { 385 String[] modules = getValue(__DATA_MODULES); 386 if (ArrayUtils.contains(modules, moduleId)) 387 { 388 setValue(__DATA_MODULES, ArrayUtils.removeElement(modules, moduleId)); 389 } 390 } 391 392 /** 393 * Get the inscription status of the project 394 * @return The inscription status 395 */ 396 public InscriptionStatus getInscriptionStatus() 397 { 398 if (hasValue(__DATA_INSCRIPTION_STATUS)) 399 { 400 return InscriptionStatus.createsFromString(getValue(__DATA_INSCRIPTION_STATUS)); 401 } 402 return InscriptionStatus.PRIVATE; 403 } 404 405 /** 406 * Set the inscription status of the project 407 * @param inscriptionStatus The inscription status 408 */ 409 public void setInscriptionStatus(String inscriptionStatus) 410 { 411 if (inscriptionStatus != null && InscriptionStatus.createsFromString(inscriptionStatus) != null) 412 { 413 setValue(__DATA_INSCRIPTION_STATUS, inscriptionStatus); 414 } 415 else 416 { 417 removeValue(__DATA_INSCRIPTION_STATUS); 418 } 419 } 420 421 /** 422 * Set the inscription status of the project 423 * @param inscriptionStatus The inscription status 424 */ 425 public void setInscriptionStatus(InscriptionStatus inscriptionStatus) 426 { 427 if (inscriptionStatus != null) 428 { 429 setValue(__DATA_INSCRIPTION_STATUS, inscriptionStatus.toString()); 430 } 431 else 432 { 433 removeValue(__DATA_INSCRIPTION_STATUS); 434 } 435 } 436 437 /** 438 * Get the default profile for new members of the project 439 * @return The default profile 440 */ 441 public String getDefaultProfile() 442 { 443 return getValue(__DATA_DEFAULT_PROFILE); 444 } 445 446 447 /** 448 * Set the default profile for the members of the project 449 * @param profileId The ID of the profile 450 */ 451 public void setDefaultProfile(String profileId) 452 { 453 if (profileId != null) 454 { 455 setValue(__DATA_DEFAULT_PROFILE, profileId); 456 } 457 else 458 { 459 removeValue(__DATA_DEFAULT_PROFILE); 460 } 461 } 462 463 /** 464 * Get the root for plugins 465 * @return the root for plugins 466 * @throws AmetysRepositoryException if an error occurs. 467 */ 468 public ModifiableTraversableAmetysObject getRootPlugins () throws AmetysRepositoryException 469 { 470 return (ModifiableTraversableAmetysObject) getChild(__PLUGINS_NODE_NAME); 471 } 472 473 /** 474 * Retrieve the list of tags 475 * @return The list of tags 476 * @throws AmetysRepositoryException if an error occurs 477 */ 478 public Set<String> getTags() throws AmetysRepositoryException 479 { 480 return TaggableAmetysObjectHelper.getTags(this); 481 } 482 483 /** 484 * Add a tag to the project 485 * @param tag The tag 486 * @throws AmetysRepositoryException if an error occurs 487 */ 488 public void tag(String tag) throws AmetysRepositoryException 489 { 490 TaggableAmetysObjectHelper.tag(this, tag); 491 } 492 493 /** 494 * Remove a tag from the project 495 * @param tag The tag 496 * @throws AmetysRepositoryException if an error occurs 497 */ 498 public void untag(String tag) throws AmetysRepositoryException 499 { 500 TaggableAmetysObjectHelper.untag(this, tag); 501 } 502 503 /** 504 * Set the project tags 505 * @param tags The list of tags 506 */ 507 public void setTags(List<String> tags) 508 { 509 Set<String> currentTags = getTags(); 510 // remove old tags not selected 511 currentTags.stream() 512 .filter(tag -> !tags.contains(tag)) 513 .forEach(tag -> untag(tag)); 514 515 // add new selected tags 516 tags.stream() 517 .filter(tag -> !currentTags.contains(tag)) 518 .forEach(tag -> tag(tag)); 519 } 520 521 522 /** 523 * Retrieve the list of categories 524 * @return The categories 525 * @throws AmetysRepositoryException if an error occurs 526 */ 527 public Set<String> getCategories() throws AmetysRepositoryException 528 { 529 return TaggableAmetysObjectHelper.getTags(this, __DATA_CATEGORIES); 530 } 531 532 /** 533 * Add a category to the project 534 * @param category The category 535 * @throws AmetysRepositoryException if an error occurs 536 */ 537 public void addCategory(String category) throws AmetysRepositoryException 538 { 539 TaggableAmetysObjectHelper.tag(this, category, __DATA_CATEGORIES); 540 } 541 542 /** 543 * Remove a category from the project 544 * @param category The category 545 * @throws AmetysRepositoryException if an error occurs 546 */ 547 public void removeCategory(String category) throws AmetysRepositoryException 548 { 549 TaggableAmetysObjectHelper.untag(this, category, __DATA_CATEGORIES); 550 } 551 552 /** 553 * Set the category tags of the project 554 * @param categoryTags The category tags 555 */ 556 public void setCategoryTags(List<String> categoryTags) 557 { 558 Set<String> currentCategories = getCategories(); 559 // remove old tags not selected 560 currentCategories.stream() 561 .filter(category -> !categoryTags.contains(category)) 562 .forEach(category -> removeCategory(category)); 563 564 // add new selected tags 565 categoryTags.stream() 566 .filter(category -> !currentCategories.contains(category)) 567 .forEach(category -> addCategory(category)); 568 } 569 570 /** 571 * Set the cover image of the site 572 * @param is The input stream of the cover image 573 * @param mimeType The mimetype of the cover image 574 * @param filename The filename of the cover image 575 * @param lastModificationDate The last modification date of the cover image 576 * @throws IOException if an error occurs while setting the cover image 577 */ 578 public void setCoverImage(InputStream is, String mimeType, String filename, ZonedDateTime lastModificationDate) throws IOException 579 { 580 if (is != null) 581 { 582 Binary coverImage = new Binary(); 583 coverImage.setInputStream(is); 584 Optional.ofNullable(mimeType).ifPresent(coverImage::setMimeType); 585 Optional.ofNullable(filename).ifPresent(coverImage::setFilename); 586 Optional.ofNullable(lastModificationDate).ifPresent(coverImage::setLastModificationDate); 587 setValue(__DATA_COVERIMAGE, coverImage); 588 } 589 else 590 { 591 removeValue(__DATA_COVERIMAGE); 592 } 593 } 594 595 /** 596 * Returns the cover image of the project 597 * @return the cover image of the project 598 */ 599 public Binary getCoverImage() 600 { 601 return getValue(__DATA_COVERIMAGE); 602 } 603 604 /** 605 * Generates SAX events for the project's cover image 606 * @param contentHandler the {@link ContentHandler} that will receive the SAX events 607 * @throws SAXException if error occurs during the SAX events generation 608 * @throws IOException if an I/O error occurs while reading the cover image 609 */ 610 public void coverImageToSAX(ContentHandler contentHandler) throws SAXException, IOException 611 { 612 dataToSAX(contentHandler, __DATA_COVERIMAGE); 613 } 614 615 /** 616 * Retrieve the list of keywords for the project 617 * @return The list of keywords 618 */ 619 public String[] getKeywords() 620 { 621 return getValue(__DATA_KEYWORDS, new String[0]); 622 } 623 624 /** 625 * Set the list of keywordss for the project 626 * @param keywords The list of keywords 627 */ 628 public void setKeywords(String[] keywords) 629 { 630 setValue(__DATA_KEYWORDS, keywords); 631 } 632}