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.ModifiableDataHolder;
036import org.ametys.plugins.repository.data.holder.impl.DefaultModifiableDataHolder;
037import org.ametys.plugins.repository.data.repositorydata.ModifiableRepositoryData;
038import org.ametys.plugins.repository.data.repositorydata.impl.JCRRepositoryData;
039import org.ametys.plugins.workspaces.project.objects.Project;
040import org.ametys.web.repository.SiteAwareAmetysObject;
041import org.ametys.web.repository.site.Site;
042
043/**
044 * Implementation of {@link Resource} used for document module of workspaces and can handle comments, backed by a JCR node.<br>
045 */
046public class File extends JCRResource<FileFactory> implements CommentableAmetysObject<Comment>, SiteAwareAmetysObject
047{
048
049    /** Tokens node */
050    public static final String TOKENS_NODE = RepositoryConstants.NAMESPACE_PREFIX + ":tokens";
051    /** Token node */
052    public static final String TOKEN_NODE = RepositoryConstants.NAMESPACE_PREFIX + ":token";
053    /** Token id */
054    public static final String TOKEN_ID = "tokenId";
055    /** Token id property */
056    public static final String TOKEN_ID_PROPERTY = RepositoryConstants.NAMESPACE_PREFIX + ":" + TOKEN_ID;
057    /** Creation date property for a token */
058    public static final String TOKEN_CREATION_DATE_PROPERTY = RepositoryConstants.NAMESPACE_PREFIX + ":creationDate";
059    /** File id property for a token */
060    public static final String TOKEN_FILE_ID_PROPERTY = RepositoryConstants.NAMESPACE_PREFIX + ":fileId";
061
062    /**
063     * Creates an {@link File}.
064     * @param node the node backing this {@link AmetysObject}
065     * @param parentPath the parentPath in the Ametys hierarchy
066     * @param factory the DefaultAmetysObjectFactory which created the AmetysObject
067     */
068    public File(Node node, String parentPath, FileFactory factory)
069    {
070        super(node, parentPath, factory);
071    }
072    
073    /**
074     * Get the file description
075     * @return the description
076     */
077    public String getDescription()
078    {
079        try
080        {
081            Node fileNode = getNode();
082            return fileNode.hasProperty("ametys:description")
083                ? fileNode.getProperty("ametys:description").getString()
084                : null;
085        }
086        catch (RepositoryException e)
087        {
088            throw new AmetysRepositoryException("Cannot get description for file " + this.getName() + " (" + this.getId() + ")", e);
089        }
090    }
091    
092    /**
093     * Set the file description
094     * @param description the description to set
095     */
096    public void setDescription (String description)
097    {
098        try
099        {
100            Node fileNode = getNode();
101            fileNode.setProperty("ametys:description", description);
102        }
103        catch (RepositoryException e)
104        {
105            throw new AmetysRepositoryException("Cannot set description for file " + this.getName() + " (" + this.getId() + ")", e);
106        }
107    }
108    /**
109     * <code>true</code> if the description was automatically generated, <code>false</code> otherwise.
110     * @return <code>true</code> if the description was automatically generated, <code>false</code> otherwise.
111     */
112    public boolean isAutomaticallyGeneratedDescription()
113    {
114        try
115        {
116            Node fileNode = getNode();
117            if (fileNode.hasProperty("ametys:isAutoGeneratedDescription"))
118            {
119                return fileNode.getProperty("ametys:isAutoGeneratedDescription").getBoolean();
120            }
121            else
122            {
123                return false;
124            }
125        }
126        catch (RepositoryException e)
127        {
128            throw new AmetysRepositoryException("Cannot get automatic description status for file " + this.getName() + " (" + this.getId() + ")", e);
129        }
130    }
131    /**
132     * Set to <code>true</code> if the description was automatically generated, <code>false</code> otherwise.
133     * @param isAutoGeneratedDescription <code>true</code> if the description was automatically generated
134     */
135    public void setIsAutoGeneratedDescription(boolean isAutoGeneratedDescription)
136    {
137        try
138        {
139            Node fileNode = getNode();
140            fileNode.setProperty("ametys:isAutoGeneratedDescription", isAutoGeneratedDescription);
141        }
142        catch (RepositoryException e)
143        {
144            throw new AmetysRepositoryException("Cannot set automatic description status for file " + this.getName() + " (" + this.getId() + ")", e);
145        }
146    }
147    
148    public Comment createComment()
149    {
150        return new Comment(_getCommentsDataHolder());
151    }
152    
153    public Comment createComment(String commentId, ZonedDateTime creationDate)
154    {
155        return new Comment(_getCommentsDataHolder(), commentId, creationDate);
156    }
157    
158    public Comment getComment(String commentId) throws AmetysRepositoryException
159    {
160        return Comment.getComment(_getCommentsDataHolder(), commentId);
161    }
162    
163    public List<Comment> getComments(boolean includeNotValidatedComments, boolean includeValidatedComments) throws AmetysRepositoryException
164    {
165        return Comment.getComments(_getCommentsDataHolder(), includeNotValidatedComments, includeValidatedComments);
166    }
167
168    private ModifiableDataHolder _getCommentsDataHolder()
169    {
170        try
171        {
172            Node baseNode = getBaseNode();
173            if (!baseNode.hasNode("ametys:comments"))
174            {
175                baseNode.addNode("ametys:comments", "ametys:compositeMetadata");
176            }
177            Node commentsNode = baseNode.getNode("ametys:comments");
178            
179            baseNode.getSession().save();
180            
181            ModifiableRepositoryData repositoryData = new JCRRepositoryData(commentsNode);
182            return new DefaultModifiableDataHolder(_getFactory().getUnversionedDataTypeExtensionPoint(), repositoryData);
183        }
184        catch (RepositoryException e)
185        {
186            throw new AmetysRepositoryException(e);
187        }
188    }
189    
190    /**
191     * Token class representing a composite object with id, creation date, and file id.
192     * @param id the token id
193     * @param creationDate the creation date
194     * @param fileId  the file id
195     */
196    public record PublicLinkToken(String id, ZonedDateTime creationDate, String fileId) { /* empty */ }
197
198    // FIXME We only handle single token for now, so we just return the first one
199    /**
200     * Retrieves the file token. The JCR can handle multiple tokens, but only the first one is returned for now.
201     * @return the file token.
202     */
203    public PublicLinkToken getToken()
204    {
205        List<PublicLinkToken> tokens = new ArrayList<>();
206        try
207        {
208            Node fileNode = getNode();
209            if (fileNode.hasNode(TOKENS_NODE))
210            {
211                Node tokensNode = fileNode.getNode(TOKENS_NODE);
212                NodeIterator tokenNodes = tokensNode.getNodes();
213                while (tokenNodes.hasNext())
214                {
215                    Node tokenNode = tokenNodes.nextNode();
216                    String id = tokenNode.getProperty(TOKEN_ID_PROPERTY).getString();
217                    ZonedDateTime creationDate = DateUtils.asZonedDateTime(tokenNode.getProperty(TOKEN_CREATION_DATE_PROPERTY).getDate());
218                    String fileId = tokenNode.getProperty(TOKEN_FILE_ID_PROPERTY).getString();
219                    tokens.add(new PublicLinkToken(id, creationDate, fileId));
220                }
221            }
222        }
223        catch (RepositoryException e)
224        {
225            throw new AmetysRepositoryException("Cannot get tokens for file " + this.getName() + " (" + this.getId() + ")", e);
226        }
227        
228        if (tokens.isEmpty())
229        {
230            return null;
231        }
232        PublicLinkToken token = tokens.get(0);
233        
234        if (!this.getId().equals(token.fileId()))
235        {
236            return null;
237        }
238       
239        return token;
240    }
241
242    /**
243     * Stores the list of tokens.
244     * @param tokens the list of tokens.
245     */
246    public void setTokens(List<PublicLinkToken> tokens)
247    {
248        try
249        {
250            Node fileNode = getNode();
251            Node tokensNode;
252            if (fileNode.hasNode(TOKENS_NODE))
253            {
254                tokensNode = fileNode.getNode(TOKENS_NODE);
255                tokensNode.remove();
256            }
257            tokensNode = fileNode.addNode(TOKENS_NODE, "ametys:compositeMetadata");
258
259            for (PublicLinkToken token : tokens)
260            {
261                Node tokenNode = tokensNode.addNode(TOKEN_NODE, "ametys:compositeMetadata");
262                tokenNode.setProperty(TOKEN_ID_PROPERTY, token.id());
263                tokenNode.setProperty(TOKEN_CREATION_DATE_PROPERTY, DateUtils.asCalendar(token.creationDate()));
264                tokenNode.setProperty(TOKEN_FILE_ID_PROPERTY, token.fileId());
265            }
266        }
267        catch (RepositoryException e)
268        {
269            throw new AmetysRepositoryException("Cannot set tokens for file " + this.getName() + " (" + this.getId() + ")", e);
270        }
271    }
272    
273    /**
274     * Creates a new token with the current date as the creation date.
275     * @return the created token.
276     */
277    public PublicLinkToken createToken()
278    {
279        String tokenId = UUID.randomUUID().toString();
280        ZonedDateTime creationDate = ZonedDateTime.now();
281        PublicLinkToken newToken = new PublicLinkToken(tokenId, creationDate, this.getId());
282
283        // Create a new list of tokens for idempotence, this may change in the future if we want to handle multiple tokens
284        List<PublicLinkToken> tokens = new ArrayList<>();
285        tokens.add(newToken);
286        setTokens(tokens);
287
288        return newToken;
289    }
290
291    public String getSiteName() throws AmetysRepositoryException
292    {
293        return getSite().getName();
294    }
295
296    public Site getSite() throws AmetysRepositoryException
297    {
298        Project project = _getFactory().getProjectManager().getParentProject(this);
299        return project.getSite();
300    }
301}