View Javadoc
1   package net.sumaris.core.dao.administration.user;
2   
3   /*-
4    * #%L
5    * SUMARiS:: Core
6    * %%
7    * Copyright (C) 2018 SUMARiS Consortium
8    * %%
9    * This program is free software: you can redistribute it and/or modify
10   * it under the terms of the GNU General Public License as
11   * published by the Free Software Foundation, either version 3 of the
12   * License, or (at your option) any later version.
13   * 
14   * This program is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17   * GNU General Public License for more details.
18   * 
19   * You should have received a copy of the GNU General Public
20   * License along with this program.  If not, see
21   * <http://www.gnu.org/licenses/gpl-3.0.html>.
22   * #L%
23   */
24  
25  import com.google.common.base.Preconditions;
26  import com.google.common.collect.ImmutableList;
27  import net.sumaris.core.dao.data.ImageAttachmentDao;
28  import net.sumaris.core.dao.referential.ReferentialDao;
29  import net.sumaris.core.dao.technical.SoftwareDao;
30  import net.sumaris.core.dao.technical.SortDirection;
31  import net.sumaris.core.dao.technical.hibernate.HibernateDaoSupport;
32  import net.sumaris.core.model.administration.user.Department;
33  import net.sumaris.core.model.administration.user.Person;
34  import net.sumaris.core.model.referential.IReferentialEntity;
35  import net.sumaris.core.model.referential.Status;
36  import net.sumaris.core.model.referential.UserProfile;
37  import net.sumaris.core.model.referential.UserProfileEnum;
38  import net.sumaris.core.util.Beans;
39  import net.sumaris.core.util.crypto.MD5Util;
40  import net.sumaris.core.vo.administration.user.DepartmentVO;
41  import net.sumaris.core.vo.administration.user.PersonVO;
42  import net.sumaris.core.vo.data.ImageAttachmentVO;
43  import net.sumaris.core.vo.filter.PersonFilterVO;
44  import net.sumaris.core.vo.referential.ReferentialVO;
45  import org.apache.commons.collections4.CollectionUtils;
46  import org.apache.commons.lang3.ArrayUtils;
47  import org.apache.commons.lang3.StringUtils;
48  import org.nuiton.i18n.I18n;
49  import org.slf4j.Logger;
50  import org.slf4j.LoggerFactory;
51  import org.springframework.beans.factory.annotation.Autowired;
52  import org.springframework.dao.DataRetrievalFailureException;
53  import org.springframework.dao.EmptyResultDataAccessException;
54  import org.springframework.stereotype.Repository;
55  
56  import javax.persistence.EntityManager;
57  import javax.persistence.LockModeType;
58  import javax.persistence.NoResultException;
59  import javax.persistence.criteria.*;
60  import java.sql.Timestamp;
61  import java.util.*;
62  import java.util.concurrent.CopyOnWriteArrayList;
63  import java.util.regex.Matcher;
64  import java.util.regex.Pattern;
65  import java.util.stream.Collectors;
66  
67  @Repository("personDao")
68  public class PersonDaoImpl extends HibernateDaoSupport implements PersonDao {
69  
70      /** Logger. */
71      private static final Logger log =
72              LoggerFactory.getLogger(PersonDaoImpl.class);
73  
74      private List<Listener> listeners = new CopyOnWriteArrayList<>();
75  
76      @Autowired
77      private DepartmentDao departmentDao;
78  
79      @Autowired
80      private ImageAttachmentDao imageAttachmentDao;
81  
82      @Autowired
83      private SoftwareDao softwareDao;
84  
85      @Autowired
86      private ReferentialDao referentialDao;
87  
88      @Override
89      @SuppressWarnings("unchecked")
90      public List<PersonVO> findByFilter(PersonFilterVO filter, int offset, int size, String sortAttribute, SortDirection sortDirection) {
91          Preconditions.checkNotNull(filter);
92          Preconditions.checkArgument(offset >= 0);
93          Preconditions.checkArgument(size > 0);
94  
95          EntityManager entityManager = getEntityManager();
96          CriteriaBuilder builder = entityManager.getCriteriaBuilder();
97          CriteriaQuery<Person> query = builder.createQuery(Person.class);
98          Root<Person> root = query.from(Person.class);
99          Join<Person, UserProfile> upJ = root.join(Person.Fields.USER_PROFILES, JoinType.LEFT);
100 
101         ParameterExpression<Boolean> hasUserProfileIdsParam = builder.parameter(Boolean.class);
102         ParameterExpression<Collection> userProfileIdsParam = builder.parameter(Collection.class);
103         ParameterExpression<Boolean> hasStatusIdsParam = builder.parameter(Boolean.class);
104         ParameterExpression<Collection> statusIdsParam = builder.parameter(Collection.class);
105         ParameterExpression<String> pubkeyParam = builder.parameter(String.class);
106         ParameterExpression<String> firstNameParam = builder.parameter(String.class);
107         ParameterExpression<String> lastNameParam = builder.parameter(String.class);
108         ParameterExpression<String> emailParam = builder.parameter(String.class);
109         ParameterExpression<String> searchTextParam = builder.parameter(String.class);
110 
111         // Prepare status ids
112         Collection<Integer> statusIds = ArrayUtils.isEmpty(filter.getStatusIds()) ?
113                 null : ImmutableList.copyOf(filter.getStatusIds());
114 
115         // Prepare user profile ids
116         Collection<Integer> userProfileIds;
117         if (ArrayUtils.isNotEmpty(filter.getUserProfiles())) {
118             userProfileIds = Arrays.stream(filter.getUserProfiles())
119                     .map(UserProfileEnum::valueOf)
120                     .map(profile -> profile.id)
121                     .collect(Collectors.toList());
122         }
123         else if (ArrayUtils.isNotEmpty(filter.getUserProfileIds())) {
124             userProfileIds = ImmutableList.copyOf(filter.getUserProfileIds());
125         }
126         else if (filter.getUserProfileId() != null) {
127             userProfileIds = ImmutableList.of(filter.getUserProfileId());
128         }
129         else {
130             userProfileIds = null;
131         }
132 
133         query.select(root).distinct(true)
134              .where(
135                 builder.and(
136                     // user profile Ids
137                     builder.or(
138                             builder.isFalse(hasUserProfileIdsParam),
139                             upJ.get(UserProfile.Fields.ID).in(userProfileIdsParam)
140                     ),
141                     // status Ids
142                     builder.or(
143                         builder.isFalse(hasStatusIdsParam),
144                         root.get(Person.Fields.STATUS).get(Status.Fields.ID).in(statusIdsParam)
145                     ),
146                     // pubkey
147                     builder.or(
148                             builder.isNull(pubkeyParam),
149                             builder.equal(root.get(Person.Fields.PUBKEY), pubkeyParam)
150                     ),
151                     // email
152                     builder.or(
153                             builder.isNull(emailParam),
154                             builder.equal(root.get(Person.Fields.EMAIL), emailParam)
155                     ),
156                     // firstName
157                     builder.or(
158                             builder.isNull(firstNameParam),
159                             builder.equal(builder.upper(root.get(Person.Fields.FIRST_NAME)), builder.upper(firstNameParam))
160                     ),
161                     // lastName
162                     builder.or(
163                             builder.isNull(lastNameParam),
164                             builder.equal(builder.upper(root.get(Person.Fields.LAST_NAME)), builder.upper(lastNameParam))
165                     ),
166                     // search text
167                     builder.or(
168                             builder.isNull(searchTextParam),
169                             builder.like(builder.upper(root.get(Person.Fields.PUBKEY)), builder.upper(searchTextParam)),
170                             builder.like(builder.upper(root.get(Person.Fields.EMAIL)), builder.upper(searchTextParam)),
171                             builder.like(builder.upper(root.get(Person.Fields.FIRST_NAME)), builder.upper(searchTextParam)),
172                             builder.like(builder.upper(root.get(Person.Fields.LAST_NAME)), builder.upper(searchTextParam))
173                     )
174                 ));
175         if (StringUtils.isNotBlank(sortAttribute)) {
176             if (sortDirection == SortDirection.ASC) {
177                 query.orderBy(builder.asc(root.get(sortAttribute)));
178             } else {
179                 query.orderBy(builder.desc(root.get(sortAttribute)));
180             }
181         }
182 
183         String searchText = StringUtils.trimToNull(filter.getSearchText());
184         String searchTextAnyMatch = null;
185         if (StringUtils.isNotBlank(searchText)) {
186             searchTextAnyMatch = ("*" + searchText + "*"); // add trailing escape char
187             searchTextAnyMatch = searchTextAnyMatch.replaceAll("[*]+", "*"); // group escape chars
188             searchTextAnyMatch = searchTextAnyMatch.replaceAll("[%]", "\\%"); // protected '%' chars
189             searchTextAnyMatch = searchTextAnyMatch.replaceAll("[*]", "%"); // replace asterix
190         }
191 
192 
193         return entityManager.createQuery(query)
194                 .setParameter(hasUserProfileIdsParam, CollectionUtils.isNotEmpty(userProfileIds))
195                 .setParameter(userProfileIdsParam,  userProfileIds)
196                 .setParameter(hasStatusIdsParam, CollectionUtils.isNotEmpty(statusIds))
197                 .setParameter(statusIdsParam, statusIds)
198                 .setParameter(pubkeyParam, filter.getPubkey())
199                 .setParameter(emailParam, filter.getEmail())
200                 .setParameter(firstNameParam, filter.getFirstName())
201                 .setParameter(lastNameParam, filter.getLastName())
202                 .setParameter(searchTextParam, searchTextAnyMatch)
203                 .setFirstResult(offset)
204                 .setMaxResults(size)
205                 .getResultList()
206                 .stream()
207                 .map(this::toPersonVO)
208                 .collect(Collectors.toList());
209     }
210 
211 
212     @Override
213     public Long countByFilter(PersonFilterVO filter) {
214         Preconditions.checkNotNull(filter);
215 
216         List<Integer> statusIds = ArrayUtils.isEmpty(filter.getStatusIds()) ?
217                 null : ImmutableList.copyOf(filter.getStatusIds());
218 
219         return getEntityManager().createNamedQuery("Person.count", Long.class)
220                 .setParameter("userProfileId", filter.getUserProfileId())
221                 .setParameter("statusIds", statusIds)
222                 .setParameter("email", StringUtils.trimToNull(filter.getEmail()))
223                 .setParameter("pubkey", StringUtils.trimToNull(filter.getPubkey()))
224                 .setParameter("firstName", StringUtils.trimToNull(filter.getFirstName()))
225                 .setParameter("lastName", StringUtils.trimToNull(filter.getLastName()))
226                 .getSingleResult();
227     }
228 
229     @Override
230     public PersonVO getByPubkeyOrNull(String pubkey) {
231         return toPersonVO(getEntityByPubkeyOrNull(pubkey));
232     }
233 
234     @Override
235     public ImageAttachmentVO getAvatarByPubkey(String pubkey) {
236 
237         Person person = getEntityByPubkeyOrNull(pubkey);
238         if (person == null || person.getAvatar() == null) {
239             throw new DataRetrievalFailureException(I18n.t("sumaris.error.person.avatar.notFound"));
240         }
241 
242         return imageAttachmentDao.get(person.getAvatar().getId());
243     }
244 
245     @Override
246     public List<String> getEmailsByProfiles(List<Integer> userProfiles, List<Integer> statusIds) {
247         Preconditions.checkNotNull(userProfiles);
248         Preconditions.checkArgument(CollectionUtils.isNotEmpty(userProfiles));
249         Preconditions.checkNotNull(statusIds);
250         Preconditions.checkArgument(CollectionUtils.isNotEmpty(statusIds));
251 
252         EntityManager entityManager = getEntityManager();
253         CriteriaBuilder builder = entityManager.getCriteriaBuilder();
254         CriteriaQuery<Person> query = builder.createQuery(Person.class);
255         Root<Person> root = query.from(Person.class);
256 
257         Join<Person, UserProfile> upJ = root.join(Person.Fields.USER_PROFILES, JoinType.INNER);
258 
259         ParameterExpression<Collection> userProfileIdParam = builder.parameter(Collection.class);
260         ParameterExpression<Collection> statusIdsParam = builder.parameter(Collection.class);
261 
262         query.select(root/*.get(Person.PROPERTY_EMAIL)*/).distinct(true)
263                 .where(
264                         builder.and(
265                                 // user profile Ids
266                                 upJ.get(UserProfile.Fields.ID).in(userProfileIdParam),
267                                 // status Ids
268                                 root.get(Person.Fields.STATUS).get(Status.Fields.ID).in(statusIdsParam)
269                         ));
270 
271         // TODO: select email column only
272         return entityManager.createQuery(query)
273                 .setParameter(userProfileIdParam, userProfiles)
274                 .setParameter(statusIdsParam, ImmutableList.copyOf(statusIds))
275                 .setMaxResults(100)
276                 .getResultList()
277                 .stream()
278                 .map(Person::getEmail)
279                 .filter(Objects::nonNull)
280                 .collect(Collectors.toList());
281     }
282 
283     @Override
284     public boolean isExistsByEmailHash(final String hash) {
285 
286         CriteriaBuilder builder = getEntityManager().getCriteriaBuilder();
287         CriteriaQuery<Long> query = builder.createQuery(Long.class);
288         Root<Person> root = query.from(Person.class);
289 
290         ParameterExpression<String> hashParam = builder.parameter(String.class);
291 
292         query.select(builder.count(root.get(Person.Fields.ID)))
293              .where(builder.equal(root.get(Person.Fields.EMAIL_M_D5), hashParam));
294 
295         return getEntityManager().createQuery(query)
296                 .setParameter(hashParam, hash)
297                 .getSingleResult() > 0;
298     }
299 
300     @Override
301     public PersonVO get(int id) {
302         return toPersonVO(get(Person.class, id));
303     }
304 
305     @Override
306     public PersonVOf="../../../../../../net/sumaris/core/vo/administration/user/PersonVO.html#PersonVO">PersonVO save(PersonVO source) {
307         Preconditions.checkNotNull(source);
308         Preconditions.checkNotNull(source.getEmail(), "Missing 'email'");
309         Preconditions.checkNotNull(source.getStatusId(), "Missing 'statusId'");
310         Preconditions.checkNotNull(source.getDepartment(), "Missing 'department'");
311         Preconditions.checkNotNull(source.getDepartment().getId(), "Missing 'department.id'");
312 
313         EntityManager entityManager = getEntityManager();
314         Person entity = null;
315         if (source.getId() != null) {
316             entity = get(Person.class, source.getId());
317         }
318         boolean isNew = (entity == null);
319         if (isNew) {
320             entity = new Person();
321         }
322 
323         // If new
324         if (isNew) {
325             // Set default status to Temporary
326             if (source.getStatusId() == null) {
327                 source.setStatusId(config.getStatusIdTemporary());
328             }
329         }
330         // If update
331         else {
332 
333             // Check update date
334             checkUpdateDateForUpdate(source, entity);
335 
336             // Lock entityName
337             lockForUpdate(entity, LockModeType.PESSIMISTIC_WRITE);
338         }
339 
340         personVOToEntity(source, entity, true);
341 
342         // Update update_dt
343         Timestamp newUpdateDate = getDatabaseCurrentTimestamp();
344         entity.setUpdateDate(newUpdateDate);
345 
346         // Save entityName
347         if (isNew) {
348             // Force creation date
349             entity.setCreationDate(newUpdateDate);
350             source.setCreationDate(newUpdateDate);
351 
352             entityManager.persist(entity);
353             source.setId(entity.getId());
354         } else {
355             entityManager.merge(entity);
356         }
357 
358         source.setUpdateDate(newUpdateDate);
359 
360         getEntityManager().flush();
361         getEntityManager().clear();
362 
363         // Emit event to listeners
364         emitSaveEvent(source);
365 
366         return source;
367     }
368 
369     @Override
370     public void delete(int id) {
371         log.debug(String.format("Deleting person {id=%s}...", id));
372         delete(Person.class, id);
373 
374         // Emit to listener
375         emitDeleteEvent(id);
376     }
377 
378     @Override
379     public PersonVO toPersonVO(Person source) {
380         if (source == null) return null;
381         PersonVOnistration/user/PersonVO.html#PersonVO">PersonVO target = new PersonVO();
382 
383         Beans.copyProperties(source, target);
384 
385         // Department
386         DepartmentVO department = departmentDao.get(source.getDepartment().getId());
387         target.setDepartment(department);
388 
389         // Status
390         target.setStatusId(source.getStatus().getId());
391 
392         // Profiles (keep only label)
393         if (CollectionUtils.isNotEmpty(source.getUserProfiles())) {
394             List<String> profiles = source.getUserProfiles().stream()
395                     .map(profile -> getUserProfileLabelTranslationMap(true).getOrDefault(profile.getLabel(), profile.getLabel()))
396                     .collect(Collectors.toList());
397             target.setProfiles(profiles);
398         }
399 
400         // Has avatar
401         target.setHasAvatar(source.getAvatar() != null);
402 
403         return target;
404     }
405 
406     @Override
407     public void addListener(Listener listener) {
408         if (!listeners.contains(listener)) {
409             listeners.add(listener);
410         }
411     }
412 
413 
414     /* -- protected methods -- */
415 
416     protected List<PersonVO> toPersonVOs(List<Person> source) {
417         return source.stream()
418                 .map(this::toPersonVO)
419                 .filter(Objects::nonNull)
420                 .collect(Collectors.toList());
421     }
422 
423     protected void personVOToEntity(PersonVO source, Person target, boolean copyIfNull) {
424 
425         Beans.copyProperties(source, target);
426 
427         // Email
428         if (StringUtils.isNotBlank(source.getEmail())) {
429             target.setEmailMD5(MD5Util.md5Hex(source.getEmail()));
430         }
431 
432         // Department
433         if (copyIfNull || source.getDepartment() != null) {
434             if (source.getDepartment() == null) {
435                 target.setDepartment(null);
436             }
437             else {
438                 target.setDepartment(load(Department.class, source.getDepartment().getId()));
439             }
440         }
441 
442         // Status
443         if (copyIfNull || source.getStatusId() != null) {
444             if (source.getStatusId() == null) {
445                 target.setStatus(null);
446             }
447             else {
448                 target.setStatus(load(Status.class, source.getStatusId()));
449             }
450         }
451 
452         // User profiles
453         if (copyIfNull || CollectionUtils.isNotEmpty(source.getProfiles())) {
454             if (CollectionUtils.isEmpty(source.getProfiles())) {
455                 target.getUserProfiles().clear();
456             }
457             else {
458                 target.getUserProfiles().clear();
459                 for (String profile: source.getProfiles()) {
460                     if (StringUtils.isNotBlank(profile)) {
461                         // translate the user profile label
462                         String translatedLabel = getUserProfileLabelTranslationMap(false).getOrDefault(profile, profile);
463                         if (StringUtils.isNotBlank(translatedLabel)) {
464                             ReferentialVO userProfileVO = referentialDao.findByUniqueLabel("UserProfile", translatedLabel);
465                             UserProfile up = load(UserProfile.class, userProfileVO.getId());
466                             target.getUserProfiles().add(up);
467                         }
468                     }
469                 }
470             }
471         }
472 
473     }
474 
475 
476     protected Person getEntityByPubkeyOrNull(String pubkey) {
477 
478         EntityManager entityManager = getEntityManager();
479         CriteriaBuilder builder = entityManager.getCriteriaBuilder();
480         CriteriaQuery<Person> query = builder.createQuery(Person.class);
481         Root<Person> root = query.from(Person.class);
482 
483         ParameterExpression<String> pubkeyParam = builder.parameter(String.class);
484 
485         query.select(root)
486                 .where(builder.equal(root.get(PersonVO.Fields.PUBKEY), pubkeyParam));
487 
488         try {
489             return entityManager.createQuery(query)
490                     .setParameter(pubkeyParam, pubkey)
491                     .getSingleResult();
492         } catch (EmptyResultDataAccessException | NoResultException e) {
493             return null;
494         }
495     }
496 
497     protected void emitSaveEvent(final PersonVO person) {
498         listeners.forEach(l -> {
499             try {
500                 l.onSave(person);
501             } catch(Throwable t) {
502                 log.error("Person listener (onSave) error: " + t.getMessage(), t);
503                 // Continue, to avoid transaction cancellation
504             }
505         });
506     }
507 
508     protected void emitDeleteEvent(final int id) {
509         listeners.forEach(l -> {
510             try {
511                 l.onDelete(id);
512             } catch(Throwable t) {
513                 log.error("Person listener (onDelete) error: " + t.getMessage(), t);
514                 // Continue, to avoid transaction cancellation
515             }
516         });
517     }
518 
519     /**
520      * Create a translate map from software properties 'sumaris.userProfile.<ENUM>.label' (<ENUM> is one of the UserProfileEnum name)
521      *
522      * @param toVO if true, the map key is the translated label, the value is the enum label; if false, it's inverted
523      * @return the translation map
524      */
525     private Map<String, String> getUserProfileLabelTranslationMap(boolean toVO) {
526         Map<String, String> translateMap = new HashMap<>();
527         Pattern pattern = Pattern.compile("sumaris.userProfile.(\\w+).label");
528         softwareDao.get(config.getAppName()).getProperties().forEach((key, value) -> {
529             Matcher matcher = pattern.matcher(key);
530             if (value != null && matcher.find()) {
531                 if (toVO)
532                     translateMap.put(value, matcher.group(1));
533                 else
534                     translateMap.put(matcher.group(1), value);
535             }
536         });
537         return translateMap;
538     }
539 }