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