1   package com.atlassian.user.util.migration;
2   
3   import java.sql.Connection;
4   import java.sql.ResultSet;
5   import java.sql.SQLException;
6   import java.util.HashMap;
7   import java.util.Iterator;
8   import java.util.List;
9   import java.util.Map;
10  
11  import javax.sql.DataSource;
12  
13  import com.atlassian.user.security.password.Credential;
14  import net.sf.hibernate.HibernateException;
15  import net.sf.hibernate.Session;
16  import net.sf.hibernate.SessionFactory;
17  
18  import org.apache.log4j.Logger;
19  import org.springframework.jdbc.core.JdbcTemplate;
20  import org.springframework.jdbc.core.RowCallbackHandler;
21  import org.springframework.jdbc.datasource.SingleConnectionDataSource;
22  import org.springframework.orm.hibernate.SessionFactoryUtils;
23  
24  import com.atlassian.user.EntityException;
25  import com.atlassian.user.ExternalEntity;
26  import com.atlassian.user.Group;
27  import com.atlassian.user.GroupManager;
28  import com.atlassian.user.User;
29  import com.atlassian.user.UserManager;
30  import com.atlassian.user.configuration.DefaultDelegationAccessor;
31  import com.atlassian.user.configuration.DelegationAccessor;
32  import com.atlassian.user.configuration.RepositoryAccessor;
33  import com.atlassian.user.impl.DefaultUser;
34  import com.atlassian.user.impl.RepositoryException;
35  import com.atlassian.user.impl.hibernate.DefaultExternalEntityDAO;
36  import com.atlassian.user.impl.hibernate.DefaultHibernateUser;
37  import com.atlassian.user.impl.hibernate.properties.HibernatePropertySetFactory;
38  import com.atlassian.user.impl.osuser.OSUAccessor;
39  import com.atlassian.user.impl.osuser.OSUUserManager;
40  import com.opensymphony.user.provider.AccessProvider;
41  
42  /**
43   * <p>
44   * Makes a raw JDBC connection to os_user tables and copies across information into the supplied
45   * {@link com.atlassian.user.UserManager}, {@link com.atlassian.user.GroupManager}, and
46   * {@link com.atlassian.user.properties.PropertySetFactory}
47   * </p>
48   * <p>
49   * <strong>Note</strong> that this class is not safe for multi-threaded use since it maintains reference to the
50   * HibernateSession it is running within for it's duration.
51   * </p>
52   */
53  
54  public class OSUEntityMigrator implements EntityMigrator
55  {
56      private static final Logger log = Logger.getLogger(OSUEntityMigrator.class);
57  
58      private static final String OSUSER_REPOSITORY_KEY = "osuserRepository";
59  
60      private UserManager targetUserManager;
61      private GroupManager targetGroupManager;
62  
63      private AccessProvider osAccessProvider;
64  
65      private final SessionFactory sessionFactory;
66  
67      private final DefaultExternalEntityDAO externalEntityDAO;
68      
69      /**
70       * The session that will be used for run of this migrator. A reference is maintained to ensure it doesn't get
71       * garbage collected (and the connection closed) during the run.
72       */
73      private Session hibernateSession;
74  
75      public OSUEntityMigrator(RepositoryAccessor osuserRepositoryAccessor, RepositoryAccessor repositoryAccessor, SessionFactory sessionFactory)
76      {
77          if (osuserRepositoryAccessor == null)
78              throw new IllegalArgumentException("osuserRepositoryAccessor is required.");
79          if (repositoryAccessor == null)
80              throw new IllegalArgumentException("targetRepositoryAccessor is required.");
81          if (sessionFactory == null)
82              throw new IllegalArgumentException("sessionFactory is required.");
83  
84          this.sessionFactory = sessionFactory;
85          this.externalEntityDAO = new DefaultExternalEntityDAO(sessionFactory);
86  
87          final DelegationAccessor targetRepositoryAccessor = getNonOSUserRepositoryAccessor(repositoryAccessor);
88          if (!targetRepositoryAccessor.getRepositoryAccessors().isEmpty())
89          {
90              final UserManager osUserManager = osuserRepositoryAccessor.getUserManager();
91              if (osUserManager == null)
92                  throw new IllegalArgumentException("osUserManager is required.");
93  
94              final OSUAccessor osuAccessor = ((OSUUserManager) osUserManager).getAccessor();
95              if (osuAccessor == null)
96                  throw new IllegalArgumentException("osuAccessor is required.");
97  
98              osAccessProvider = osuAccessor.getAccessProvider();
99              if (osAccessProvider == null)
100                 throw new IllegalArgumentException("osAccessProvider is required.");
101 
102             targetUserManager = targetRepositoryAccessor.getUserManager();
103             targetGroupManager = targetRepositoryAccessor.getGroupManager();
104 
105             if (targetUserManager == null)
106                 throw new IllegalArgumentException("userManager is required.");
107             if (targetGroupManager == null)
108                 throw new IllegalArgumentException("groupManager is required.");
109         }
110     }
111 
112     private DelegationAccessor getNonOSUserRepositoryAccessor(RepositoryAccessor repositoryAccessor)
113     {
114         final DelegationAccessor nonOSUserDelegationAccessor = new DefaultDelegationAccessor();
115         if (repositoryAccessor instanceof DelegationAccessor)
116         {
117             final DelegationAccessor delegationAccessor = (DelegationAccessor) repositoryAccessor;
118             for (Iterator iterator = delegationAccessor.getRepositoryAccessors().iterator(); iterator.hasNext();)
119             {
120                 final RepositoryAccessor accessor = (RepositoryAccessor) iterator.next();
121                 if (!OSUSER_REPOSITORY_KEY.equals(accessor.getIdentifier().getKey()))
122                     nonOSUserDelegationAccessor.addRepositoryAccessor(accessor);
123             }
124             return nonOSUserDelegationAccessor;
125         }
126         else
127         {
128             if (!OSUSER_REPOSITORY_KEY.equals(repositoryAccessor.getIdentifier().getKey()))
129                 nonOSUserDelegationAccessor.addRepositoryAccessor(repositoryAccessor);
130         }
131         return nonOSUserDelegationAccessor;
132     }
133 
134     public boolean hasExistingUsers()
135     {
136         try
137         {
138             return !targetUserManager.getUsers().isEmpty();
139         }
140         catch (EntityException e)
141         {
142             throw new RuntimeException(e);
143         }
144     }
145 
146     /**
147      * The method is organised in a 'strange' way for performance reasons. DON'T change it. Every white space is there for a reason :)
148      * The performace problem was: every time we add a member to a hibernate group, hibernate marks this group object as dirty.
149      * Because hibernate group contains the list of all its members and needs to iterate through all of them when flush() is called.
150      * flush() is called every time hibernate thinks it needs to do it, for example when we call getUser() it first calls flush to make
151      * sure we get up to date data. We structured the code in a way so that flush is not called until we add all users to a group.
152      * That's why we cache the list of users. We also rely on the fact that targetGroupManager maintains its own cache and does not need
153      * to call flush() each time we get a group by a groupname.
154      *
155      * @throws RepositoryException if there is no non OSUser repository was included in target repository Accessor
156      */
157     public void migrate(MigratorConfiguration config, MigrationProgressListener progressListener) throws EntityException
158     {
159         if (targetUserManager == null)
160         {
161             throw new RepositoryException("No non OSUser repository configured. Cannot perform migration.");
162         }
163         
164         hibernateSession = SessionFactoryUtils.getSession(sessionFactory, true);        
165 
166         OSUserDao osUserDao = new OSUserDao(getDataSource());
167         final Map<Long, DefaultUser> users = osUserDao.findAllUsers();
168         final Map<String, List<String>> userGroups = osUserDao.findAllUserGroups(users);
169         Map<User, Boolean> migratedUsers = migrateUsers(progressListener, users);
170 
171         // migrate group memberships
172         for (Iterator<Map.Entry<User,Boolean>> it = migratedUsers.entrySet().iterator(); it.hasNext();)
173         {
174             Map.Entry<User,Boolean> userEntry = it.next();
175             final User user = userEntry.getKey();
176             migrateUserGroupMembership(user, userGroups.get(user.getName()), (userEntry.getValue()).booleanValue(), config, progressListener);
177         }
178 
179         // some groups could be empty hence they would not be migrated
180         // when migrating users' membership -- need to migrate them explicitly
181         migrateGroups(progressListener);
182     }
183 
184     private Map<User, Boolean> migrateUsers(MigrationProgressListener progressListener, Map<Long, DefaultUser> users)
185             throws EntityException
186     {
187         // starting user migration
188         progressListener.userMigrationStarted(users.size());
189 
190         Map<User, Boolean> migratedUsers = new HashMap<User, Boolean>();
191 
192         int i = 0;
193         for (Iterator<Map.Entry<Long,DefaultUser>> it = users.entrySet().iterator(); it.hasNext(); i++)
194         {
195             Map.Entry<Long,DefaultUser> userEntry = it.next();
196             final Long osUserId = userEntry.getKey();
197             final User user = userEntry.getValue();
198 
199             User existingUser = targetUserManager.getUser(user.getName());
200             if(existingUser == null)
201             {
202                 User newUser = addUser(targetUserManager, (DefaultUser) user);
203                 migratedUsers.put(newUser, Boolean.TRUE);
204             }
205             else
206             {
207                 migratedUsers.put(existingUser, Boolean.FALSE);
208             }
209             migratePropertySet(osUserId, user);
210 
211 
212             progressListener.userMigrated();
213             if (i % 100 == 0){
214                 try
215                 {
216                     hibernateSession.flush();
217                     hibernateSession.clear();
218                 }
219                 catch (HibernateException e)
220                 {
221                     log.error(e);
222                 }
223             }
224         }
225         // users migrated, starting group migration
226         progressListener.userMigrationComplete();
227         return migratedUsers;
228     }
229 
230     private void migrateGroups(MigrationProgressListener progressListener)
231             throws EntityException
232     {
233         final List groups = osAccessProvider.list();
234         progressListener.groupMigrationStarted(groups.size());
235 
236         for (Iterator groupsIterator = groups.iterator(); groupsIterator.hasNext();)
237         {
238             final String groupName = (String) groupsIterator.next();
239             getOrCreateGroup(groupName);
240             progressListener.groupMigrated();
241         }
242         // group migration complete
243         progressListener.groupMigrationComplete();
244     }
245 
246     private void migrateUserGroupMembership(User user, List<String> userGroups, boolean isCreatedUser, MigratorConfiguration config,
247                                             MigrationProgressListener progressListener) throws EntityException
248     {
249         if (userGroups != null){
250             for(Iterator<String> iter = userGroups.iterator();  iter.hasNext();){
251                 String groupName = iter.next();
252                 Group group = getOrCreateGroup(groupName);
253                 if (isCreatedUser || config.isMigrateMembershipsForExistingUsers())
254                 {
255                     if (log.isInfoEnabled()) log.info("Adding member <" + user.getName() + "> to group <" + groupName + ">");
256                     if (!targetGroupManager.isReadOnly(group))
257                     {
258                         targetGroupManager.addMembership(group, user);
259                     }
260                     else
261                     {
262                         progressListener.readonlyGroupMembershipNotMigrated(group.getName(), user.getName());
263                     }
264                 }
265             }
266         }
267     }
268 
269     private void migratePropertySet(Long userId, User user) throws EntityException
270     {
271         if (log.isInfoEnabled()) log.info("Migrating properties for <" + user.getName() + ">");
272 
273         final User targetUser = targetUserManager.getUser(user.getName());
274         final String entityName = getEntityName(targetUser);
275         final long entityId = getEntityId(targetUser);
276 
277         final JdbcTemplate template = new JdbcTemplate(getDataSource());
278 
279         if (template.queryForInt("SELECT count(*) FROM OS_PROPERTYENTRY WHERE entity_name=? AND entity_id=?", new Object[] {entityName, new Long(entityId)}) == 0)
280         {
281             template.query("SELECT * FROM OS_PROPERTYENTRY WHERE entity_name = 'OSUser_user' AND entity_id = ? AND entity_key <> 'fullName' AND entity_key <> 'email'", new Object[]{userId}, new RowCallbackHandler()
282             {
283                 public void processRow(ResultSet resultSet) throws SQLException
284                 {
285                     template.update("INSERT INTO OS_PROPERTYENTRY (entity_name,entity_id,entity_key,key_type,boolean_val,double_val,string_val,long_val,int_val,date_val) VALUES (?,?,?,?,?,?,?,?,?,?)", new Object[]{
286                         entityName,
287                         new Long(entityId),
288                         resultSet.getString("entity_key"),
289                         new Integer(resultSet.getInt("key_type")),
290                         Boolean.valueOf(resultSet.getBoolean("boolean_val")),
291                         new Double(resultSet.getDouble("double_val")),
292                         resultSet.getString("string_val"),
293                         new Long(resultSet.getLong("long_val")),
294                         new Integer(resultSet.getInt("int_val")),
295                         resultSet.getTimestamp("date_val")
296                     });
297                 }
298             });
299         }
300     }
301 
302     private String getEntityName(User user) throws EntityException
303     {
304         if (isExternalUser(user))
305         {
306             return HibernatePropertySetFactory.EXTERNAL_ENTITY + "_" + user.getName();
307         }
308         return HibernatePropertySetFactory.LOCAL_USER + "_" + user.getName();
309     }
310 
311     private long getEntityId(User user) throws EntityException
312     {
313         if (!isExternalUser(user))
314         {
315             return ((DefaultHibernateUser) user).getId();
316         }
317         ExternalEntity externalEntity = externalEntityDAO.getExternalEntity(user.getName());
318         if (externalEntity != null)
319         {
320             return externalEntity.getId();
321         }
322         return externalEntityDAO.createExternalEntity(user.getName()).getId();
323     }
324 
325     private boolean isExternalUser(User user)
326     {
327         return !(user instanceof DefaultHibernateUser);
328     }
329 
330     /**
331      * Adds the given user using the user manager.
332      *
333      * @param userManager the user manager to add the user to
334      * @param user the user to add
335      * @throws EntityException if a problem occur adding the user in the user manager
336      */
337     private User addUser(UserManager userManager, DefaultUser user) throws EntityException
338     {
339         if (log.isInfoEnabled()) log.info("Adding user <" + user.getName() + ">");
340         Credential credential = user.getPassword() == null ?
341             Credential.NONE :
342             Credential.encrypted(user.getPassword());
343         if (user.getFullName() == null) user.setFullName("");
344         if (user.getEmail() == null) user.setEmail("");
345         return userManager.createUser(user, credential);
346     }
347 
348     private Group getOrCreateGroup(String groupName) throws EntityException
349     {
350         Group group = targetGroupManager.getGroup(groupName);
351         if (group == null)
352         {
353             if (log.isInfoEnabled()) log.info("Creating group <" + groupName + ">");
354             group = targetGroupManager.createGroup(groupName);
355         }
356 
357         return group;
358     }
359 
360     private DataSource getDataSource()
361     {
362         Connection conn;
363         try
364         {
365             conn = hibernateSession.connection();
366         }
367         catch (HibernateException e)
368         {
369             throw new RuntimeException(e);
370         }
371         return new SingleConnectionDataSource(conn, true);
372     }
373 }