001/*
002 *  Copyright 2010 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.repository.jcr;
017
018import java.util.Arrays;
019import java.util.Calendar;
020import java.util.Comparator;
021import java.util.Date;
022import java.util.HashMap;
023import java.util.List;
024import java.util.Map;
025import java.util.Map.Entry;
026import java.util.Optional;
027import java.util.Set;
028
029import javax.jcr.Node;
030import javax.jcr.NodeIterator;
031import javax.jcr.Property;
032import javax.jcr.PropertyIterator;
033import javax.jcr.RepositoryException;
034import javax.jcr.version.OnParentVersionAction;
035import javax.jcr.version.Version;
036import javax.jcr.version.VersionException;
037import javax.jcr.version.VersionHistory;
038import javax.jcr.version.VersionIterator;
039
040import org.apache.jackrabbit.JcrConstants;
041import org.apache.jackrabbit.core.NodeImpl;
042
043import org.ametys.core.group.GroupIdentity;
044import org.ametys.core.right.ProfileAssignmentStorage.AnonymousOrAnyConnectedKeys;
045import org.ametys.core.right.ProfileAssignmentStorage.UserOrGroup;
046import org.ametys.core.user.UserIdentity;
047import org.ametys.plugins.repository.AmetysObject;
048import org.ametys.plugins.repository.AmetysRepositoryException;
049import org.ametys.plugins.repository.ModifiableACLAmetysObject;
050import org.ametys.plugins.repository.UnknownAmetysObjectException;
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.lock.LockableAmetysObject;
056import org.ametys.plugins.repository.version.ModifiableDataAwareVersionableAmetysObject;
057import org.ametys.plugins.repository.version.VersionableAmetysObject;
058
059/**
060 * Default implementation of a {@link JCRAmetysObject}, which is also a {@link VersionableAmetysObject}.
061 * @param <F> the actual type of factory.
062 */
063public class DefaultAmetysObject<F extends DefaultAmetysObjectFactory> extends SimpleAmetysObject<F> implements ModifiableDataAwareVersionableAmetysObject, ModifiableACLAmetysObject
064{
065    /** Properties that are auto-created or protected, which mustn't be copied when copying a node. */
066    protected static final List<String> PROTECTED_PROPERTIES = Arrays.asList(
067            JcrConstants.JCR_UUID,
068            JcrConstants.JCR_PRIMARYTYPE,
069            JcrConstants.JCR_PREDECESSORS,
070            JcrConstants.JCR_VERSIONHISTORY,
071            JcrConstants.JCR_BASEVERSION
072    );
073    
074    /** Compare a pair of JCR version name and creation date with an other **/
075    protected static final Comparator<Entry<String, Calendar>> __VERSION_COMPARATOR = Comparator.comparing(Entry<String, Calendar>::getValue).thenComparing(Entry::getKey);
076
077    // Root JCR Node of this content
078    private Node _baseNode;
079
080    // pointed version, or null if HEAD
081    private Version _versionNode;
082    
083    // Current version, either HEAD or a given version
084    private Node _currentNode;
085    
086    // The version history of the base node
087    private VersionHistory _versionHistory;
088
089    /**
090     * Creates an {@link DefaultAmetysObject}.
091     * @param node the node backing this {@link AmetysObject}
092     * @param parentPath the parentPath in the Ametys hierarchy
093     * @param factory the DefaultAmetysObjectFactory which created the AmetysObject
094     */
095    public DefaultAmetysObject(Node node, String parentPath, F factory)
096    {
097        super(node, parentPath, factory);
098        _baseNode = node;
099        _currentNode = node;
100    }
101    
102    @Override
103    public Node getNode()
104    {
105        return _currentNode;
106    }
107    
108    /**
109     * Returns the JCR node backing this {@link AmetysObject} in the default JCR workspace
110     * @return the JCR node backing this {@link AmetysObject} in the default JCR workspace
111     */
112    public Node getBaseNode ()
113    {
114        return _baseNode;
115    }
116
117    // Versioning capabilities
118    
119    /**
120     * Returns the JCR {@link VersionHistory} of the base node.
121     * @return the JCR {@link VersionHistory} of the base node.
122     * @throws RepositoryException if something wrong occurs retrieving the VersionHistory.
123     */
124    protected VersionHistory getVersionHistory() throws RepositoryException
125    {
126        if (_versionHistory == null)
127        {
128            _versionHistory = _baseNode.getSession().getWorkspace().getVersionManager().getVersionHistory(_baseNode.getPath());
129        }
130        
131        return _versionHistory;
132    }
133    
134    /**
135     * Returns the JCR base version of the node.
136     * @return the JCR base version of the node.
137     * @throws RepositoryException if something wrong occurs retrieving the base version.
138     */
139    protected Version getBaseVersion() throws RepositoryException
140    {
141        return _baseNode.getSession().getWorkspace().getVersionManager().getBaseVersion(_baseNode.getPath());
142    }
143
144    public void checkpoint() throws AmetysRepositoryException
145    {
146        try
147        {
148            getNode().getSession().getWorkspace().getVersionManager().checkpoint(getNode().getPath());
149        }
150        catch (RepositoryException e)
151        {
152            throw new AmetysRepositoryException("Unable to checkpoint", e);
153        }
154    }
155    
156    public void switchToLabel(String label) throws UnknownAmetysObjectException, AmetysRepositoryException
157    {
158        if (label == null)
159        {
160            // back to current version
161            _versionNode = null;
162            _currentNode = _baseNode;
163        }
164        else
165        {
166            try
167            {
168                VersionHistory history = getVersionHistory();
169                _versionNode = history.getVersionByLabel(label);
170                _currentNode = _versionNode.getFrozenNode();
171            }
172            catch (VersionException e)
173            {
174                throw new UnknownAmetysObjectException("There's no label : " + label, e);
175            }
176            catch (RepositoryException e)
177            {
178                throw new AmetysRepositoryException("Unable to switch to label : " + label, e);
179            }
180        }
181    }
182    
183    public void switchToRevision(String revision) throws UnknownAmetysObjectException, AmetysRepositoryException
184    {
185        if (revision == null)
186        {
187            // back to current version
188            _versionNode = null;
189            _currentNode = _baseNode;
190        }
191        else
192        {
193            try
194            {
195                VersionHistory history = getVersionHistory();
196                _versionNode = history.getVersion(revision);
197                _currentNode = _versionNode.getNode(JcrConstants.JCR_FROZENNODE);
198            }
199            catch (VersionException e)
200            {
201                throw new UnknownAmetysObjectException("There's no revision : " + revision, e);
202            }
203            catch (RepositoryException e)
204            {
205                throw new AmetysRepositoryException("Unable to switch to revision : " + revision, e);
206            }
207        }
208    }
209    
210    public void restoreFromLabel(String label) throws UnknownAmetysObjectException, AmetysRepositoryException
211    {
212        try
213        {
214            VersionHistory history = getVersionHistory();
215            Node versionNode = history.getVersionByLabel(label);
216            restoreFromNode(versionNode.getNode(JcrConstants.JCR_FROZENNODE));
217        }
218        catch (RepositoryException e)
219        {
220            throw new AmetysRepositoryException("Unable to restore from label: " + label, e);
221        }
222    }
223    
224    public void restoreFromRevision(String revision) throws UnknownAmetysObjectException, AmetysRepositoryException
225    {
226        try
227        {
228            VersionHistory history = getVersionHistory();
229            Node versionNode = history.getVersion(revision);
230            restoreFromNode(versionNode.getNode(JcrConstants.JCR_FROZENNODE));
231        }
232        catch (RepositoryException e)
233        {
234            throw new AmetysRepositoryException("Unable to restore from revision: " + revision, e);
235        }
236    }
237    
238    /**
239     * Restore from a node
240     * @param node The node to restore
241     * @throws RepositoryException If error occurs
242     */
243    protected void restoreFromNode(Node node) throws RepositoryException
244    {
245        // Remove all properties and nodes of the current node (except jcr and OnParentVersion=IGNORE).
246        PropertyIterator propIt = _baseNode.getProperties();
247        while (propIt.hasNext())
248        {
249            Property prop = propIt.nextProperty();
250            String propName = prop.getName();
251            if (!propName.startsWith("jcr:") && prop.getDefinition().getOnParentVersion() != OnParentVersionAction.IGNORE)
252            {
253                prop.remove();
254            }
255        }
256        
257        NodeIterator nodeIt = _baseNode.getNodes();
258        while (nodeIt.hasNext())
259        {
260            Node childNode = nodeIt.nextNode();
261            String nodeName = childNode.getName();
262            if (!nodeName.startsWith("jcr:") && childNode.getDefinition().getOnParentVersion() != OnParentVersionAction.IGNORE)
263            {
264                childNode.remove();
265            }
266        }
267        
268        // Copy all properties and nodes from the given node (except jcr).
269        PropertyIterator newPropIt = node.getProperties();
270        while (newPropIt.hasNext())
271        {
272            Property newProp = newPropIt.nextProperty();
273            
274            if (!newProp.getName().startsWith("jcr:"))
275            {
276                if (newProp.getDefinition().isMultiple())
277                {
278                    _baseNode.setProperty(newProp.getName(), newProp.getValues(), newProp.getType());
279                }
280                else
281                {
282                    _baseNode.setProperty(newProp.getName(), newProp.getValue(), newProp.getType());
283                }
284            }
285        }
286        
287        NodeIterator newNodeIt = node.getNodes();
288        while (newNodeIt.hasNext())
289        {
290            Node newNode = newNodeIt.nextNode();
291            
292            if (!newNode.getName().startsWith("jcr:"))
293            {
294                copyNode(_baseNode, newNode);
295            }
296        }
297    }
298    
299    /**
300     * Copy the source node in parent node
301     * @param parentDest The dest node
302     * @param src The source node to copy
303     * @throws RepositoryException If error occurs
304     */
305    protected void copyNode(Node parentDest, Node src) throws RepositoryException
306    {
307        Node dest;
308        if (parentDest.hasNode(src.getName()))
309        {
310            // case of auto created child
311            dest = parentDest.getNode(src.getName());
312        }
313        else
314        {
315            String uuid = null;
316            if (src.hasProperty(JcrConstants.JCR_FROZENUUID))
317            {
318                uuid = src.getProperty(JcrConstants.JCR_FROZENUUID).getString();
319            }
320            
321            if (uuid == null)
322            {
323                dest = parentDest.addNode(src.getName(), src.getProperty(JcrConstants.JCR_FROZENPRIMARYTYPE).getString());
324            }
325            else
326            {
327                dest = ((NodeImpl) parentDest).addNodeWithUuid(src.getName(), src.getProperty(JcrConstants.JCR_FROZENPRIMARYTYPE).getString(), uuid);
328            }
329        }
330        
331        PropertyIterator pit = src.getProperties();
332        while (pit.hasNext())
333        {
334            Property p = pit.nextProperty();
335            String name = p.getName();
336            
337            // Tests for protected and/or autocreated properties
338            if (!PROTECTED_PROPERTIES.contains(name) && !name.startsWith("jcr:frozen") && !dest.hasProperty(name))
339            {
340                if (p.getDefinition().isMultiple())
341                {
342                    dest.setProperty(name, p.getValues());
343                }
344                else
345                {
346                    dest.setProperty(name, p.getValue());
347                }
348            }
349        }
350        
351        NodeIterator nit = src.getNodes();
352        while (nit.hasNext())
353        {
354            copyNode(dest, nit.nextNode());
355        }
356    }
357
358    public void addLabel(String label, boolean moveIfPresent) throws AmetysRepositoryException
359    {
360        try
361        {
362            VersionHistory history = getVersionHistory();
363            String versionName;
364
365            if (_versionNode == null)
366            {
367                // not sticked to a particular version
368                versionName = getBaseVersion().getName();
369            }
370            else
371            {
372                // sticked to label
373                versionName = _versionNode.getName();
374            }
375
376            history.addVersionLabel(versionName, label, moveIfPresent);
377        }
378        catch (RepositoryException e)
379        {
380            throw new AmetysRepositoryException("Unable to add label : " + label, e);
381        }
382    }
383
384    public void removeLabel(String label) throws AmetysRepositoryException
385    {
386        try
387        {
388            VersionHistory history = getVersionHistory();
389            history.removeVersionLabel(label);
390        }
391        catch (RepositoryException ex)
392        {
393            throw new AmetysRepositoryException("Unable to remove label : " + label, ex);
394        }
395    }
396
397    public String[] getAllLabels() throws AmetysRepositoryException
398    {
399        try
400        {
401            return getVersionHistory().getVersionLabels();
402        }
403        catch (RepositoryException e)
404        {
405            throw new AmetysRepositoryException("Unable to retrieve list of labels", e);
406        }
407    }
408
409    public String[] getLabels() throws AmetysRepositoryException
410    {
411        try
412        {
413            Version version = _versionNode;
414
415            if (version == null)
416            {
417                // not sticked to a particular version
418                version = getBaseVersion();
419            }
420
421            return version.getContainingHistory().getVersionLabels(version);
422        }
423        catch (RepositoryException e)
424        {
425            throw new AmetysRepositoryException("Unable to retrieve list of labels for current version", e);
426        }
427    }
428
429
430    public String[] getLabels(String revision) throws UnknownAmetysObjectException, AmetysRepositoryException
431    {
432        try
433        {
434            VersionHistory history = getVersionHistory();
435            Version version = history.getVersion(revision);
436            return history.getVersionLabels(version);
437        }
438        catch (VersionException e)
439        {
440            throw new UnknownAmetysObjectException("There's no revision " + revision, e);
441        }
442        catch (RepositoryException e)
443        {
444            throw new AmetysRepositoryException("Unable to retrieve list of labels for current version", e);
445        }
446    }
447
448    public String getRevision() throws AmetysRepositoryException
449    {
450        if (_versionNode == null)
451        {
452            // Current version
453            return null;
454        }
455
456        try
457        {
458            return _versionNode.getName();
459        }
460        catch (RepositoryException e)
461        {
462            throw new AmetysRepositoryException("Unable to get revision", e);
463        }
464    }
465    
466    @Override
467    public Date getRevisionTimestamp() throws AmetysRepositoryException
468    {
469        if (_versionNode == null)
470        {
471            // Current version
472            return null;
473        }
474
475        try
476        {
477            return _versionNode.getCreated().getTime();
478        }
479        catch (RepositoryException e)
480        {
481            throw new AmetysRepositoryException("Unable to get revision date", e);
482        }
483    }
484    
485    @Override
486    public Date getRevisionTimestamp(String revision) throws UnknownAmetysObjectException, AmetysRepositoryException
487    {
488        try
489        {
490            VersionHistory history = getVersionHistory();
491            Version version = history.getVersion(revision);
492            
493            return version.getCreated().getTime();
494        }
495        catch (VersionException e)
496        {
497            throw new UnknownAmetysObjectException("There's no revision " + revision, e);
498        }
499        catch (RepositoryException e)
500        {
501            throw new AmetysRepositoryException("Unable to get revision date", e);
502        }
503    }
504
505    public String[] getAllRevisions() throws AmetysRepositoryException
506    {
507        try
508        {
509            Map<String, Calendar> revisions = new HashMap<>();
510            VersionIterator iterator = getVersionHistory().getAllVersions();
511            
512            while (iterator.hasNext())
513            {
514                Version version = iterator.nextVersion();
515                String name = version.getName();
516                if (!JcrConstants.JCR_ROOTVERSION.equals(name))
517                {
518                    revisions.put(name, version.getCreated());
519                }
520            }
521            
522            return revisions.entrySet().stream()
523                .sorted(__VERSION_COMPARATOR)
524                .map(Entry::getKey)
525                .toArray(String[]::new);
526        }
527        catch (RepositoryException ex)
528        {
529            throw new AmetysRepositoryException("Unable to get revisions list", ex);
530        }
531    }
532
533    @Override
534    public ModifiableModelLessDataHolder getUnversionedDataHolder()
535    {
536        try
537        {
538            ModifiableRepositoryData repositoryData = new JCRRepositoryData(_baseNode.getNode("ametys-internal:unversioned"));
539            Optional<LockableAmetysObject> lockableAmetysObject = Optional.of(this)
540                                                                          .filter(LockableAmetysObject.class::isInstance)
541                                                                          .map(LockableAmetysObject.class:: cast);
542            return new DefaultModifiableModelLessDataHolder(_getFactory().getUnversionedDataTypeExtensionPoint(), repositoryData, lockableAmetysObject);
543        }
544        catch (RepositoryException e)
545        {
546            throw new AmetysRepositoryException(e);
547        }
548    }
549    
550    public Map<AnonymousOrAnyConnectedKeys, Set<String>> getProfilesForAnonymousAndAnyConnectedUser()
551    {
552        return ACLJCRAmetysObjectHelper.getProfilesForAnonymousAndAnyConnectedUser(getNode());
553    }
554    
555    public Map<GroupIdentity, Map<UserOrGroup, Set<String>>> getProfilesForGroups(Set<GroupIdentity> groups)
556    {
557        return ACLJCRAmetysObjectHelper.getProfilesForGroups(getNode(), groups);
558    }
559    
560    public Map<UserIdentity, Map<UserOrGroup, Set<String>>> getProfilesForUsers(UserIdentity user)
561    {
562        return ACLJCRAmetysObjectHelper.getProfilesForUsers(getNode(), user);
563    }
564    
565    public void addAllowedProfilesForAnyConnectedUser(Set<String> profileIds)
566    {
567        ACLJCRAmetysObjectHelper.addAllowedProfilesForAnyConnectedUser(getNode(), profileIds);
568    }
569    
570    public void removeAllowedProfilesForAnyConnectedUser(Set<String> profileIds)
571    {
572        ACLJCRAmetysObjectHelper.removeAllowedProfilesForAnyConnectedUser(getNode(), profileIds);
573    }
574    
575    public void addDeniedProfilesForAnyConnectedUser(Set<String> profileIds)
576    {
577        ACLJCRAmetysObjectHelper.addDeniedProfilesForAnyConnectedUser(getNode(), profileIds);
578    }
579    
580    public void removeDeniedProfilesForAnyConnectedUser(Set<String> profileIds)
581    {
582        ACLJCRAmetysObjectHelper.removeDeniedProfilesForAnyConnectedUser(getNode(), profileIds);
583    }
584    
585    public void addAllowedProfilesForAnonymous(Set<String> profileIds)
586    {
587        ACLJCRAmetysObjectHelper.addAllowedProfilesForAnonymous(getNode(), profileIds);
588    }
589    
590    public void removeAllowedProfilesForAnonymous(Set<String> profileIds)
591    {
592        ACLJCRAmetysObjectHelper.removeAllowedProfilesForAnonymous(getNode(), profileIds);
593    }
594    
595    public void addDeniedProfilesForAnonymous(Set<String> profileIds)
596    {
597        ACLJCRAmetysObjectHelper.addDeniedProfilesForAnonymous(getNode(), profileIds);
598    }
599    
600    public void removeDeniedProfilesForAnonymous(Set<String> profileIds)
601    {
602        ACLJCRAmetysObjectHelper.removeDeniedProfilesForAnonymous(getNode(), profileIds);
603    }
604    
605    public void addAllowedUsers(Set<UserIdentity> users, String profileId)
606    {
607        ACLJCRAmetysObjectHelper.addAllowedUsers(users, getNode(), profileId);
608    }
609    
610    public void removeAllowedUsers(Set<UserIdentity> users, String profileId)
611    {
612        ACLJCRAmetysObjectHelper.removeAllowedUsers(users, getNode(), profileId);
613    }
614    
615    public void removeAllowedUsers(Set<UserIdentity> users)
616    {
617        ACLJCRAmetysObjectHelper.removeAllowedUsers(users, getNode());
618    }
619    
620    public void addAllowedGroups(Set<GroupIdentity> groups, String profileId)
621    {
622        ACLJCRAmetysObjectHelper.addAllowedGroups(groups, getNode(), profileId);
623    }
624    
625    public void removeAllowedGroups(Set<GroupIdentity> groups, String profileId)
626    {
627        ACLJCRAmetysObjectHelper.removeAllowedGroups(groups, getNode(), profileId);
628    }
629    
630    public void removeAllowedGroups(Set<GroupIdentity> groups)
631    {
632        ACLJCRAmetysObjectHelper.removeAllowedGroups(groups, getNode());
633    }
634    
635    public void addDeniedUsers(Set<UserIdentity> users, String profileId)
636    {
637        ACLJCRAmetysObjectHelper.addDeniedUsers(users, getNode(), profileId);
638    }
639    
640    public void removeDeniedUsers(Set<UserIdentity> users, String profileId)
641    {
642        ACLJCRAmetysObjectHelper.removeDeniedUsers(users, getNode(), profileId);
643    }
644    
645    public void removeDeniedUsers(Set<UserIdentity> users)
646    {
647        ACLJCRAmetysObjectHelper.removeDeniedUsers(users, getNode());
648    }
649    
650    public void addDeniedGroups(Set<GroupIdentity> groups, String profileId)
651    {
652        ACLJCRAmetysObjectHelper.addDeniedGroups(groups, getNode(), profileId);
653    }
654    
655    public void removeDeniedGroups(Set<GroupIdentity> groups, String profileId)
656    {
657        ACLJCRAmetysObjectHelper.removeDeniedGroups(groups, getNode(), profileId);
658    }
659    
660    public void removeDeniedGroups(Set<GroupIdentity> groups)
661    {
662        ACLJCRAmetysObjectHelper.removeDeniedGroups(groups, getNode());
663    }
664    
665    public boolean isInheritanceDisallowed()
666    {
667        return ACLJCRAmetysObjectHelper.isInheritanceDisallowed(getNode());
668    }
669    
670    public void disallowInheritance(boolean disallow)
671    {
672        ACLJCRAmetysObjectHelper.disallowInheritance(getNode(), disallow);
673    }
674}