001/*
002 *  Copyright 2024 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.documents.jcr;
017
018import java.time.ZonedDateTime;
019import java.util.ArrayList;
020import java.util.List;
021import java.util.UUID;
022
023import javax.jcr.Node;
024import javax.jcr.NodeIterator;
025import javax.jcr.RepositoryException;
026
027import org.ametys.cms.repository.CommentableAmetysObject;
028import org.ametys.cms.repository.comment.Comment;
029import org.ametys.core.util.DateUtils;
030import org.ametys.plugins.explorer.resources.Resource;
031import org.ametys.plugins.explorer.resources.jcr.JCRResource;
032import org.ametys.plugins.repository.AmetysObject;
033import org.ametys.plugins.repository.AmetysRepositoryException;
034import org.ametys.plugins.repository.RepositoryConstants;
035import org.ametys.plugins.repository.data.holder.ModifiableModelLessDataHolder;
036import org.ametys.plugins.repository.data.holder.impl.DefaultModifiableModelLessDataHolder;
037import org.ametys.plugins.repository.data.repositorydata.ModifiableRepositoryData;
038import org.ametys.plugins.repository.data.repositorydata.impl.JCRRepositoryData;
039
040/**
041 * Implementation of {@link Resource} used for document module of workspaces and can handle comments, backed by a JCR node.<br>
042 */
043public class File extends JCRResource<FileFactory> implements CommentableAmetysObject<Comment>
044{
045
046    /** Tokens node */
047    public static final String TOKENS_NODE = RepositoryConstants.NAMESPACE_PREFIX + ":tokens";
048    /** Token node */
049    public static final String TOKEN_NODE = RepositoryConstants.NAMESPACE_PREFIX + ":token";
050    /** Token id */
051    public static final String TOKEN_ID = "tokenId";
052    /** Token id property */
053    public static final String TOKEN_ID_PROPERTY = RepositoryConstants.NAMESPACE_PREFIX + ":" + TOKEN_ID;
054    /** Creation date property for a token */
055    public static final String TOKEN_CREATION_DATE_PROPERTY = RepositoryConstants.NAMESPACE_PREFIX + ":creationDate";
056    /** File id property for a token */
057    public static final String TOKEN_FILE_ID_PROPERTY = RepositoryConstants.NAMESPACE_PREFIX + ":fileId";
058
059    /**
060     * Creates an {@link File}.
061     * @param node the node backing this {@link AmetysObject}
062     * @param parentPath the parentPath in the Ametys hierarchy
063     * @param factory the DefaultAmetysObjectFactory which created the AmetysObject
064     */
065    public File(Node node, String parentPath, FileFactory factory)
066    {
067        super(node, parentPath, factory);
068    }
069
070    public Comment createComment()
071    {
072        return new Comment(_getCommentsDataHolder());
073    }
074    
075    public Comment createComment(String commentId, ZonedDateTime creationDate)
076    {
077        return new Comment(_getCommentsDataHolder(), commentId, creationDate);
078    }
079    
080    public Comment getComment(String commentId) throws AmetysRepositoryException
081    {
082        return Comment.getComment(_getCommentsDataHolder(), commentId);
083    }
084    
085    public List<Comment> getComments(boolean includeNotValidatedComments, boolean includeValidatedComments) throws AmetysRepositoryException
086    {
087        return Comment.getComments(_getCommentsDataHolder(), includeNotValidatedComments, includeValidatedComments);
088    }
089
090    private ModifiableModelLessDataHolder _getCommentsDataHolder()
091    {
092        try
093        {
094            Node baseNode = getBaseNode();
095            if (!baseNode.hasNode("ametys:comments"))
096            {
097                baseNode.addNode("ametys:comments", "ametys:compositeMetadata");
098            }
099            Node commentsNode = baseNode.getNode("ametys:comments");
100            
101            baseNode.getSession().save();
102            
103            ModifiableRepositoryData repositoryData = new JCRRepositoryData(commentsNode);
104            return new DefaultModifiableModelLessDataHolder(_getFactory().getUnversionedDataTypeExtensionPoint(), repositoryData);
105        }
106        catch (RepositoryException e)
107        {
108            throw new AmetysRepositoryException(e);
109        }
110    }
111    
112    /**
113     * Token class representing a composite object with id, creation date, and file id.
114     * @param id the token id
115     * @param creationDate the creation date
116     * @param fileId  the file id
117     */
118    public record PublicLinkToken(String id, ZonedDateTime creationDate, String fileId) { /* empty */ }
119
120    // FIXME We only handle single token for now, so we just return the first one
121    /**
122     * Retrieves the file token. The JCR can handle multiple tokens, but only the first one is returned for now.
123     * @return the file token.
124     */
125    public PublicLinkToken getToken()
126    {
127        List<PublicLinkToken> tokens = new ArrayList<>();
128        try
129        {
130            Node fileNode = getNode();
131            if (fileNode.hasNode(TOKENS_NODE))
132            {
133                Node tokensNode = fileNode.getNode(TOKENS_NODE);
134                NodeIterator tokenNodes = tokensNode.getNodes();
135                while (tokenNodes.hasNext())
136                {
137                    Node tokenNode = tokenNodes.nextNode();
138                    String id = tokenNode.getProperty(TOKEN_ID_PROPERTY).getString();
139                    ZonedDateTime creationDate = DateUtils.asZonedDateTime(tokenNode.getProperty(TOKEN_CREATION_DATE_PROPERTY).getDate());
140                    String fileId = tokenNode.getProperty(TOKEN_FILE_ID_PROPERTY).getString();
141                    tokens.add(new PublicLinkToken(id, creationDate, fileId));
142                }
143            }
144        }
145        catch (RepositoryException e)
146        {
147            throw new AmetysRepositoryException("Cannot get tokens for file " + this.getName() + " (" + this.getId() + ")", e);
148        }
149        
150        if (tokens.isEmpty())
151        {
152            return null;
153        }
154        PublicLinkToken token = tokens.get(0);
155        
156        if (!this.getId().equals(token.fileId()))
157        {
158            return null;
159        }
160       
161        return token;
162    }
163
164    /**
165     * Stores the list of tokens.
166     * @param tokens the list of tokens.
167     */
168    public void setTokens(List<PublicLinkToken> tokens)
169    {
170        try
171        {
172            Node fileNode = getNode();
173            Node tokensNode;
174            if (fileNode.hasNode(TOKENS_NODE))
175            {
176                tokensNode = fileNode.getNode(TOKENS_NODE);
177                tokensNode.remove();
178            }
179            tokensNode = fileNode.addNode(TOKENS_NODE, "ametys:compositeMetadata");
180
181            for (PublicLinkToken token : tokens)
182            {
183                Node tokenNode = tokensNode.addNode(TOKEN_NODE, "ametys:compositeMetadata");
184                tokenNode.setProperty(TOKEN_ID_PROPERTY, token.id());
185                tokenNode.setProperty(TOKEN_CREATION_DATE_PROPERTY, DateUtils.asCalendar(token.creationDate()));
186                tokenNode.setProperty(TOKEN_FILE_ID_PROPERTY, token.fileId());
187            }
188        }
189        catch (RepositoryException e)
190        {
191            throw new AmetysRepositoryException("Cannot set tokens for file " + this.getName() + " (" + this.getId() + ")", e);
192        }
193    }
194    
195    /**
196     * Creates a new token with the current date as the creation date.
197     * @return the created token.
198     */
199    public PublicLinkToken createToken()
200    {
201        String tokenId = UUID.randomUUID().toString();
202        ZonedDateTime creationDate = ZonedDateTime.now();
203        PublicLinkToken newToken = new PublicLinkToken(tokenId, creationDate, this.getId());
204
205        // Create a new list of tokens for idempotence, this may change in the future if we want to handle multiple tokens
206        List<PublicLinkToken> tokens = new ArrayList<>();
207        tokens.add(newToken);
208        setTokens(tokens);
209
210        return newToken;
211    }
212}