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