001/*
002 *  Copyright 2021 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.extraction.execution;
017
018import java.io.IOException;
019import java.util.Collection;
020import java.util.HashMap;
021import java.util.List;
022import java.util.Map;
023import java.util.Set;
024
025import org.apache.avalon.framework.activity.Initializable;
026import org.apache.avalon.framework.component.Component;
027import org.apache.avalon.framework.service.ServiceException;
028import org.apache.avalon.framework.service.ServiceManager;
029import org.apache.avalon.framework.service.Serviceable;
030import org.apache.commons.io.FileUtils;
031import org.apache.commons.lang3.StringUtils;
032import org.apache.excalibur.source.SourceException;
033import org.apache.excalibur.source.SourceResolver;
034import org.apache.excalibur.source.TraversableSource;
035import org.apache.excalibur.source.impl.FileSource;
036
037import org.ametys.core.cache.AbstractCacheManager;
038import org.ametys.core.cache.Cache;
039import org.ametys.core.file.FileHelper;
040import org.ametys.core.group.GroupIdentity;
041import org.ametys.core.right.ProfileAssignmentStorage.AnonymousOrAnyConnectedKeys;
042import org.ametys.core.right.ProfileAssignmentStorage.UserOrGroup;
043import org.ametys.core.right.ProfileAssignmentStorageExtensionPoint;
044import org.ametys.core.right.RightManager;
045import org.ametys.core.right.RightManager.RightResult;
046import org.ametys.core.ui.Callable;
047import org.ametys.core.user.CurrentUserProvider;
048import org.ametys.core.user.UserIdentity;
049import org.ametys.plugins.core.user.UserHelper;
050import org.ametys.plugins.extraction.ExtractionConstants;
051import org.ametys.plugins.extraction.rights.ExtractionAccessController;
052import org.ametys.runtime.i18n.I18nizableText;
053import org.ametys.runtime.plugin.component.AbstractLogEnabled;
054
055/**
056 * Object representing the extraction definition file content
057 */
058public class ExtractionDAO extends AbstractLogEnabled implements Serviceable, Component, Initializable
059{
060    /** The Avalon role */
061    public static final String ROLE = ExtractionDAO.class.getName();
062    
063    /** Extraction author cache id */
064    private static final String EXTRACTION_AUTHOR_CACHE = ExtractionDAO.class.getName() + "$extractionAuthor";
065    
066    private CurrentUserProvider _userProvider;
067    private RightManager _rightManager;
068    private SourceResolver _sourceResolver;
069    private ExtractionDefinitionReader _definitionReader;
070    private ProfileAssignmentStorageExtensionPoint _profileAssignmentStorageEP;
071    private CurrentUserProvider _currentUserProvider;
072    private AbstractCacheManager _cacheManager;
073    private UserHelper _userHelper;
074    private TraversableSource _root;
075    private FileHelper _fileHelper;
076    
077    public void service(ServiceManager manager) throws ServiceException
078    {
079        _userProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
080        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
081        _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
082        _definitionReader = (ExtractionDefinitionReader) manager.lookup(ExtractionDefinitionReader.ROLE);
083        _profileAssignmentStorageEP = (ProfileAssignmentStorageExtensionPoint) manager.lookup(ProfileAssignmentStorageExtensionPoint.ROLE);
084        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
085        _cacheManager = (AbstractCacheManager) manager.lookup(AbstractCacheManager.ROLE);
086        _userHelper = (UserHelper) manager.lookup(UserHelper.ROLE);
087        _fileHelper = (FileHelper) manager.lookup(FileHelper.ROLE);
088    }
089
090    public void initialize() throws Exception
091    {
092        _root = (TraversableSource) _sourceResolver.resolveURI(ExtractionConstants.DEFINITIONS_DIR);
093
094        _cacheManager.createRequestCache(EXTRACTION_AUTHOR_CACHE,
095                new I18nizableText("plugin.extraction", "PLUGINS_EXTRACTION_CACHE_DEFINITION_AUTHOR_LABEL"),
096                new I18nizableText("plugin.extraction", "PLUGINS_EXTRACTION_CACHE_DEFINITION_AUTHOR_DESCRIPTION"),
097                true);
098    }
099    
100    /**
101     * Get the root container properties
102     * @return The root container properties
103     * @throws IOException If an error occurred while reading folder
104     */
105    @Callable(rights = Callable.NO_CHECK_REQUIRED) // required for bus message
106    public Map<String, Object> getRootProperties() throws IOException
107    {
108        TraversableSource rootDir = (TraversableSource) _sourceResolver.resolveURI(ExtractionConstants.DEFINITIONS_DIR);
109        return getExtractionContainerProperties(rootDir, rootDir, true);
110    }
111
112    /**
113     * Get the extraction folder properties
114     * @param relPath the relative folder path
115     * @return the extraction container properties
116     * @throws IOException if an error occured
117     */
118    @Callable (rights = Callable.NO_CHECK_REQUIRED) // required for bus message
119    public Map<String, Object> getExtractionContainerProperties(String relPath) throws IOException
120    {
121        TraversableSource root = (TraversableSource) _sourceResolver.resolveURI(ExtractionConstants.DEFINITIONS_DIR);
122        TraversableSource folderSrc = (FileSource) _sourceResolver.resolveURI(ExtractionConstants.DEFINITIONS_DIR + relPath);
123        return getExtractionContainerProperties(root, folderSrc, true);
124    }
125    
126    /**
127     * Get extraction container properties
128     * @param root the root of extraction definitions
129     * @param folder the source of the extraction container
130     * @param withRights true to include rights information
131     * @return The extraction container properties
132     */
133    public Map<String, Object> getExtractionContainerProperties(TraversableSource root, TraversableSource folder, boolean withRights)
134    {
135        Map<String, Object> infos = new HashMap<>();
136        
137        infos.put("type", "collection");
138        infos.put("isRoot", folder.getURI().equals(root.getURI()));
139        infos.put("name", folder.getName());
140        infos.put("path", _getRelativePath(root, folder));
141        
142        if (withRights)
143        {
144            UserIdentity currentUser = _userProvider.getUser();
145            infos.put("canRead", canRead(currentUser, folder));
146            infos.put("canRename", canRename(currentUser, folder));
147            infos.put("canWrite", canWrite(currentUser, folder));
148            infos.put("canDelete", canDelete(currentUser, folder));
149            infos.put("canAssignRights", canAssignRights(currentUser, folder));
150        }
151        
152        return infos;
153    }
154    
155    private String _getRelativePath (TraversableSource root, TraversableSource file)
156    {
157        String relPath = StringUtils.substringAfter(file.getURI(), root.getURI());
158        return StringUtils.removeEnd(relPath, "/");
159    }
160    
161    /**
162     * Get the extraction properties
163     * @param relDefinitionFilePath the relative fiel path
164     * @return the extraction properties
165     * @throws Exception if an error occured
166     */
167    @Callable (rights = Callable.NO_CHECK_REQUIRED) // required for bus message
168    public Map<String, Object> getExtractionProperties(String relDefinitionFilePath) throws Exception
169    {
170        FileSource root = (FileSource) _sourceResolver.resolveURI(ExtractionConstants.DEFINITIONS_DIR);
171        FileSource fileSource = (FileSource) _sourceResolver.resolveURI(ExtractionConstants.DEFINITIONS_DIR + relDefinitionFilePath);
172        Extraction extraction = _definitionReader.readExtractionDefinitionFile(fileSource.getFile());
173        return getExtractionProperties(extraction, root, fileSource, true);
174    }
175    
176    /**
177     * Get extraction properties
178     * @param extraction the extraction
179     * @param root the root of extraction definitions
180     * @param file the source of the extraction
181     * @param withRights true to include rights information
182     * @return The extraction properties
183     */
184    public Map<String, Object> getExtractionProperties(Extraction extraction, TraversableSource root, TraversableSource file, boolean withRights)
185    {
186        Map<String, Object> infos = new HashMap<>();
187        
188        infos.put("name", file.getName());
189        infos.put("path", _getRelativePath(root, file));
190        
191        infos.put("descriptionId", extraction.getDescriptionId());
192        
193        UserIdentity author = extraction.getAuthor();
194        infos.put("author", _userHelper.user2json(author));
195  
196        if (withRights)
197        {
198            UserIdentity currentUser = _userProvider.getUser();
199            infos.put("canRead", canRead(currentUser, file));
200            infos.put("canWrite", canWrite(currentUser, file));
201            infos.put("canDelete", canDelete(currentUser, file));
202            infos.put("canAssignRights", canAssignRights(currentUser, file));
203        }
204
205        return infos;
206    }
207
208    /**
209     * Check if a folder has a descendant in read access for a given user
210     * @param userIdentity the user
211     * @param folder the source of the extraction container
212     * @return <code>true</code> if the folder has a descendant in read access, <code>false</code> otherwise
213     */
214    public Boolean hasAnyReadableDescendant(UserIdentity userIdentity, TraversableSource folder)
215    {
216        try
217        {
218            if (folder.exists())
219            {
220                for (TraversableSource child : (Collection<TraversableSource>) folder.getChildren())
221                {
222                    if (child.isCollection())
223                    {
224                        if (canRead(userIdentity, child) || hasAnyReadableDescendant(userIdentity, child))
225                        {
226                            return true;
227                        }
228                    }
229                    else if (child.getName().endsWith(".xml") && canRead(userIdentity, child))
230                    {
231                        return true;
232                    }
233                }
234            }
235            
236            return false;
237        }
238        catch (SourceException e)
239        {
240            throw new RuntimeException("Cannot list child elements of " + folder.getURI(), e);
241        }
242    }
243
244    /**
245     * Check if a folder have descendant in write access for a given user
246     * @param userIdentity the user identity
247     * @param folder the source of the extraction container
248     * @return true if the user have write right for at least one child of this container
249     */
250    public Boolean hasAnyWritableDescendant(UserIdentity userIdentity, TraversableSource folder)
251    {
252        return hasAnyWritableDescendant(userIdentity, folder, false);
253    }
254    
255    /**
256     * Check if a folder have descendant in write access for a given user
257     * @param userIdentity the user identity
258     * @param folder the source of the extraction container
259     * @param ignoreExtraction true to ignore extraction file from search (rights will check only on containers)
260     * @return true if the user have write right for at least one child of this container
261     */
262    public Boolean hasAnyWritableDescendant(UserIdentity userIdentity, TraversableSource folder, boolean ignoreExtraction)
263    {
264        try
265        {
266            if (folder.exists())
267            {
268                for (TraversableSource child : (Collection<TraversableSource>) folder.getChildren())
269                {
270                    if (child.isCollection())
271                    {
272                        if (canWrite(userIdentity, child) || hasAnyWritableDescendant(userIdentity, child))
273                        {
274                            return true;
275                        }
276                    }
277                    else if (!ignoreExtraction && child.getName().endsWith(".xml") && canWrite(userIdentity, child))
278                    {
279                        return true;
280                    }
281                }
282            }
283            
284            return false;
285        }
286        catch (SourceException e)
287        {
288            throw new RuntimeException("Cannot list child elements of " + folder.getURI(), e);
289        }
290    }
291    
292    /**
293     * Checks if a folder has descendants in the right assignment access for a given user
294     * @param userIdentity the user
295     * @param folder the source of the extraction container
296     * @return <code>true</code> if the folder has descendant, <code>false</code> otherwise
297     */
298    public boolean hasAnyAssignableDescendant(UserIdentity userIdentity, TraversableSource folder)
299    {
300        try
301        {
302            if (folder.exists())
303            {
304                for (TraversableSource child : (Collection<TraversableSource>) folder.getChildren())
305                {
306                    if (child.isCollection())
307                    {
308                        if (canAssignRights(userIdentity, child) || hasAnyAssignableDescendant(userIdentity, child))
309                        {
310                            return true;
311                        }
312                    }
313                    else if (child.getName().endsWith(".xml"))
314                    {
315                        if (canAssignRights(userIdentity, child))
316                        {
317                            return true;
318                        }
319                    }
320                }
321            }
322            
323            return false;
324        }
325        catch (SourceException e)
326        {
327            throw new RuntimeException("Cannot list child elements of " + folder.getURI(), e);
328        }
329    }
330    
331    /**
332     * Check if a user has read rights on an extraction container or file
333     * @param userIdentity the user
334     * @param source the source of the extraction container or file
335     * @return <code>true</code> if the user has read rights on an extraction container, <code>false</code> otherwise
336     */
337    public boolean canRead(UserIdentity userIdentity, TraversableSource source)
338    {
339        return _rightManager.hasReadAccess(userIdentity, source) || canWrite(userIdentity, source);
340    }
341    
342    /**
343     * Check if a user has write rights on an extraction container or an extraction
344     * @param userIdentity the user
345     * @param source the source of the extraction file or extration container
346     * @return <code>true</code> if the user has write rights on an extraction container, <code>false</code> otherwise
347     */
348    public boolean canWrite(UserIdentity userIdentity, TraversableSource source)
349    {
350        return canWrite(userIdentity, source, false);
351    }
352    
353    /**
354     * Determines if the user can rename an extraction container
355     * @param userIdentity the user
356     * @param folder the extraction container
357     * @return true if the user can delete the extraction container
358     */
359    public boolean canRename(UserIdentity userIdentity, TraversableSource folder)
360    {
361        try
362        {
363            return !_isRoot(folder) // is not root
364                    && canWrite(userIdentity, folder) // has write access
365                    && canWrite(userIdentity, (TraversableSource) folder.getParent()); // has write access on parent
366        }
367        catch (SourceException e)
368        {
369            throw new RuntimeException("Unable to determine user rights on the extraction container " + folder.getURI(), e);
370        }
371    }
372    
373    /**
374     * Determines if the user can delete an extraction container or the extraction file
375     * @param userIdentity the user
376     * @param source the extraction container or the extraction file
377     * @return true if the user can delete the extraction container
378     */
379    public boolean canDelete(UserIdentity userIdentity, TraversableSource source)
380    {
381        try
382        {
383            return !_isRoot(source) // is not root
384                && canWrite(userIdentity, (TraversableSource) source.getParent()) // has write access on parent
385                && canWrite(userIdentity, source, true); // has write access on itselft and each descendant
386        }
387        catch (SourceException e)
388        {
389            throw new RuntimeException("Unable to determine user rights on extraction container or file at uri " + source.getURI(), e);
390        }
391    }
392    
393    /**
394     * Check if a user has write access on an extraction container
395     * @param userIdentity the user user identity
396     * @param source the extraction container or the extraction file
397     * @param recursively true to check write access on all descendants recursively
398     * @return true if the user has write access on the extraction container
399     */
400    public boolean canWrite(UserIdentity userIdentity, TraversableSource source, boolean recursively)
401    {
402        boolean hasRight = _rightManager.hasRight(userIdentity, ExtractionConstants.MODIFY_EXTRACTION_RIGHT_ID, source) == RightResult.RIGHT_ALLOW;
403        if (!hasRight)
404        {
405            return false;
406        }
407           
408        try
409        {
410            if (recursively && source.isCollection())
411            {
412                for (TraversableSource child : (Collection<TraversableSource>) source.getChildren())
413                {
414                    hasRight = hasRight && canWrite(userIdentity, child);
415                    
416                    if (!hasRight)
417                    {
418                        return false;
419                    }
420                }
421            }
422            
423            return hasRight;
424        }
425        catch (SourceException e)
426        {
427            throw new RuntimeException("Unable to determine user rights on extraction container " + source.getURI(), e);
428        }
429    }
430    
431    /**
432     * Check if a user can edit rights on an extraction container or an extraction file
433     * @param userIdentity the user
434     * @param source the source of the extraction container or file
435     * @return true if the user can edit rights on an extraction container or file
436     */
437    public boolean canAssignRights(UserIdentity userIdentity, TraversableSource source)
438    {
439        try
440        {
441            return _rightManager.hasRight(userIdentity, "Runtime_Rights_Rights_Handle", "/cms") == RightResult.RIGHT_ALLOW
442                    || !_isRoot(source) // is not root
443                    && canWrite(userIdentity, (TraversableSource) source.getParent()) // has write access on parent
444                    && canWrite(userIdentity, source, true); // has write access on itselft and each descendant
445        }
446        catch (SourceException e)
447        {
448            throw new RuntimeException("Unable to determine the user rights on the extraction container or file at uri " + source.getURI(), e);
449        }
450    }
451    
452    /**
453     * Determines if the extraction container is the root node
454     * @param folder the extraction container
455     * @return true if is root
456     */
457    protected boolean _isRoot(TraversableSource folder)
458    {
459        return trimLastFileSeparator(_root.getURI()).equals(trimLastFileSeparator(folder.getURI()));
460    }
461
462    /**
463     * Get the path for rights of an extraction container or file
464     * @param source the source of extraction container or file
465     * @return the path for rights
466     */
467    public String getExtractionRightPath(TraversableSource source)
468    {
469        String rootURI = trimLastFileSeparator(_root.getURI());
470        String sourceURI = source.getURI();
471        
472        if (!sourceURI.startsWith(rootURI))
473        {
474            // The source is an extraction source
475            return null;
476        }
477        
478        // Get only the part after the root folder to get the relative path
479        String relPath = StringUtils.substringAfter(trimLastFileSeparator(sourceURI), rootURI);
480
481        // In some case, relPath can start with a /, we need to trim it to test if it is an empty path corresponding to the root
482        if (relPath.startsWith("/"))
483        {
484            relPath = StringUtils.substringAfter(relPath, "/");
485        }
486        
487        return StringUtils.isEmpty(relPath) ? ExtractionAccessController.ROOT_CONTEXT : ExtractionAccessController.ROOT_CONTEXT + "/" + relPath;
488    }
489    
490    /**
491     * Get the source corresponding to the right context of an extraction container or file
492     * @param rightContext The rights context such as '/extraction-dir/path/to/file
493     * @return the resolved source file or null if the given context is not an extraction context
494     * @throws IOException if an error occured
495     */
496    public TraversableSource getExtractionSource(String rightContext) throws IOException
497    {
498        if (rightContext.startsWith(ExtractionAccessController.ROOT_CONTEXT))
499        {
500            String relPath = StringUtils.substringAfter(rightContext, ExtractionAccessController.ROOT_CONTEXT);
501            String fileUri = ExtractionConstants.DEFINITIONS_DIR + relPath;
502            return (TraversableSource) _sourceResolver.resolveURI(fileUri);
503        }
504        
505        return null;
506    }
507    
508    /**
509     * Copy rights from one context to another one
510     * @param sourceContext the source context
511     * @param targetContext the target context
512     */
513    public void copyRights(String sourceContext, String targetContext)
514    {
515        // Get the mapping between users and profiles
516        Map<UserIdentity, Map<UserOrGroup, Set<String>>> profilesForUsers = _profileAssignmentStorageEP.getProfilesForUsers(sourceContext, null);
517        // Copy allowed user assignment profiles to new context
518        profilesForUsers.entrySet()
519            .forEach(entry -> _copyAllowedUsers(entry.getKey(), entry.getValue().get(UserOrGroup.ALLOWED), targetContext));
520        // Copy denied user assignment profiles to new context
521        profilesForUsers.entrySet()
522            .forEach(entry -> _copyDeniedUsers(entry.getKey(), entry.getValue().get(UserOrGroup.DENIED), targetContext));
523        
524        // Get the mapping between groups and profiles
525        Map<GroupIdentity, Map<UserOrGroup, Set<String>>> profilesForGroups = _profileAssignmentStorageEP.getProfilesForGroups(sourceContext, null);
526        // Copy allowed group assignment profiles to new context
527        profilesForGroups.entrySet()
528            .forEach(entry -> _copyAllowedGroups(entry.getKey(), entry.getValue().get(UserOrGroup.ALLOWED), targetContext));
529        // Copy denied group assignment profiles to new context
530        profilesForGroups.entrySet()
531            .forEach(entry -> _copyDeniedGroups(entry.getKey(), entry.getValue().get(UserOrGroup.DENIED), targetContext));
532        
533        // Get the mapping between anonymous or any connected user and profiles
534        Map<AnonymousOrAnyConnectedKeys, Set<String>> profilesForAnonymousOrAnyConnectedUser = _profileAssignmentStorageEP.getProfilesForAnonymousAndAnyConnectedUser(sourceContext);
535        // Copy allowed anonymous user assignment profiles to new context
536        profilesForAnonymousOrAnyConnectedUser.get(AnonymousOrAnyConnectedKeys.ANONYMOUS_ALLOWED)
537            .forEach(profileId -> _profileAssignmentStorageEP.allowProfileToAnonymous(profileId, targetContext));
538        // Copy denied anonymous user assignment profiles to new context
539        profilesForAnonymousOrAnyConnectedUser.get(AnonymousOrAnyConnectedKeys.ANONYMOUS_DENIED)
540            .forEach(profileId -> _profileAssignmentStorageEP.denyProfileToAnonymous(profileId, targetContext));
541        // Copy allowed any connected user assignment profiles to new context
542        profilesForAnonymousOrAnyConnectedUser.get(AnonymousOrAnyConnectedKeys.ANYCONNECTEDUSER_ALLOWED)
543            .forEach(profileId -> _profileAssignmentStorageEP.allowProfileToAnyConnectedUser(profileId, targetContext));
544        // Copy denied any connected user assignment profiles to new context
545        profilesForAnonymousOrAnyConnectedUser.get(AnonymousOrAnyConnectedKeys.ANYCONNECTEDUSER_DENIED)
546            .forEach(profileId -> _profileAssignmentStorageEP.denyProfileToAnyConnectedUser(profileId, targetContext));
547    }
548    
549    private void _copyAllowedUsers(UserIdentity userIdentity, Set<String> profiles, String context)
550    {
551        profiles.forEach(profile -> _profileAssignmentStorageEP.allowProfileToUser(userIdentity, profile, context));
552    }
553    
554    private void _copyDeniedUsers(UserIdentity userIdentity, Set<String> profiles, String context)
555    {
556        profiles.forEach(profile -> _profileAssignmentStorageEP.denyProfileToUser(userIdentity, profile, context));
557    }
558    
559    private void _copyAllowedGroups(GroupIdentity groupIdentity, Set<String> profiles, String context)
560    {
561        profiles.forEach(profile -> _profileAssignmentStorageEP.allowProfileToGroup(groupIdentity, profile, context));
562    }
563    
564    private void _copyDeniedGroups(GroupIdentity groupIdentity, Set<String> profiles, String context)
565    {
566        profiles.forEach(profile -> _profileAssignmentStorageEP.denyProfileToGroup(groupIdentity, profile, context));
567    }
568    
569    /**
570     * Delete rights from a context
571     * @param context the context
572     */
573    public void deleteRights(String context)
574    {
575        // Get the mapping between users and profiles
576        Map<UserIdentity, Map<UserOrGroup, Set<String>>> profilesForUsers = _profileAssignmentStorageEP.getProfilesForUsers(context, null);
577        // Copy allowed user assignment profiles to new context
578        profilesForUsers.entrySet()
579            .forEach(entry -> _removeAllowedUsers(entry.getKey(), entry.getValue().get(UserOrGroup.ALLOWED), context));
580        // Copy denied user assignment profiles to new context
581        profilesForUsers.entrySet()
582            .forEach(entry -> _removeDeniedUsers(entry.getKey(), entry.getValue().get(UserOrGroup.DENIED), context));
583        
584        // Get the mapping between groups and profiles
585        Map<GroupIdentity, Map<UserOrGroup, Set<String>>> profilesForGroups = _profileAssignmentStorageEP.getProfilesForGroups(context, null);
586        // Copy allowed group assignment profiles to new context
587        profilesForGroups.entrySet()
588            .forEach(entry -> _removeAllowedGroups(entry.getKey(), entry.getValue().get(UserOrGroup.ALLOWED), context));
589        // Copy denied group assignment profiles to new context
590        profilesForGroups.entrySet()
591            .forEach(entry -> _removeDeniedGroups(entry.getKey(), entry.getValue().get(UserOrGroup.DENIED), context));
592        
593        // Get the mapping between anonymous or any connected user and profiles
594        Map<AnonymousOrAnyConnectedKeys, Set<String>> profilesForAnonymousOrAnyConnectedUser = _profileAssignmentStorageEP.getProfilesForAnonymousAndAnyConnectedUser(context);
595        // Copy allowed anonymous user assignment profiles to new context
596        profilesForAnonymousOrAnyConnectedUser.get(AnonymousOrAnyConnectedKeys.ANONYMOUS_ALLOWED)
597            .forEach(profileId -> _profileAssignmentStorageEP.removeAllowedProfileFromAnonymous(profileId, context));
598        // Copy denied anonymous user assignment profiles to new context
599        profilesForAnonymousOrAnyConnectedUser.get(AnonymousOrAnyConnectedKeys.ANONYMOUS_DENIED)
600            .forEach(profileId -> _profileAssignmentStorageEP.removeDeniedProfileFromAnonymous(profileId, context));
601        // Copy allowed any connected user assignment profiles to new context
602        profilesForAnonymousOrAnyConnectedUser.get(AnonymousOrAnyConnectedKeys.ANYCONNECTEDUSER_ALLOWED)
603            .forEach(profileId -> _profileAssignmentStorageEP.removeAllowedProfileFromAnyConnectedUser(profileId, context));
604        // Copy denied any connected user assignment profiles to new context
605        profilesForAnonymousOrAnyConnectedUser.get(AnonymousOrAnyConnectedKeys.ANYCONNECTEDUSER_DENIED)
606            .forEach(profileId -> _profileAssignmentStorageEP.removeDeniedProfileFromAnyConnectedUser(profileId, context));
607    }
608    
609    private void _removeAllowedUsers(UserIdentity userIdentity, Set<String> profiles, String context)
610    {
611        profiles.forEach(profile -> _profileAssignmentStorageEP.removeAllowedProfileFromUser(userIdentity, profile, context));
612    }
613    
614    private void _removeDeniedUsers(UserIdentity userIdentity, Set<String> profiles, String context)
615    {
616        profiles.forEach(profile -> _profileAssignmentStorageEP.removeDeniedProfileFromUser(userIdentity, profile, context));
617    }
618    
619    private void _removeAllowedGroups(GroupIdentity groupIdentity, Set<String> profiles, String context)
620    {
621        profiles.forEach(profile -> _profileAssignmentStorageEP.removeAllowedProfileFromGroup(groupIdentity, profile, context));
622    }
623    
624    private void _removeDeniedGroups(GroupIdentity groupIdentity, Set<String> profiles, String context)
625    {
626        profiles.forEach(profile -> _profileAssignmentStorageEP.removeDeniedProfileFromGroup(groupIdentity, profile, context));
627    }
628
629    /**
630     * Move an extraction file or folder inside a given directory
631     * 
632     * @param srcRelPath The relative URI of file/folder to move
633     * @param targetRelPath The target relative URI of file/folder to move
634     * @return a result map with the name and uri of moved file in case of
635     *         success.
636     * @throws IOException If an error occurred manipulating the source
637     */
638    @Callable (rights = ExtractionConstants.MODIFY_EXTRACTION_RIGHT_ID)
639    public Map<String, Object> moveOrRenameExtractionDefinitionFile(String srcRelPath, String targetRelPath) throws IOException
640    {
641        Map<String, Object> result = new HashMap<>();
642
643        FileSource srcFile = null;
644        FileSource targetFile = null;
645        try
646        {
647            srcFile = (FileSource) _sourceResolver.resolveURI(ExtractionConstants.DEFINITIONS_DIR + srcRelPath);
648            targetFile = (FileSource) _sourceResolver.resolveURI(ExtractionConstants.DEFINITIONS_DIR + targetRelPath);
649
650            String sourceContext = ExtractionAccessController.ROOT_CONTEXT + "/" + srcRelPath;
651            String targetContext = ExtractionAccessController.ROOT_CONTEXT + "/" + targetRelPath;
652
653            result = _moveOrRenameSource(srcFile, targetFile, sourceContext, targetContext);
654            
655            if (result.containsKey("uri"))
656            {
657                String newURI = (String) result.get("uri");
658                String path = newURI.substring(_root.getURI().length());
659                result.put("path", path);
660            }
661        }
662        finally
663        {
664            _sourceResolver.release(srcFile);
665            _sourceResolver.release(targetFile);
666        }
667        
668        return result;
669    }
670        
671    /**
672     * Move a file or folder
673     * 
674     * @param sourceFile The file/folder to move
675     * @param targetFile The target file
676     * @param sourceContext the source context
677     * @param targetContext the target context
678     * @return a result map with the name and uri of moved file in case of
679     *         success.
680     * @throws IOException If an error occurred manipulating the source
681     */
682    private Map<String, Object> _moveOrRenameSource(FileSource sourceFile, FileSource targetFile, String sourceContext, String targetContext) throws IOException
683    {
684        Map<String, Object> result = new HashMap<>();
685            
686        // Check if the user try to move files outside the root folder
687        if (!StringUtils.startsWith(sourceFile.getURI(), _root.getURI()) || !StringUtils.startsWith(targetFile.getURI(), _root.getURI()))
688        {
689            result.put("success", false);
690            result.put("error", "no-exists");
691            
692            getLogger().error("User '{}' tried to  move parameter file outside of the root extraction directory.", _currentUserProvider.getUser());
693            
694            return result;
695        }
696        
697        if (!sourceFile.exists())
698        {
699            result.put("success", false);
700            result.put("error", "no-exists");
701            return result;
702        }
703        
704        if (targetFile.exists())
705        {
706            // If both files are equals, there is no need to rename or move it
707            if (sourceFile.getFile().equals(targetFile.getFile()))
708            {
709                result.put("success", true);
710                result.put("name", targetFile.getName());
711                result.put("uri", targetFile.getURI());
712                return result;
713            }
714            else
715            {
716                result.put("success", false);
717                result.put("error", "already-exists");
718                return result;
719            }
720        }
721
722        copyRightsRecursively(sourceContext, targetContext, sourceFile);
723        if (sourceFile.getFile().isFile())
724        {
725            FileUtils.moveFile(sourceFile.getFile(), targetFile.getFile());
726        }
727        else
728        {
729            FileUtils.moveDirectory(sourceFile.getFile(), targetFile.getFile());
730        }
731        deleteRightsRecursively(sourceContext, targetFile);
732
733        result.put("success", true);
734        result.put("name", targetFile.getName());
735        result.put("uri", targetFile.getURI());
736
737        return result;
738    }
739    
740    /**
741     * Copy rights from one context to another one
742     * @param sourceContext the source context
743     * @param targetContext the target context
744     * @param file the source of the file to copy
745     */
746    public void copyRightsRecursively(String sourceContext, String targetContext, TraversableSource file)
747    {
748        copyRights(sourceContext, targetContext);
749        if (file.isCollection())
750        {
751            try
752            {
753                for (TraversableSource child : (Collection<TraversableSource>) file.getChildren())
754                {
755                    copyRightsRecursively(sourceContext + "/" + child.getName(), targetContext + "/" + child.getName(), child);
756                }
757            }
758            catch (SourceException e)
759            {
760                throw new RuntimeException("Cannot list child elements of " + file.getURI(), e);
761            }
762        }
763    }
764
765    /**
766     * Copy rights from one context to another one
767     * @param context the context
768     * @param file the source of the file to copy
769     */
770    public void deleteRightsRecursively(String context, TraversableSource file)
771    {
772        deleteRights(context);
773        if (file.isCollection())
774        {
775            try
776            {
777                for (TraversableSource child : (Collection<TraversableSource>) file.getChildren())
778                {
779                    deleteRightsRecursively(context + "/" + child.getName(), child);
780                }
781            }
782            catch (SourceException e)
783            {
784                throw new RuntimeException("Cannot list child elements of " + file.getURI(), e);
785            }
786        }
787    }
788    
789    /**
790     * Get the author of extraction
791     * @param extractionPath the path of the extraction
792     * @return the author
793     */
794    public UserIdentity getAuthor(FileSource extractionPath)
795    {
796        return _getExtractionAuthorCache().get(extractionPath, path -> _getUserIdentityByExtractionFile(path));
797        
798    }
799    
800    private UserIdentity _getUserIdentityByExtractionFile(FileSource extractionPath)
801    {
802        try
803        {
804            Extraction extraction = _definitionReader.readExtractionDefinitionFile(extractionPath.getFile());
805            return extraction.getAuthor();
806        }
807        catch (Exception e)
808        {
809            throw new RuntimeException("Cannot read extraction " + extractionPath, e);
810        }
811    }
812    
813    private Cache<FileSource, UserIdentity> _getExtractionAuthorCache()
814    {
815        return this._cacheManager.get(EXTRACTION_AUTHOR_CACHE);
816    }
817    
818    /**
819     * Remove the last separator from the uri if it has any
820     * @param uri the uri
821     * @return the uri without any ending separator
822     */
823    public static String trimLastFileSeparator(String uri)
824    {
825        return StringUtils.endsWith(uri, "/") ? StringUtils.substringBeforeLast(uri, "/") : uri;
826    }
827    
828    /**
829     * Get the path of all children that match the provided value.
830     * @param path the path to the extraction to consider as root
831     * @param value the value
832     * @return the list of path
833     */
834    @Callable(rights = {"Runtime_Rights_Rights_Handle", "Extraction_Rights_ExecuteExtraction"}) // assignment for the tree in assignment tool
835    public List<String> getFilteredPath(String path, String value)
836    {
837        try
838        {
839            TraversableSource currentSrc = (TraversableSource) _sourceResolver.resolveURI(ExtractionConstants.DEFINITIONS_DIR + (path.length() > 0 ? "/" + path : ""));
840            
841            List<String> result = _fileHelper.filterSources(currentSrc, value);
842            return result.stream()
843                  .map(this::_toRelativePath)
844                  .toList();
845        }
846        catch (IOException e)
847        {
848            getLogger().error("Failed to filter extraction definition at path '" + path + "'", e);
849            return List.of();
850        }
851    }
852
853    private String _toRelativePath(String absoluteURI)
854    {
855        // the root URI has a trailing slash or not depending on the existence of the folder at start time
856        // always remove the trailing slash so that we have a consistent behavior
857        return StringUtils.substringAfter(trimLastFileSeparator(absoluteURI), trimLastFileSeparator(_root.getURI()));
858    }
859}