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.InputStream; 019import java.time.ZonedDateTime; 020import java.util.ArrayList; 021import java.util.Collection; 022import java.util.Date; 023import java.util.Iterator; 024import java.util.Objects; 025import java.util.Optional; 026import java.util.stream.Collectors; 027import java.util.stream.StreamSupport; 028 029import javax.jcr.Node; 030import javax.jcr.NodeIterator; 031import javax.jcr.Property; 032import javax.jcr.RepositoryException; 033import javax.jcr.Value; 034import javax.jcr.ValueFactory; 035 036import org.apache.commons.lang3.ArrayUtils; 037 038import org.ametys.core.user.UserIdentity; 039import org.ametys.plugins.explorer.ExplorerNode; 040import org.ametys.plugins.repository.AmetysObject; 041import org.ametys.plugins.repository.AmetysObjectIterable; 042import org.ametys.plugins.repository.AmetysRepositoryException; 043import org.ametys.plugins.repository.RepositoryConstants; 044import org.ametys.plugins.repository.TraversableAmetysObject; 045import org.ametys.plugins.repository.data.ametysobject.ModifiableModelLessDataAwareAmetysObject; 046import org.ametys.plugins.repository.data.holder.ModifiableModelLessDataHolder; 047import org.ametys.plugins.repository.data.holder.impl.DefaultModifiableModelLessDataHolder; 048import org.ametys.plugins.repository.data.repositorydata.ModifiableRepositoryData; 049import org.ametys.plugins.repository.data.repositorydata.impl.JCRRepositoryData; 050import org.ametys.plugins.repository.events.EventHolder; 051import org.ametys.plugins.repository.events.JCREventHelper; 052import org.ametys.plugins.repository.jcr.DefaultTraversableAmetysObject; 053import org.ametys.plugins.repository.metadata.BinaryMetadata; 054import org.ametys.plugins.repository.metadata.ModifiableBinaryMetadata; 055import org.ametys.plugins.repository.metadata.UnknownMetadataException; 056import org.ametys.web.repository.site.Site; 057import org.ametys.web.repository.site.SiteManager; 058 059/** 060 * {@link AmetysObject} for storing project informations. 061 */ 062public class Project extends DefaultTraversableAmetysObject<ProjectFactory> implements ProjectsTreeNode, ModifiableModelLessDataAwareAmetysObject, EventHolder 063{ 064 /** Project node type name. */ 065 public static final String NODE_TYPE = RepositoryConstants.NAMESPACE_PREFIX + ":project"; 066 067 /** Metadata name for project 's sites */ 068 public static final String DATA_SITES = "sites"; 069 070 private static final String __EXPLORER_NODE_NAME = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":resources"; 071 private static final String __DATA_TITLE = "title"; 072 private static final String __DATA_DESCRIPTION = "description"; 073 074 private static final String __DATA_MAILING_LIST = "mailingList"; 075 private static final String __DATA_CREATION = "creationDate"; 076 private static final String __DATA_MANAGERS = "managers"; 077 private static final String __DATA_MODULES = "modules"; 078 private static final String __DATA_INSCRIPTION_STATUS = "inscriptionStatus"; 079 private static final String __DATA_DEFAULT_PROFILE = "defaultProfile"; 080 private static final String __DATA_COVERIMAGE = "coverImage"; 081 082 /** 083 * The inscription status of the project 084 */ 085 public enum InscriptionStatus 086 { 087 /** Inscriptions are opened to anyone */ 088 OPEN("open"), 089 /** Inscriptions are moderated */ 090 MODERATED("moderated"), 091 /** Inscriptions are private */ 092 PRIVATE("private"); 093 094 private String _value; 095 096 private InscriptionStatus(String value) 097 { 098 this._value = value; 099 } 100 101 @Override 102 public String toString() 103 { 104 return _value; 105 } 106 107 /** 108 * Converts a string to an Inscription 109 * @param status The status to convert 110 * @return The status corresponding to the string or null if unknown 111 */ 112 public static InscriptionStatus createsFromString(String status) 113 { 114 for (InscriptionStatus v : InscriptionStatus.values()) 115 { 116 if (v.toString().equals(status)) 117 { 118 return v; 119 } 120 } 121 return null; 122 } 123 } 124 125 /** 126 * Creates a {@link Project}. 127 * @param node the node backing this {@link AmetysObject}. 128 * @param parentPath the parent path in the Ametys hierarchy. 129 * @param factory the {@link ProjectFactory} which creates the AmetysObject. 130 */ 131 public Project(Node node, String parentPath, ProjectFactory factory) 132 { 133 super(node, parentPath, factory); 134 } 135 136 public ModifiableModelLessDataHolder getDataHolder() 137 { 138 ModifiableRepositoryData repositoryData = new JCRRepositoryData(getNode()); 139 return new DefaultModifiableModelLessDataHolder(_getFactory().getProjectDataTypeExtensionPoint(), repositoryData); 140 } 141 142 /** 143 * Retrieves the title. 144 * @return the title. 145 * @throws AmetysRepositoryException if an error occurs. 146 */ 147 public String getTitle() throws AmetysRepositoryException 148 { 149 return getValue(__DATA_TITLE); 150 } 151 152 /** 153 * Set the title. 154 * @param title the title. 155 * @throws AmetysRepositoryException if an error occurs. 156 */ 157 public void setTitle(String title) throws AmetysRepositoryException 158 { 159 setValue(__DATA_TITLE, title); 160 } 161 162 /** 163 * Retrieves the description. 164 * @return the description. 165 * @throws AmetysRepositoryException if an error occurs. 166 */ 167 public String getDescription() throws AmetysRepositoryException 168 { 169 return getValue(__DATA_DESCRIPTION); 170 } 171 172 /** 173 * Set the description. 174 * @param description the description. 175 * @throws AmetysRepositoryException if an error occurs. 176 */ 177 public void setDescription(String description) throws AmetysRepositoryException 178 { 179 setValue(__DATA_DESCRIPTION, description); 180 } 181 182 /** 183 * Remove the description. 184 * @throws AmetysRepositoryException if an error occurs. 185 */ 186 public void removeDescription() throws AmetysRepositoryException 187 { 188 if (hasValue(__DATA_DESCRIPTION)) 189 { 190 removeValue(__DATA_DESCRIPTION); 191 } 192 } 193 194 /** 195 * Retrieves the explorer nodes. 196 * @return the explorer nodes or an empty {@link AmetysObjectIterable}. 197 * @throws AmetysRepositoryException if an error occurs. 198 */ 199 public ExplorerNode getExplorerRootNode() throws AmetysRepositoryException 200 { 201 return (ExplorerNode) getChild(__EXPLORER_NODE_NAME); 202 } 203 204 /** 205 * Retrieves the explorer nodes. 206 * @return the explorer nodes or an empty {@link AmetysObjectIterable}. 207 * @throws AmetysRepositoryException if an error occurs. 208 */ 209 public AmetysObjectIterable<ExplorerNode> getExplorerNodes() throws AmetysRepositoryException 210 { 211 return ((TraversableAmetysObject) getChild(__EXPLORER_NODE_NAME)).getChildren(); 212 } 213 214 215 /** 216 * Retrieves the mailing list. 217 * @return the mailing list. 218 * @throws AmetysRepositoryException if an error occurs. 219 */ 220 public String getMailingList() throws AmetysRepositoryException 221 { 222 return getValue(__DATA_MAILING_LIST); 223 } 224 225 /** 226 * Set the mailing list. 227 * @param mailingList the mailing list. 228 * @throws AmetysRepositoryException if an error occurs. 229 */ 230 public void setMailingList(String mailingList) throws AmetysRepositoryException 231 { 232 setValue(__DATA_MAILING_LIST, mailingList); 233 } 234 235 /** 236 * Remove the mailing list. 237 * @throws AmetysRepositoryException if an error occurs. 238 */ 239 public void removeMailingList() throws AmetysRepositoryException 240 { 241 removeValue(__DATA_MAILING_LIST); 242 } 243 244 /** 245 * Retrieves the date of creation. 246 * @return the date of creation. 247 * @throws AmetysRepositoryException if an error occurs. 248 */ 249 public ZonedDateTime getCreationDate() throws AmetysRepositoryException 250 { 251 return getValue(__DATA_CREATION); 252 } 253 254 /** 255 * Set the date of creation. 256 * @param creationDate the date of creation 257 * @throws AmetysRepositoryException if an error occurs. 258 */ 259 public void setCreationDate(ZonedDateTime creationDate) throws AmetysRepositoryException 260 { 261 setValue(__DATA_CREATION, creationDate); 262 } 263 264 @Override 265 public Node getEventsRootNode() throws RepositoryException 266 { 267 return JCREventHelper.getEventsRootNode(getNode()); 268 } 269 270 @Override 271 public NodeIterator getEvents() throws RepositoryException 272 { 273 return JCREventHelper.getEvents(this); 274 } 275 276 /** 277 * Get the sites of the project 278 * @return The collection of sites 279 */ 280 public Collection<Site> getSites() 281 { 282 try 283 { 284 if (!hasValue(DATA_SITES)) 285 { 286 return new ArrayList<>(); 287 } 288 else 289 { 290 SiteManager siteManager = _getFactory()._getSiteManager(); 291 292 // Stream over the properties to retrieve the corresponding sites. 293 JCRRepositoryData sitesData = (JCRRepositoryData) new JCRRepositoryData(getNode()).getRepositoryData(DATA_SITES); 294 Node jcrSitesNode = sitesData.getNode(); 295 Iterator<Property> sitesIterator = jcrSitesNode.getProperties(); 296 Iterable<Property> sitesIterable = () -> sitesIterator; 297 298 return StreamSupport.stream(sitesIterable.spliterator(), false) 299 .map(p -> 300 { 301 try 302 { 303 return p.getNode().getName(); 304 } 305 catch (Exception e) 306 { 307 // site might not exist (anymore...) 308 return null; 309 } 310 }) 311 .filter(Objects::nonNull) 312 .map(siteName -> siteManager.getSite(siteName)) 313 .collect(Collectors.toList()); 314 } 315 } 316 catch (RepositoryException e) 317 { 318 return new ArrayList<>(); 319 } 320 } 321 322 /** 323 * Set the sites of the project 324 * @param sites The names of the site 325 */ 326 public void setSites(Collection<String> sites) 327 { 328 if (hasValue(DATA_SITES)) 329 { 330 removeValue(DATA_SITES); 331 } 332 333 JCRRepositoryData sitesData = (JCRRepositoryData) new JCRRepositoryData(getNode()).addRepositoryData(DATA_SITES, RepositoryConstants.NAMESPACE_PREFIX + ":compositeMetadata"); 334 Node jcrSitesNode = sitesData.getNode(); 335 SiteManager siteManager = _getFactory()._getSiteManager(); 336 337 // create weak references to site nodes 338 int[] propIdx = {0}; 339 sites.forEach(siteName -> 340 { 341 if (siteManager.hasSite(siteName)) 342 { 343 Site site = siteManager.getSite(siteName); 344 345 try 346 { 347 ValueFactory valueFactory = jcrSitesNode.getSession().getValueFactory(); 348 Value weakRefValue = valueFactory.createValue(site.getNode(), true); 349 350 propIdx[0]++; // increment index 351 jcrSitesNode.setProperty(Integer.toString(propIdx[0]), weakRefValue); 352 } 353 catch (RepositoryException e) 354 { 355 throw new AmetysRepositoryException("Unexpected repository exception", e); 356 } 357 } 358 }); 359 360 if (needsSave()) 361 { 362 saveChanges(); 363 } 364 } 365 366// /** 367// * Get the project path of the project 368// * The project path is composed of the project category names and the project name separated by slashes. 369// * e.g. cat1/cat2/project-name 370// * @return he project path of the project 371// */ 372// public String getProjectPath() 373// { 374// Deque<String> path = new ArrayDeque<>(); 375// path.addFirst(getName()); 376// 377// try 378// { 379// Node parentNode = getNode().getParent(); 380// 381// while (NodeTypeHelper.isNodeType(parentNode, "ametys:projectCategory")) 382// { 383// path.addFirst(parentNode.getName()); 384// parentNode = parentNode.getParent(); 385// } 386// 387// return String.join("/", path); 388// } 389// catch (RepositoryException e) 390// { 391// throw new AmetysRepositoryException("Unexpected repository exception while retrieving project path", e); 392// } 393// } 394 395 /** 396 * Get the project managers user identities 397 * @return The managers 398 */ 399 public UserIdentity[] getManagers() 400 { 401 return getValue(__DATA_MANAGERS, new UserIdentity[0]); 402 } 403 404 /** 405 * Set the project managers 406 * @param user The managers 407 */ 408 public void setManagers(UserIdentity[] user) 409 { 410 setValue(__DATA_MANAGERS, user); 411 } 412 413 /** 414 * Retrieve the list of activated modules for the project 415 * @return The list of modules ids 416 */ 417 public String[] getModules() 418 { 419 return getValue(__DATA_MODULES, new String[0]); 420 } 421 422 /** 423 * Set the list of activated modules for the project 424 * @param modules The list of modules 425 */ 426 public void setModules(String[] modules) 427 { 428 setValue(__DATA_MODULES, modules); 429 } 430 431 /** 432 * Add a module to the list of activated modules 433 * @param moduleId The module id 434 */ 435 public void addModule(String moduleId) 436 { 437 String[] modules = getValue(__DATA_MODULES); 438 if (!ArrayUtils.contains(modules, moduleId)) 439 { 440 setValue(__DATA_MODULES, modules == null ? new String[]{moduleId} : ArrayUtils.add(modules, moduleId)); 441 } 442 } 443 444 /** 445 * Remove a module from the list of activated modules 446 * @param moduleId The module id 447 */ 448 public void removeModule(String moduleId) 449 { 450 String[] modules = getValue(__DATA_MODULES); 451 if (ArrayUtils.contains(modules, moduleId)) 452 { 453 setValue(__DATA_MODULES, ArrayUtils.removeElement(modules, moduleId)); 454 } 455 } 456 457 /** 458 * Get the inscription status of the project 459 * @return The inscription status 460 */ 461 public InscriptionStatus getInscriptionStatus() 462 { 463 if (hasValue(__DATA_INSCRIPTION_STATUS)) 464 { 465 return InscriptionStatus.createsFromString(getValue(__DATA_INSCRIPTION_STATUS)); 466 } 467 return InscriptionStatus.PRIVATE; 468 } 469 470 /** 471 * Set the inscription status of the project 472 * @param inscriptionStatus The inscription status 473 */ 474 public void setInscriptionStatus(String inscriptionStatus) 475 { 476 if (inscriptionStatus != null && InscriptionStatus.createsFromString(inscriptionStatus) != null) 477 { 478 setValue(__DATA_INSCRIPTION_STATUS, inscriptionStatus); 479 } 480 else if (hasValue(__DATA_INSCRIPTION_STATUS)) 481 { 482 removeValue(__DATA_INSCRIPTION_STATUS); 483 } 484 } 485 486 /** 487 * Get the default profile for new members of the project 488 * @return The default profile 489 */ 490 public String getDefaultProfile() 491 { 492 return getValue(__DATA_DEFAULT_PROFILE); 493 } 494 495 496 /** 497 * Set the default profile for the members of the project 498 * @param profileId The ID of the profile 499 */ 500 public void setDefaultProfile(String profileId) 501 { 502 if (profileId != null) 503 { 504 setValue(__DATA_DEFAULT_PROFILE, profileId); 505 } 506 else if (hasValue(__DATA_DEFAULT_PROFILE)) 507 { 508 removeValue(__DATA_DEFAULT_PROFILE); 509 } 510 } 511 512 /** 513 * Set the cover image of the site 514 * @param is The input stream of the cover image 515 * @param mimeType The mimetype of the cover image 516 * @param filename The filename of the cover image 517 * @param lastModifiedDate The last modified date of the cover image 518 */ 519 public void setCoverImage(InputStream is, String mimeType, String filename, Date lastModifiedDate) 520 { 521 if (hasValue(__DATA_COVERIMAGE)) 522 { 523 removeValue(__DATA_COVERIMAGE); 524 } 525 if (is != null) 526 { 527 ModifiableBinaryMetadata coverImage = getMetadataHolder().getBinaryMetadata(__DATA_COVERIMAGE, true); 528 coverImage.setInputStream(is); 529 Optional.ofNullable(mimeType).ifPresent(coverImage::setMimeType); 530 Optional.ofNullable(filename).ifPresent(coverImage::setFilename); 531 Optional.ofNullable(lastModifiedDate).ifPresent(coverImage::setLastModified); 532 } 533 } 534 535 /** 536 * Returns the cover image of the project 537 * @return the cover image of the project 538 */ 539 public BinaryMetadata getCoverImage() 540 { 541 try 542 { 543 return getMetadataHolder().getBinaryMetadata(__DATA_COVERIMAGE); 544 } 545 catch (UnknownMetadataException e) 546 { 547 return null; 548 } 549 } 550 551}