View Javadoc
1   /*
2    * Copyright (C) 2015 eXo Platform SAS.
3    *
4    * This is free software; you can redistribute it and/or modify it
5    * under the terms of the GNU Lesser General Public License as
6    * published by the Free Software Foundation; either version 2.1 of
7    * the License, or (at your option) any later version.
8    *
9    * This software is distributed in the hope that it will be useful,
10   * but WITHOUT ANY WARRANTY; without even the implied warranty of
11   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12   * Lesser General Public License for more details.
13   *
14   * You should have received a copy of the GNU Lesser General Public
15   * License along with this software; if not, write to the Free
16   * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
17   * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
18   */
19  
20  package org.exoplatform.social.core.jpa.storage.dao.jpa;
21  
22  import java.lang.reflect.Array;
23  import java.math.BigInteger;
24  import java.util.ArrayList;
25  import java.util.HashMap;
26  import java.util.LinkedHashMap;
27  import java.util.List;
28  import java.util.Map;
29  import java.util.function.Function;
30  import java.util.stream.Collectors;
31  
32  import javax.persistence.EntityExistsException;
33  import javax.persistence.NoResultException;
34  import javax.persistence.Query;
35  import javax.persistence.TypedQuery;
36  
37  import org.exoplatform.commons.api.persistence.ExoTransactional;
38  import org.exoplatform.commons.persistence.impl.GenericDAOJPAImpl;
39  import org.exoplatform.commons.utils.ListAccess;
40  import org.exoplatform.services.log.ExoLogger;
41  import org.exoplatform.services.log.Log;
42  import org.exoplatform.social.core.identity.model.Identity;
43  import org.exoplatform.social.core.identity.model.Profile;
44  import org.exoplatform.social.core.identity.provider.OrganizationIdentityProvider;
45  import org.exoplatform.social.core.jpa.search.ExtendProfileFilter;
46  import org.exoplatform.social.core.jpa.storage.dao.IdentityDAO;
47  import org.exoplatform.social.core.jpa.storage.dao.jpa.query.ProfileQueryBuilder;
48  import org.exoplatform.social.core.jpa.storage.entity.ConnectionEntity;
49  import org.exoplatform.social.core.jpa.storage.entity.IdentityEntity;
50  import org.exoplatform.social.core.relationship.model.Relationship.Type;
51  
52  /**
53   * @author <a href="mailto:tuyennt@exoplatform.com">Tuyen Nguyen The</a>.
54   */
55  public class IdentityDAOImpl extends GenericDAOJPAImpl<IdentityEntity, Long> implements IdentityDAO {
56    
57    private static final Log LOG = ExoLogger.getLogger(IdentityDAOImpl.class);
58  
59    @Override
60    public IdentityEntity create(IdentityEntity entity) {
61      IdentityEntity exists = findByProviderAndRemoteId(entity.getProviderId(), entity.getRemoteId());
62      if (exists != null) {
63        throw new EntityExistsException("Identity is existed with ProviderID=" + entity.getProviderId() + " and RemoteId=" + entity.getRemoteId());
64      }
65      return super.create(entity);
66    }
67  
68    @Override
69    public IdentityEntity findByProviderAndRemoteId(String providerId, String remoteId) {
70      TypedQuery<IdentityEntity> query = getEntityManager().createNamedQuery("SocIdentity.findByProviderAndRemoteId", IdentityEntity.class);
71      query.setParameter("providerId", providerId);
72      query.setParameter("remoteId", remoteId);
73  
74      try {
75        return query.getSingleResult();
76      } catch (NoResultException ex) {
77        return null;
78      }
79    }
80  
81    @Override
82    public long countIdentityByProvider(String providerId) {
83      TypedQuery<Long> query = getEntityManager().createNamedQuery("SocIdentity.countIdentityByProvider", Long.class);
84      query.setParameter("providerId", providerId);
85      return query.getSingleResult();
86    }
87  
88    @Override
89    public List<Long> getAllIds(int offset, int limit) {
90      TypedQuery<Long> query = getEntityManager().createNamedQuery("SocIdentity.getAllIds", Long.class);
91      if (limit > 0) {
92        query.setFirstResult(offset);
93        query.setMaxResults(limit);
94      }
95      return query.getResultList();
96    }
97  
98    @Override
99    public List<Long> getAllIdsByProvider(String providerId, int offset, int limit) {
100     TypedQuery<Long> query = getEntityManager().createNamedQuery("SocIdentity.getAllIdsByProvider", Long.class);
101     query.setParameter("providerId", providerId);
102     if (limit > 0) {
103       query.setFirstResult(offset);
104       query.setMaxResults(limit);
105     }
106     return query.getResultList();
107   }
108 
109   @Override
110   public ListAccess<Map.Entry<IdentityEntity, ConnectionEntity>> findAllIdentitiesWithConnections(long identityId, String sortField, char firstChar) {
111     Query listQuery = getIdentitiesQuerySortedByField(OrganizationIdentityProvider.NAME, sortField, firstChar);
112 
113     TypedQuery<ConnectionEntity> connectionsQuery = getEntityManager().createNamedQuery("SocConnection.findConnectionsByIdentityIds", ConnectionEntity.class);
114 
115     TypedQuery<Long> countQuery = getEntityManager().createNamedQuery("SocIdentity.countIdentitiesByProviderWithExcludedIdentity", Long.class);
116     countQuery.setParameter("providerId", OrganizationIdentityProvider.NAME);
117 
118     return new IdentityWithRelationshipListAccess(identityId, listQuery, connectionsQuery, countQuery);
119   }
120 
121   @Override
122   public ListAccess<IdentityEntity> findIdentities(ExtendProfileFilter filter) {
123     if (filter.getConnection() != null) {
124       Identity owner = filter.getConnection();
125       Long ownerId = Long.valueOf(owner.getId());
126       Type status = filter.getConnectionStatus();
127       List<Long> connections = getConnections(ownerId, status);
128       if (connections.isEmpty()) {
129         return new JPAListAccess<>(IdentityEntity.class);
130       } else if (filter.getIdentityIds() == null || filter.getIdentityIds().isEmpty()) {
131         filter.setIdentityIds(connections);
132       } else {
133         filter.getIdentityIds().retainAll(connections);
134       }
135     }
136 
137     ProfileQueryBuilder qb = ProfileQueryBuilder.builder()
138             .withFilter(filter);
139     TypedQuery[] queries = qb.build(getEntityManager());
140 
141     return new JPAListAccess<>(IdentityEntity.class, queries[0], queries[1]);
142   }
143 
144   @Override
145   public List<String> getAllIdsByProviderSorted(String providerId, String sortField, char firstChar, long offset, long limit) {
146     Query query = getIdentitiesQuerySortedByField(providerId, sortField, firstChar);
147     return getResultsFromQuery(query, 0, offset, limit, String.class);
148   }
149 
150   @Override
151   @ExoTransactional
152   public void setAsDeleted(long identityId) {
153     IdentityEntity entity = find(identityId);
154     if (entity != null) {
155       entity.setDeleted(true);
156       update(entity);
157     }
158   }
159 
160   @Override
161   @ExoTransactional
162   public void hardDeleteIdentity(long identityId) {
163     IdentityEntity entity = find(identityId);
164     if (entity != null) {
165       delete(entity);
166     }
167   }
168 
169   public List<IdentityEntity> findIdentitiesByIDs(List<?> ids) {
170     TypedQuery<IdentityEntity> query = getEntityManager().createNamedQuery("SocIdentity.findIdentitiesByIDs", IdentityEntity.class);
171     query.setParameter("ids", ids);
172     return query.getResultList();
173   }
174 
175   @SuppressWarnings("unchecked")
176   private List<Long> getConnections(Long ownerId, Type status) {
177     String queryName = null;
178     Class<?> returnType = null;
179     if (status == null || status == Type.ALL) {
180       queryName = "SocConnection.getConnectionsWithoutStatus";
181       returnType = ConnectionEntity.class;
182     } else if (status == Type.INCOMING) {
183       queryName = "SocConnection.getSenderIdsByReceiverWithStatus";
184       returnType = Long.class;
185       status = Type.PENDING;
186     } else if (status == Type.OUTGOING) {
187       queryName = "SocConnection.getReceiverIdsBySenderWithStatus";
188       returnType = Long.class;
189       status = Type.PENDING;
190     } else {
191       queryName = "SocConnection.getConnectionsWithStatus";
192       returnType = ConnectionEntity.class;
193     }
194 
195     Query query = getEntityManager().createNamedQuery(queryName);
196     query.setParameter("identityId", ownerId);
197     if (status != null && status != Type.ALL) {
198       query.setParameter("status", status);
199     }
200     if (returnType == Long.class) {
201       return query.getResultList();
202     } else {
203       List<Long> ids = new ArrayList<Long>();
204       List<ConnectionEntity> connectionEntities = query.getResultList();
205       for (ConnectionEntity connectionEntity : connectionEntities) {
206         if (connectionEntity.getReceiver().getId() == ownerId) {
207           ids.add(connectionEntity.getSender().getId());
208         } else if (connectionEntity.getSender().getId() == ownerId) {
209           ids.add(connectionEntity.getReceiver().getId());
210         } else {
211           LOG.warn("Neither sender neither receiver corresponds to owner with id {}. ", ownerId);
212         }
213       }
214       return ids;
215     }
216   }
217 
218   public static class JPAListAccess<T> implements ListAccess<T> {
219     private final TypedQuery<T> selectQuery;
220     private final TypedQuery<Long> countQuery;
221     private final Class<T> clazz;
222 
223     public JPAListAccess(Class<T> clazz) {
224       this.clazz = clazz;
225       this.selectQuery = null;
226       this.countQuery = null;
227     }
228                          
229     public JPAListAccess(Class<T> clazz, TypedQuery<T> selectQuery, TypedQuery<Long> countQuery) {
230       this.clazz = clazz;
231       this.selectQuery = selectQuery;
232       this.countQuery = countQuery;
233     }
234 
235     @Override
236     public T[] load(int offset, int limit) throws Exception, IllegalArgumentException {
237       if (selectQuery == null) {
238         return (T[]) Array.newInstance(clazz, 0);
239       }
240       if (limit > 0 && offset >= 0) {
241         selectQuery.setFirstResult(offset);
242         selectQuery.setMaxResults(limit);
243       } else {
244         selectQuery.setMaxResults(Integer.MAX_VALUE);
245       }
246 
247       List<T> list = selectQuery.getResultList();
248       if (list != null && list.size() > 0) {
249         T[] arr = (T[])Array.newInstance(clazz, list.size());
250         return list.toArray(arr);
251       } else {
252         return (T[])Array.newInstance(clazz, 0);
253       }
254     }
255 
256     @Override
257     public int getSize() throws Exception {
258       if (countQuery == null) {
259         return 0;
260       }
261       return countQuery.getSingleResult().intValue();
262     }
263   }
264 
265   public class IdentityWithRelationshipListAccess implements ListAccess<Map.Entry<IdentityEntity, ConnectionEntity>> {
266     private final Query identityQuery;
267     private final TypedQuery<ConnectionEntity> connectionsQuery;
268     private final TypedQuery<Long> countQuery;
269     private final long identityId;
270 
271     public IdentityWithRelationshipListAccess(long identityId, Query identityQuery, TypedQuery<ConnectionEntity> connctionsQuery, TypedQuery<Long> countQuery) {
272       this.identityQuery = identityQuery;
273       this.connectionsQuery = connctionsQuery;
274       this.countQuery = countQuery;
275       this.identityId = identityId;
276     }
277 
278     @SuppressWarnings("unchecked")
279     @Override
280     public Map.Entry<IdentityEntity, ConnectionEntity>[] load(int offset, int limit) throws Exception, IllegalArgumentException {
281       List<Object> ids = getResultsFromQuery(identityQuery, 1, offset, limit, Object.class);
282 
283       if(ids.isEmpty()) {
284         return new Map.Entry[0];
285       }
286       List<Long> idsLong = ids.stream().map(i -> Long.parseLong(i.toString())).collect(Collectors.toList());
287       List<IdentityEntity> identitiesList = findIdentitiesByIDs(idsLong);
288       Map<Long, IdentityEntity> identitiesMap = identitiesList.stream().collect(Collectors.toMap(identity -> identity.getId(), Function.identity()));
289       connectionsQuery.setParameter("identityId", identityId);
290       connectionsQuery.setParameter("ids", idsLong);
291       connectionsQuery.setMaxResults(Integer.MAX_VALUE);
292       List<ConnectionEntity> connectionsList = connectionsQuery.getResultList();
293       Map<IdentityEntity, ConnectionEntity> map = new LinkedHashMap<IdentityEntity, ConnectionEntity>();
294       for (Long identityId : idsLong) {
295         IdentityEntity identityEntity = identitiesMap.get(identityId);
296         if (identityEntity == null) {
297           LOG.error("Can't find identity with id '{}'", identityId);
298           continue;
299         }
300         CONN: for (ConnectionEntity connectionEntity : connectionsList) {
301           if(connectionEntity.getReceiver().getId() == identityEntity.getId() || connectionEntity.getSender().getId() == identityEntity.getId()) {
302             map.put(identityEntity, connectionEntity);
303             break CONN;
304           }
305         }
306         if (!map.containsKey(identityEntity)) {
307           map.put(identityEntity, null);
308         }
309       }
310       return map.entrySet().toArray(new Map.Entry[0]);
311     }
312 
313     @Override
314     public int getSize() throws Exception {
315       return countQuery.getSingleResult().intValue();
316     }
317   }
318 
319   private Query getIdentitiesQuerySortedByField(String providerId, String sortField, char firstCharacter) {
320     // Oracle and MSSQL support only 1/0 for boolean, Postgresql supports only TRUE/FALSE, MySQL supports both
321     String dbBoolFalse = isOrcaleDialect() || isMSSQLDialect() ? "0" : "FALSE";
322     String dbBoolTrue = isOrcaleDialect() || isMSSQLDialect() ? "1" : "TRUE";
323     // Oracle Dialect in Hibernate 4 is not registering NVARCHAR correctly, see HHH-10495
324     StringBuilder queryStringBuilder =
325             isOrcaleDialect() ? new StringBuilder("SELECT to_char(identity_1.remote_id), identity_1.identity_id, to_char(identity_prop.value) \n")
326                     :isMSSQLDialect() ? new StringBuilder("SELECT try_convert(varchar(200), identity_1.remote_id) as remote_id , identity_1.identity_id, try_convert(varchar(200), identity_prop.value) as identity_prop_value \n")
327                     : new StringBuilder("SELECT (identity_1.remote_id), identity_1.identity_id, (identity_prop.value) \n");
328     queryStringBuilder.append(" FROM SOC_IDENTITIES identity_1 \n");
329     if (firstCharacter > 0) {
330       queryStringBuilder.append(" INNER JOIN SOC_IDENTITY_PROPERTIES identity_prop_first_char \n");
331       queryStringBuilder.append("   ON identity_1.identity_id = identity_prop_first_char.identity_id \n");
332       queryStringBuilder.append("       AND identity_prop_first_char.name = '").append(Profile.LAST_NAME).append("' \n");
333       queryStringBuilder.append("       AND (lower(identity_prop_first_char.value) like '" + Character.toLowerCase(firstCharacter) + "%')\n");
334     }
335     queryStringBuilder.append(" LEFT JOIN SOC_IDENTITY_PROPERTIES identity_prop \n");
336     queryStringBuilder.append("   ON identity_1.identity_id = identity_prop.identity_id \n");
337     queryStringBuilder.append("       AND identity_prop.name = '").append(sortField).append("' \n");
338     queryStringBuilder.append(" WHERE identity_1.provider_id = '").append(providerId).append("' \n");
339     queryStringBuilder.append(" AND identity_1.deleted = ").append(dbBoolFalse).append(" \n");
340     queryStringBuilder.append(" AND identity_1.enabled = ").append(dbBoolTrue).append(" \n");
341     queryStringBuilder.append(" ORDER BY identity_prop.value ASC");
342 
343     Query query = getEntityManager().createNativeQuery(queryStringBuilder.toString());
344     return query;
345   }
346 
347 
348   private <T> List<T> getResultsFromQuery(Query query, int fieldIndex, long offset, long limit, Class<T> clazz) {
349     if (limit > 0) {
350       query.setMaxResults((int) limit);
351     }
352     if (offset >= 0) {
353       query.setFirstResult((int) offset);
354     }
355 
356     List<?> resultList = query.getResultList();
357     List<T> result = new ArrayList<T>();
358     for (Object object : resultList) {
359       Object[] resultEntry = (Object[]) object;
360       Object resultObject = resultEntry[fieldIndex];
361       if (resultObject == null) {
362         continue;
363       }
364       result.add((T) resultObject);
365     }
366     return result;
367   }
368 
369 }