001/*
002 *  Copyright 2016 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.core.group;
017
018import java.sql.Connection;
019import java.sql.PreparedStatement;
020import java.sql.ResultSet;
021import java.sql.SQLException;
022import java.util.HashMap;
023import java.util.HashSet;
024import java.util.List;
025import java.util.Map;
026import java.util.Set;
027import java.util.stream.Collectors;
028
029import org.apache.avalon.framework.activity.Initializable;
030import org.apache.avalon.framework.component.Component;
031import org.apache.avalon.framework.service.ServiceException;
032import org.apache.avalon.framework.service.ServiceManager;
033import org.apache.avalon.framework.service.Serviceable;
034
035import org.ametys.core.cache.AbstractCacheManager;
036import org.ametys.core.cache.Cache;
037import org.ametys.core.datasource.ConnectionHelper;
038import org.ametys.core.group.directory.GroupDirectory;
039import org.ametys.core.ui.Callable;
040import org.ametys.runtime.config.Config;
041import org.ametys.runtime.i18n.I18nizableText;
042import org.ametys.runtime.plugin.component.AbstractLogEnabled;
043
044/**
045 * Helper for associating {@link GroupDirectory}(ies) to contexts.
046 */
047public class GroupDirectoryContextHelper extends AbstractLogEnabled implements Component, Serviceable, Initializable
048{
049    /** Avalon Role */
050    public static final String ROLE = GroupDirectoryContextHelper.class.getName();
051    
052    /** The "admin" context */
053    public static final String ADMIN_CONTEXT = "/admin";
054    
055    /** The name of the JDBC table for group directories by context */
056    private static final String __GROUP_DIRECTORIES_TABLE = "GroupDirectoriesByContext";
057
058    private static final String __GROUP_DIRECTORIES_BY_CONTEXT_CACHE = "groupDirectoriesByContext";
059    
060    private static final String IS_CACHE_FILLED = "###iscachefilled###";
061
062    
063    /** The DAO for {@link GroupDirectory}(ies) */
064    private GroupDirectoryDAO _groupDirectoryDAO;
065
066    private AbstractCacheManager _abstractCacheManager;
067
068    
069    @Override
070    public void service(ServiceManager manager) throws ServiceException
071    {
072        _groupDirectoryDAO = (GroupDirectoryDAO) manager.lookup(GroupDirectoryDAO.ROLE);
073        _abstractCacheManager = (AbstractCacheManager) manager.lookup(AbstractCacheManager.ROLE);
074    }
075    
076    @Override
077    public void initialize() throws Exception
078    {
079        _abstractCacheManager.createMemoryCache(__GROUP_DIRECTORIES_BY_CONTEXT_CACHE, 
080                new I18nizableText("plugin.core", "PLUGINS_CORE_CACHE_GROUP_DIRECTORY_CONTEXT_LABEL"),
081                new I18nizableText("plugin.core", "PLUGINS_CORE_CACHE_GROUP_DIRECTORY_CONTEXT_DESCRIPTION"),
082                true,
083                null);
084    }
085    
086    /**
087     * Get the connection to the database 
088     * @return the SQL connection
089     */
090    protected Connection getSQLConnection ()
091    {
092        String datasourceId = Config.getInstance().getValue("runtime.assignments.groups");
093        return ConnectionHelper.getConnection(datasourceId);
094    }
095    
096    /**
097     * Links given group directories to a context.
098     * @param context The context
099     * @param ids The ids of the group directories to link
100     * @return The ids of the linked group directories
101     */
102    @SuppressWarnings("resource")
103    @Callable
104    public Set<String> link(String context, List<String> ids)
105    {
106        Set<String> result = getGroupDirectoriesOnContext(context);
107        
108        Connection connection = null;
109        PreparedStatement stmt = null;
110        try
111        {
112            connection = getSQLConnection();
113            
114            // Remove all the ids affected to this context
115            String sql = "DELETE FROM " + __GROUP_DIRECTORIES_TABLE + " WHERE Context=?";
116            stmt = connection.prepareStatement(sql);
117            stmt.setString(1, context);
118            stmt.executeUpdate();
119            getLogger().info("{}\n[{}]", sql, context);
120            
121            // Set the new ids to this context
122            sql = "INSERT INTO " + __GROUP_DIRECTORIES_TABLE + " (Context, GroupDirectory_Id) VALUES(?, ?)";
123            stmt = connection.prepareStatement(sql);
124            for (String id : ids)
125            {
126                if (_groupDirectoryDAO.getGroupDirectory(id) != null)
127                {
128                    stmt.setString(1, context);
129                    stmt.setString(2, id);
130                    stmt.executeUpdate();
131                    
132                    getLogger().info("{}\n[{}, {}]", sql, context, id);
133                    if (result.contains(id))
134                    {
135                        result.remove(id); // the group directory was already linked, so its status didn't changed
136                    }
137                    else
138                    {
139                        result.add(id);
140                    }
141                }
142                else
143                {
144                    getLogger().warn("The GroupDirectory with id '{}' does not exist. It will not be linked.", id);
145                }
146            }            
147        }
148        catch (SQLException e)
149        {
150            getLogger().error("Error in sql query", e);
151        }
152        finally
153        {
154            ConnectionHelper.cleanup(connection);
155            ConnectionHelper.cleanup(stmt);
156            _getCache().invalidateAll();
157        }
158        
159        return result;
160    }
161    
162    /**
163     * Gets the group directories linked to the given context
164     * @param context The context
165     * @return The ids of group directories linked to the context
166     */
167    @Callable
168    public Set<String> getGroupDirectoriesOnContext(String context)
169    {
170        if (ADMIN_CONTEXT.equals(context))
171        {
172            // Return all the group directories
173            return _groupDirectoryDAO.getGroupDirectoriesIds();
174        }
175        else
176        {
177            return  _getDirectoriesOnContextFromDatabase(context);
178        }
179    }
180    
181    /**
182     * Gets the group directories linked to at least one of the given contexts
183     * @param contexts The contexts
184     * @return The ids of group directories linked to the contexts
185     */
186    @Callable
187    public Set<String> getGroupDirectoriesOnContexts(Set<String> contexts)
188    {
189        return contexts.stream()
190                .map(this::getGroupDirectoriesOnContext)
191                .flatMap(Set::stream)
192                .collect(Collectors.toSet());
193    }
194    
195    private Set<String> _getDirectoriesOnContextFromDatabase(String context)
196    {
197        return _getCacheAsMap().computeIfAbsent(context, str -> new HashSet<>());
198    }
199    
200    /**
201     * create a group directory map from the database
202     * @return Non null map of group directory content
203     */
204    private synchronized Map<String, Set<String>> _getAllGroupDirectoryContent()
205    {
206        Map<String, Set<String>> cache = new HashMap<>();
207        
208        Connection connection = null;
209        PreparedStatement stmt = null;
210        ResultSet rs = null;
211        try
212        {
213            connection = getSQLConnection();
214            
215            String sql = "SELECT Context, GroupDirectory_Id FROM " + __GROUP_DIRECTORIES_TABLE;
216            stmt = connection.prepareStatement(sql);
217            rs = stmt.executeQuery();
218            
219            while (rs.next())
220            {
221                String context = rs.getString(1);
222                String groupDirectoryId = rs.getString(2);
223                
224                if (_groupDirectoryDAO.getGroupDirectory(groupDirectoryId) == null)
225                {
226                    getLogger().warn("The group directory of id '{}' is linked to a context, but does not exist anymore.", groupDirectoryId);
227                }
228                else
229                {
230                    Set<String> cacheForContext = cache.computeIfAbsent(context, c -> new HashSet<>());
231                    
232                    cacheForContext.add(groupDirectoryId);
233                }
234            }
235        }
236        catch (SQLException e)
237        {
238            throw new IllegalStateException("Cannot get group directories and contexts association because of an error in a sql query", e);
239            
240        }
241        finally
242        {
243            ConnectionHelper.cleanup(connection);
244            ConnectionHelper.cleanup(stmt);
245            ConnectionHelper.cleanup(rs);
246        }
247        return cache;
248    }
249    
250    /**
251     * Get the cache
252     * @return Non null cache
253     */
254    protected synchronized Cache<String, Set<String>> _getCache()
255    {
256        Cache<String, Set<String>> cache = _abstractCacheManager.get(__GROUP_DIRECTORIES_BY_CONTEXT_CACHE); 
257        
258        if (!cache.hasKey(IS_CACHE_FILLED))
259        {
260            Map<String, Set<String>> cacheMap = _getAllGroupDirectoryContent();
261            cache.putAll(cacheMap);
262            cache.put(IS_CACHE_FILLED, null);
263        }
264        return cache;
265    }
266    
267    /**
268     * Get the cache as map
269     * @return Non null cache
270     */
271    protected synchronized Map<String, Set<String>> _getCacheAsMap()
272    {
273        Map<String, Set<String>> cacheAsMap = _getCache().asMap();
274        cacheAsMap.remove(IS_CACHE_FILLED);
275        return cacheAsMap;
276    }
277}