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