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.plugins.core.impl.group.directory.jdbc;
017
018import java.sql.Connection;
019import java.sql.PreparedStatement;
020import java.sql.ResultSet;
021import java.sql.SQLException;
022import java.sql.Statement;
023import java.util.ArrayList;
024import java.util.Arrays;
025import java.util.Collection;
026import java.util.Collections;
027import java.util.HashMap;
028import java.util.HashSet;
029import java.util.Iterator;
030import java.util.LinkedHashSet;
031import java.util.List;
032import java.util.Map;
033import java.util.Set;
034
035import org.apache.avalon.framework.activity.Disposable;
036import org.apache.avalon.framework.service.ServiceException;
037import org.apache.avalon.framework.service.ServiceManager;
038import org.apache.avalon.framework.service.Serviceable;
039import org.apache.commons.lang3.ArrayUtils;
040import org.apache.commons.lang3.StringUtils;
041import org.apache.excalibur.source.SourceResolver;
042
043import org.ametys.core.ObservationConstants;
044import org.ametys.core.cache.AbstractCacheManager;
045import org.ametys.core.cache.Cache;
046import org.ametys.core.datasource.ConnectionHelper;
047import org.ametys.core.group.Group;
048import org.ametys.core.group.GroupIdentity;
049import org.ametys.core.group.InvalidModificationException;
050import org.ametys.core.group.ModifiableGroup;
051import org.ametys.core.group.directory.GroupDirectory;
052import org.ametys.core.group.directory.GroupDirectoryModel;
053import org.ametys.core.group.directory.ModifiableGroupDirectory;
054import org.ametys.core.observation.Event;
055import org.ametys.core.observation.ObservationManager;
056import org.ametys.core.script.SQLScriptHelper;
057import org.ametys.core.user.CurrentUserProvider;
058import org.ametys.core.user.UserIdentity;
059import org.ametys.core.util.Cacheable;
060import org.ametys.runtime.i18n.I18nizableText;
061import org.ametys.runtime.plugin.component.AbstractLogEnabled;
062
063/**
064 * Standard implementation of {@link GroupDirectory} from the core database.
065 */
066public class JdbcGroupDirectory extends AbstractLogEnabled implements ModifiableGroupDirectory, Serviceable, Cacheable, Disposable
067{
068    /** Name of the parameter holding the datasource id */
069    private static final String __DATASOURCE_PARAM_NAME = "runtime.groups.jdbc.datasource";
070    /** Name of the parameter holding the SQL table name for storing the groups */
071    private static final String __GROUPS_LIST_TABLE_PARAM_NAME = "runtime.groups.jdbc.list.table";
072    /** Name of the parameter holding the SQL table name for storing the composition (i.e. users) of the groups */
073    private static final String __GROUPS_COMPOSITION_TABLE_PARAM_NAME = "runtime.groups.jdbc.composition.table";
074    
075    private static final String __GROUPS_LIST_COLUMN_ID = "Id";
076    private static final String __GROUPS_LIST_COLUMN_LABEL = "Label";
077    private static final String __GROUPS_COMPOSITION_COLUMN_GROUPID = "Group_Id";
078    private static final String __GROUPS_COMPOSITION_COLUMN_LOGIN = "Login";
079    private static final String __GROUPS_COMPOSITION_COLUMN_POPULATIONID = "UserPopulation_Id";
080    
081    private static final String __JDBC_GROUPDIRECTORY_GROUPS_BY_USER_CACHE_NAME_PREFIX = JdbcGroupDirectory.class.getName() + "$groups.by.user$";
082    
083    /** The observation manager */
084    protected ObservationManager _observationManager;
085    /** The current user provider */
086    protected CurrentUserProvider _currentUserProvider;
087    /** The cocoon source resolver */
088    protected SourceResolver _sourceResolver;
089    /** The cache manager */
090    protected AbstractCacheManager _cacheManager;
091    
092    /** The identifier of data source */
093    protected String _dataSourceId;
094    /** The name of the SQL table storing the groups */
095    protected String _groupsListTableName;
096    /** The name of the SQL table storing the composition (i.e. users) of the groups*/
097    protected String _groupsCompositionTableName;
098    
099    /** The id */
100    protected String _id;
101    /** The label */
102    protected I18nizableText _label;
103    /** The id of the {@link GroupDirectoryModel} */
104    private String _groupDirectoryModelId;
105    /** The map of the values of the parameters */
106    private Map<String, Object> _paramValues;
107    private boolean _lazyInitialized;
108    
109    // Cannot use _id as two GroupDirectories with same id can co-exist during a short amount of time (during GroupDirectoryDAO#_read)
110    private final String _uniqueCacheSuffix = org.ametys.core.util.StringUtils.generateKey();
111    
112    @Override
113    public void service(ServiceManager manager) throws ServiceException
114    {
115        _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE);
116        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
117        _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
118        _cacheManager = (AbstractCacheManager) manager.lookup(AbstractCacheManager.ROLE);
119    }
120    
121    @Override
122    public String getId()
123    {
124        return _id;
125    }
126    
127    @Override
128    public I18nizableText getLabel()
129    {
130        return _label;
131    }
132    
133    @Override
134    public void setId(String id)
135    {
136        _id = id;
137    }
138    
139    @Override
140    public void setLabel(I18nizableText label)
141    {
142        _label = label;
143    }
144    
145    @Override
146    public String getGroupDirectoryModelId ()
147    {
148        return _groupDirectoryModelId;
149    }
150    
151    @Override
152    public Map<String, Object> getParameterValues()
153    {
154        return _paramValues;
155    }
156    
157    @Override
158    public void dispose()
159    {
160        removeCaches();
161    }
162    
163    @Override
164    public Collection<SingleCacheConfiguration> getManagedCaches()
165    {
166        return Arrays.asList(
167                SingleCacheConfiguration.of(
168                        __JDBC_GROUPDIRECTORY_GROUPS_BY_USER_CACHE_NAME_PREFIX + _uniqueCacheSuffix, 
169                        _buildI18n("PLUGINS_CORE_GROUPS_JDBC_CACHE_GROUPS_BY_USER_LABEL"), 
170                        _buildI18n("PLUGINS_CORE_GROUPS_JDBC_CACHE_GROUPS_BY_USER_DESC"))
171        );
172    }
173    
174    @Override
175    public boolean hasComputableSize()
176    {
177        return true;
178    }
179    
180    private I18nizableText _buildI18n(String i18Key)
181    {
182        String catalogue = "plugin.core-impl";
183        I18nizableText groupDirectoryId = new I18nizableText(getId());
184        Map<String, I18nizableText> params = Map.of(
185                "id", groupDirectoryId);
186        return new I18nizableText(catalogue, i18Key, params);
187    }
188    
189    private Cache<UserIdentity, Set<String>> _getCacheGroupsByUser()
190    {
191        return getCacheManager().get(__JDBC_GROUPDIRECTORY_GROUPS_BY_USER_CACHE_NAME_PREFIX + _uniqueCacheSuffix);
192    }
193    
194    @Override
195    public AbstractCacheManager getCacheManager()
196    {
197        return _cacheManager;
198    }
199    
200    @Override
201    public void init(String groupDirectoryModelId, Map<String, Object> paramValues)
202    {
203        _groupDirectoryModelId = groupDirectoryModelId;
204        _paramValues = paramValues;
205        
206        _groupsListTableName = (String) paramValues.get(__GROUPS_LIST_TABLE_PARAM_NAME);
207        _groupsCompositionTableName = (String) paramValues.get(__GROUPS_COMPOSITION_TABLE_PARAM_NAME);
208        _dataSourceId = (String) paramValues.get(__DATASOURCE_PARAM_NAME);
209        
210        createCaches();
211    }
212    
213    /**
214     * Get the connection to the database 
215     * @return the SQL connection
216     */
217    @SuppressWarnings("unchecked")
218    protected Connection getSQLConnection()
219    {
220        Connection connection = ConnectionHelper.getConnection(_dataSourceId);
221        
222        if (!_lazyInitialized)
223        {
224            try
225            {
226                SQLScriptHelper.createTableIfNotExists(connection, _groupsListTableName, "plugin:core://scripts/%s/jdbc_groups.template.sql", _sourceResolver, 
227                        (Map) ArrayUtils.toMap(new String[][] {{"%TABLENAME%", _groupsListTableName}, {"%TABLENAME_COMPOSITION%", _groupsCompositionTableName}}));
228            }
229            catch (Exception e)
230            {
231                getLogger().error("The tables requires by the " + this.getClass().getName() + " could not be created. A degraded behavior will occur", e);
232            }
233            
234            _lazyInitialized = true;
235        }
236        
237        return connection;
238    }
239    
240    @Override
241    public ModifiableGroup getGroup(String groupID)
242    {
243        JdbcGroup group = null;
244
245        Connection connection = null;
246        PreparedStatement stmt = null;
247        ResultSet rs = null;
248
249        try
250        {
251            connection = getSQLConnection();
252
253            String sql = "SELECT " + __GROUPS_LIST_COLUMN_LABEL + " FROM " + _groupsListTableName + " WHERE " + __GROUPS_LIST_COLUMN_ID + " =  ?";
254            stmt = connection.prepareStatement(sql);
255            stmt.setInt(1, Integer.parseInt(groupID));
256
257            if (getLogger().isDebugEnabled())
258            {
259                getLogger().debug(sql);
260            }
261            rs = stmt.executeQuery();
262
263            // Iterate over all the groups
264            if (rs.next())
265            {
266                String label = rs.getString(__GROUPS_LIST_COLUMN_LABEL);
267                group = new JdbcGroup(new GroupIdentity(groupID, getId()), label, this);
268
269                _fillGroup(group, connection);
270            }
271        }
272        catch (NumberFormatException e)
273        {
274            getLogger().error("Group ID must be an integer.", e);
275            return null;
276        }
277        catch (SQLException e)
278        {
279            getLogger().error("Error communication with database", e);
280            return null;
281        }
282        finally
283        {
284            ConnectionHelper.cleanup(rs);       
285            ConnectionHelper.cleanup(stmt);       
286            ConnectionHelper.cleanup(connection);       
287        }
288
289        // Return the found group or null
290        return group;
291    }
292
293    @Override
294    public Set<Group> getGroups()
295    {
296        Set<Group> groups = new LinkedHashSet<>();
297
298        Connection connection = null;
299        Statement stmt = null;
300        ResultSet rs = null;
301        
302        try
303        {
304            connection = getSQLConnection();
305
306            stmt = connection.createStatement();
307            String sql = _createGetGroupsClause();
308
309            if (getLogger().isDebugEnabled())
310            {
311                getLogger().debug(sql);
312            }
313            rs = stmt.executeQuery(sql);
314
315            // Iterate over all the groups
316            while (rs.next())
317            {
318                String groupID = rs.getString(__GROUPS_LIST_COLUMN_ID);
319                String label = rs.getString(__GROUPS_LIST_COLUMN_LABEL);
320                JdbcGroup group = new JdbcGroup(new GroupIdentity(groupID, getId()), label, this);
321
322                _fillGroup(group, connection);
323
324                // Add current group
325                groups.add(group);
326            }
327        }
328        catch (SQLException e)
329        {
330            getLogger().error("Error communication with database", e);
331            return Collections.emptySet();
332        }
333        finally
334        {
335            ConnectionHelper.cleanup(rs);       
336            ConnectionHelper.cleanup(stmt);       
337            ConnectionHelper.cleanup(connection);       
338        }
339
340        return groups;
341    }
342    
343    /**
344     * Get the sql clause that gets all groups
345     * @return A non null sql clause (e.g. "select ... from ... where ...")
346     */
347    protected String _createGetGroupsClause()
348    {
349        return "SELECT " + __GROUPS_LIST_COLUMN_ID + ", " + __GROUPS_LIST_COLUMN_LABEL + " FROM " + _groupsListTableName + " ORDER BY " + __GROUPS_LIST_COLUMN_LABEL;
350    }
351    
352    /**
353     * Fill users set in a group.
354     * 
355     * @param group The group to fill.
356     * @param connection The SQL connection.
357     * @throws SQLException If a problem occurs.
358     */
359    protected void _fillGroup(JdbcGroup group, Connection connection) throws SQLException
360    {
361        PreparedStatement stmt = null;
362        ResultSet rs = null;
363
364        try
365        {
366            String sql = "SELECT " + __GROUPS_COMPOSITION_COLUMN_LOGIN + ", " + __GROUPS_COMPOSITION_COLUMN_POPULATIONID + " FROM " + _groupsCompositionTableName + " WHERE " + __GROUPS_COMPOSITION_COLUMN_GROUPID + " = ?";
367
368            stmt = connection.prepareStatement(sql);
369
370            stmt.setInt(1, Integer.parseInt(group.getIdentity().getId()));
371
372            if (getLogger().isDebugEnabled())
373            {
374                getLogger().debug(sql);
375            }
376            rs = stmt.executeQuery();
377
378            // Iterate over all the users from current group 
379            while (rs.next())
380            {
381                UserIdentity identity = new UserIdentity(rs.getString(__GROUPS_COMPOSITION_COLUMN_LOGIN), rs.getString(__GROUPS_COMPOSITION_COLUMN_POPULATIONID));
382                group.addUser(identity);
383            }
384        }
385        finally
386        {
387            ConnectionHelper.cleanup(rs);
388            ConnectionHelper.cleanup(stmt);
389        }
390    }
391
392    @Override
393    public Set<String> getUserGroups(UserIdentity userIdentity)
394    {
395        if (isCachingEnabled() && _getCacheGroupsByUser().hasKey(userIdentity))
396        {
397            Set<String> userGroups = _getCacheGroupsByUser().get(userIdentity);
398            // Cache hit, return the results. 
399            return userGroups;
400        }
401        
402        Set<String> userGroups = _executeSqlForUserGroups(userIdentity);
403        
404        // Cache the results.
405        if (isCachingEnabled())
406        {
407            _getCacheGroupsByUser().put(userIdentity, userGroups);
408        }
409        
410        return userGroups;
411    }
412    
413    private Set<String> _executeSqlForUserGroups(UserIdentity userIdentity)
414    {
415        String login = userIdentity.getLogin();
416        String populationId = userIdentity.getPopulationId();
417        
418        Set<String> groups = new HashSet<>();
419        if (login == null)
420        {
421            return groups;
422        }
423        
424        Connection connection = null;
425        PreparedStatement stmt = null;
426        ResultSet rs = null;
427
428        try
429        {
430            connection = getSQLConnection();
431
432            String sql = "SELECT " + __GROUPS_COMPOSITION_COLUMN_GROUPID + " FROM " + _groupsCompositionTableName + " WHERE " + __GROUPS_COMPOSITION_COLUMN_LOGIN + " = ? AND " + __GROUPS_COMPOSITION_COLUMN_POPULATIONID + " = ?";
433            stmt = connection.prepareStatement(sql);
434            stmt.setString(1, login);
435            stmt.setString(2, populationId);
436
437            if (getLogger().isDebugEnabled())
438            {
439                getLogger().debug(sql);
440            }
441            rs = stmt.executeQuery();
442
443            // Iterate over all the groups
444            while (rs.next())
445            {
446                String groupID = rs.getString(__GROUPS_COMPOSITION_COLUMN_GROUPID);
447
448                // Add the current group
449                groups.add(groupID);
450            }
451        }
452        catch (SQLException e)
453        {
454            getLogger().error("Error communication with database", e);
455            return Collections.emptySet();
456        }
457        finally
458        {
459            ConnectionHelper.cleanup(rs);       
460            ConnectionHelper.cleanup(stmt);       
461            ConnectionHelper.cleanup(connection);       
462        }
463
464        // Return the groups, potentially empty
465        return groups;
466    }
467
468    @Override
469    public List<Group> getGroups(int count, int offset, Map parameters)
470    {
471        List<Group> groups = new ArrayList<>();
472        
473        String pattern = (String) parameters.get("pattern");
474
475        Iterator iterator = getGroups().iterator();
476
477        //int totalCount = 0;
478        int currentOffset = offset;
479
480        while (currentOffset > 0 && iterator.hasNext())
481        {
482            Group group = (Group) iterator.next();
483            if (StringUtils.isEmpty(pattern) || group.getLabel().toLowerCase().indexOf(pattern.toLowerCase()) != -1 || (group.getIdentity() != null && group.getIdentity().getId().toLowerCase().indexOf(pattern.toLowerCase()) != -1))
484            {
485                currentOffset--;
486                //totalCount++;
487            }
488        }
489
490        int currentCount = count;
491        while ((count == -1 || currentCount > 0) && iterator.hasNext())
492        {
493            Group group = (Group) iterator.next();
494
495            if (StringUtils.isEmpty(pattern) || group.getLabel().toLowerCase().indexOf(pattern.toLowerCase()) != -1 || (group.getIdentity() != null && group.getIdentity().getId().toLowerCase().indexOf(pattern.toLowerCase()) != -1))
496            {
497                groups.add(group);
498                currentCount--;
499                //totalCount++;
500            }
501        }
502
503        /*while (iterator.hasNext())
504        {
505            Group group = (Group) iterator.next();
506            
507            if (StringUtils.isEmpty(pattern) || group.getLabel().toLowerCase().indexOf(pattern.toLowerCase()) != -1)
508            {
509                totalCount++;
510            }
511        }*/
512        
513        // TODO return totalCount
514        return groups;
515    }
516
517    @Override
518    public ModifiableGroup add(String name) throws InvalidModificationException
519    {
520        Connection connection = null; 
521        PreparedStatement statement = null;
522        ResultSet rs = null;
523
524        String id = null;
525
526        try
527        {
528            connection = getSQLConnection();
529            String dbType = ConnectionHelper.getDatabaseType(connection);
530            
531            if (ConnectionHelper.DATABASE_ORACLE.equals(dbType))
532            {
533                statement = connection.prepareStatement("SELECT seq_" + _groupsListTableName + ".nextval FROM dual");
534                rs = statement.executeQuery();
535                if (rs.next())
536                {
537                    id = rs.getString(1);
538                }
539                ConnectionHelper.cleanup(rs);
540                ConnectionHelper.cleanup(statement);
541
542                statement = connection.prepareStatement("INSERT INTO " + _groupsListTableName + " (Id, Label) VALUES(?, ?)");
543                statement.setString(1, id);
544                statement.setString(2, name);
545            }
546            else
547            {
548                statement = connection.prepareStatement("INSERT INTO " + _groupsListTableName + " (" + __GROUPS_LIST_COLUMN_LABEL + ") VALUES (?)");
549                statement.setString(1, name);
550            }
551
552            statement.executeUpdate();
553
554            ConnectionHelper.cleanup(statement);
555
556            //FIXME Write query working with all database
557            if (ConnectionHelper.DATABASE_MYSQL.equals(dbType))
558            {
559                statement = connection.prepareStatement("SELECT " + __GROUPS_LIST_COLUMN_ID + " FROM " + _groupsListTableName + " WHERE " + __GROUPS_LIST_COLUMN_ID + " = last_insert_id()");    
560                rs = statement.executeQuery();
561                if (rs.next())
562                {
563                    id = rs.getString(__GROUPS_LIST_COLUMN_ID);
564                }
565                else
566                {
567                    if (connection.getAutoCommit())
568                    {
569                        throw new InvalidModificationException("Cannot retrieve inserted group. Group was created but listeners not called : base may be inconsistant");
570                    }
571                    else
572                    {
573                        connection.rollback();
574                        throw new InvalidModificationException("Cannot retrieve inserted group. Rolling back");
575                    }
576                }
577            }
578            else if (ConnectionHelper.DATABASE_DERBY.equals(dbType))
579            {
580                statement = connection.prepareStatement("VALUES IDENTITY_VAL_LOCAL ()");
581                rs = statement.executeQuery();
582                if (rs.next())
583                {
584                    id = rs.getString(1);
585                }
586            }
587            else if (ConnectionHelper.DATABASE_HSQLDB.equals(dbType))
588            {
589                statement = connection.prepareStatement("CALL IDENTITY ()");
590                rs = statement.executeQuery();
591                if (rs.next())
592                {
593                    id = rs.getString(1);
594                }
595            }
596            else if (ConnectionHelper.DATABASE_POSTGRES.equals(dbType))
597            {
598                statement = connection.prepareStatement("SELECT currval('groups_id_seq')");
599                rs = statement.executeQuery();
600                if (rs.next())
601                {
602                    id = rs.getString(1);
603                }
604            }
605
606            if (id != null)
607            {
608                Map<String, Object> eventParams = new HashMap<>();
609                eventParams.put(ObservationConstants.ARGS_GROUP, new GroupIdentity(id, getId()));
610                _observationManager.notify(new Event(ObservationConstants.EVENT_GROUP_ADDED, _currentUserProvider.getUser(), eventParams));
611            }
612        }
613        catch (SQLException ex)
614        {
615            throw new RuntimeException(ex);
616        }
617        finally
618        {
619            ConnectionHelper.cleanup(rs);       
620            ConnectionHelper.cleanup(statement);       
621            ConnectionHelper.cleanup(connection);       
622        }
623
624        return new JdbcGroup(new GroupIdentity(id, getId()), name, this);
625    }
626
627    @Override
628    public void update(ModifiableGroup userGroup) throws InvalidModificationException
629    {
630        Connection connection = null;
631        PreparedStatement statement = null;
632
633        if (getLogger().isDebugEnabled())
634        {
635            getLogger().debug("Updating group " + GroupIdentity.groupIdentityToString(userGroup.getIdentity()) + " with " + userGroup.getUsers().size() + " user(s)");
636        }
637        
638        try
639        {
640            
641            connection = getSQLConnection();
642
643            // Start transaction.
644            connection.setAutoCommit(false);
645
646            statement = connection.prepareStatement("UPDATE " + _groupsListTableName + " SET " + __GROUPS_LIST_COLUMN_LABEL + "=? WHERE " + __GROUPS_LIST_COLUMN_ID + " = ?");
647            statement.setString(1, userGroup.getLabel());
648            statement.setInt(2, Integer.parseInt(userGroup.getIdentity().getId()));
649
650            if (statement.executeUpdate() == 0)
651            {
652                throw new InvalidModificationException("No group with id '" + userGroup.getIdentity().getId() + "' may be removed");
653            }
654            ConnectionHelper.cleanup(statement);
655
656            statement = connection.prepareStatement("DELETE FROM " + _groupsCompositionTableName + " WHERE " + __GROUPS_COMPOSITION_COLUMN_GROUPID + " = ?");
657            statement.setInt(1, Integer.parseInt(userGroup.getIdentity().getId()));
658
659            statement.executeUpdate();
660            ConnectionHelper.cleanup(statement);
661
662            if (!userGroup.getUsers().isEmpty())
663            {
664                // Tests if the connection supports batch updates.
665                boolean supportsBatch = connection.getMetaData().supportsBatchUpdates();
666
667                statement = connection.prepareStatement("INSERT INTO " + _groupsCompositionTableName + " (" + __GROUPS_COMPOSITION_COLUMN_GROUPID + ", " + __GROUPS_COMPOSITION_COLUMN_LOGIN + ", " + __GROUPS_COMPOSITION_COLUMN_POPULATIONID + ") VALUES (?, ?, ?)");
668                
669                for (UserIdentity identity : userGroup.getUsers())
670                {
671                    String login = identity.getLogin();
672                    String populationId = identity.getPopulationId();
673                    statement.setInt(1, Integer.parseInt(userGroup.getIdentity().getId()));
674                    statement.setString(2, login);
675                    statement.setString(3, populationId);
676                    
677                    // If batch updates are supported, add to the batch, else execute directly.
678                    if (supportsBatch)
679                    {
680                        statement.addBatch();
681                    }
682                    else
683                    {
684                        statement.executeUpdate();
685                    }
686                }
687                
688                // If the insert queries were queued in a batch, execute it.
689                if (supportsBatch)
690                {
691                    statement.executeBatch();
692                }
693            }
694
695            ConnectionHelper.cleanup(statement);
696
697            // Commit transaction.
698            connection.commit();
699            
700            // Clear global cache
701            _getCacheGroupsByUser().invalidateAll();
702
703            Map<String, Object> eventParams = new HashMap<>();
704            eventParams.put(ObservationConstants.ARGS_GROUP, userGroup.getIdentity());
705            _observationManager.notify(new Event(ObservationConstants.EVENT_GROUP_UPDATED, _currentUserProvider.getUser(), eventParams));
706        }
707        catch (NumberFormatException ex)
708        {
709            throw new InvalidModificationException("No group with id '" + userGroup.getIdentity().getId() + "' may be removed", ex);
710        }
711        catch (SQLException ex)
712        {
713            throw new RuntimeException(ex);
714        }
715        finally
716        {
717            ConnectionHelper.cleanup(statement);
718            ConnectionHelper.cleanup(connection);
719        }
720        
721        if (getLogger().isDebugEnabled())
722        {
723            getLogger().debug("Updated group " + GroupIdentity.groupIdentityToString(userGroup.getIdentity()) + " with " + userGroup.getUsers().size() + " user(s)");
724        }
725    }
726
727    @Override
728    public void remove(String groupID) throws InvalidModificationException
729    {
730        Connection connection = null;
731        PreparedStatement statement = null;
732
733        try
734        {
735            connection = getSQLConnection();
736
737            statement = connection.prepareStatement("DELETE FROM " + _groupsListTableName + " WHERE " + __GROUPS_LIST_COLUMN_ID + " = ?");
738            statement.setInt(1, Integer.parseInt(groupID));
739
740            if (statement.executeUpdate() == 0)
741            {
742                throw new InvalidModificationException("No group with id '" + groupID + "' may be removed");
743            }
744            ConnectionHelper.cleanup(statement);       
745
746            statement = connection.prepareStatement("DELETE FROM " + _groupsCompositionTableName + " WHERE " + __GROUPS_COMPOSITION_COLUMN_GROUPID + " = ?");
747            statement.setInt(1, Integer.parseInt(groupID));
748
749            statement.executeUpdate();
750            
751            // Clear global cache
752            _getCacheGroupsByUser().invalidateAll();
753
754            Map<String, Object> eventParams = new HashMap<>();
755            eventParams.put(ObservationConstants.ARGS_GROUP, new GroupIdentity(groupID, getId()));
756            _observationManager.notify(new Event(ObservationConstants.EVENT_GROUP_DELETED, _currentUserProvider.getUser(), eventParams));
757        }
758        catch (NumberFormatException ex)
759        {
760            throw new InvalidModificationException("No group with id '" + groupID + "' may be removed, the ID must be a number.", ex);
761        }
762        catch (SQLException ex)
763        {
764            throw new RuntimeException(ex);
765        }
766        finally
767        {
768            ConnectionHelper.cleanup(statement);       
769            ConnectionHelper.cleanup(connection);       
770        }
771    }
772    
773    private static final class JdbcGroup implements ModifiableGroup
774    {
775        private Set<UserIdentity> _users;
776        private GroupIdentity _identity;
777        private String _groupLabel;
778        private GroupDirectory _groupDirectory;
779        
780        JdbcGroup(GroupIdentity identity, String label, GroupDirectory groupDirectory)
781        {
782            _identity = identity;
783            _groupLabel = label;
784            _groupDirectory = groupDirectory;
785            _users = new HashSet<>();
786        }
787        
788        @Override
789        public GroupIdentity getIdentity()
790        {
791            return _identity;
792        }
793
794        @Override
795        public String getLabel()
796        {
797            return _groupLabel;
798        }
799
800        @Override
801        public GroupDirectory getGroupDirectory()
802        {
803            return _groupDirectory;
804        }
805        
806        @Override
807        public void setLabel(String label)
808        {
809            _groupLabel = label;
810        }
811
812        @Override
813        public void addUser(UserIdentity user)
814        {
815            _users.add(user);
816        }
817
818        @Override
819        public void removeUser(UserIdentity user)
820        {
821            _users.remove(user);
822        }
823        
824        @Override
825        public void removeUsers()
826        {
827            _users.clear();
828        }
829        
830        @Override
831        public Set<UserIdentity> getUsers()
832        {
833            return _users;
834        }
835        
836        @Override
837        public String toString()
838        {
839            StringBuffer sb = new StringBuffer("UserGroup[");
840            sb.append(_identity);
841            sb.append(" (");
842            sb.append(_groupLabel);
843            sb.append(") => ");
844            sb.append(_users.toString());
845            sb.append("]");
846            return sb.toString();
847        }    
848        
849        @Override
850        public boolean equals(Object another)
851        {
852            if (another == null || !(another instanceof JdbcGroup))
853            {
854                return false;
855            }
856            
857            JdbcGroup otherGroup = (JdbcGroup) another;
858            
859            return _identity != null && _identity.equals(otherGroup.getIdentity());
860        }
861        
862        @Override
863        public int hashCode()
864        {
865            return _identity.hashCode();
866        }
867    }
868}