1 package net.avcompris.examples.users3.core.impl;
2
3 import static com.google.common.base.Preconditions.checkNotNull;
4 import static net.avcompris.commons3.core.DateTimeHolderImpl.toDateTimeHolder;
5 import static net.avcompris.commons3.core.DateTimeHolderImpl.toDateTimeHolderOrNull;
6 import static net.avcompris.commons3.databeans.DataBeans.instantiate;
7 import static net.avcompris.examples.shared3.core.impl.Validations.assertNonSuperadminUsername;
8 import static net.avcompris.examples.shared3.core.impl.Validations.assertValidPassword;
9 import static net.avcompris.examples.shared3.core.impl.Validations.assertValidUsername;
10 import static org.apache.commons.lang3.StringUtils.isBlank;
11
12 import java.io.IOException;
13 import java.sql.SQLException;
14
15 import javax.annotation.Nullable;
16
17 import org.apache.commons.lang3.NotImplementedException;
18 import org.apache.commons.logging.Log;
19 import org.springframework.beans.factory.annotation.Autowired;
20 import org.springframework.stereotype.Service;
21
22 import net.avcompris.commons.query.FilterSyntaxException;
23 import net.avcompris.commons.query.impl.FilteringsFactory;
24 import net.avcompris.commons3.api.EnumRole;
25 import net.avcompris.commons3.api.User;
26 import net.avcompris.commons3.api.exception.DaoException;
27 import net.avcompris.commons3.api.exception.InvalidQueryFilteringException;
28 import net.avcompris.commons3.api.exception.InvalidRoleException;
29 import net.avcompris.commons3.api.exception.ServiceException;
30 import net.avcompris.commons3.api.exception.UnauthorizedException;
31 import net.avcompris.commons3.api.exception.UsernameAlreadyExistsException;
32 import net.avcompris.commons3.api.exception.UsernameNotFoundException;
33 import net.avcompris.commons3.core.Permissions;
34 import net.avcompris.commons3.core.impl.AbstractServiceImpl;
35 import net.avcompris.commons3.core.impl.SortBysParser;
36 import net.avcompris.commons3.dao.exception.DuplicateEntityException;
37 import net.avcompris.commons3.utils.Clock;
38 import net.avcompris.commons3.utils.LogFactory;
39 import net.avcompris.examples.shared3.Role;
40 import net.avcompris.examples.users3.api.UserCreate;
41 import net.avcompris.examples.users3.api.UserInfo;
42 import net.avcompris.examples.users3.api.UserUpdate;
43 import net.avcompris.examples.users3.api.UsersInfo;
44 import net.avcompris.examples.users3.api.UsersQuery;
45 import net.avcompris.examples.users3.api.UsersQuery.SortBy;
46 import net.avcompris.examples.users3.core.api.MutableUserInfo;
47 import net.avcompris.examples.users3.core.api.MutableUsersInfo;
48 import net.avcompris.examples.users3.core.api.UsersService;
49 import net.avcompris.examples.users3.dao.AuthDao;
50 import net.avcompris.examples.users3.dao.UserDto;
51 import net.avcompris.examples.users3.dao.UsersDao;
52 import net.avcompris.examples.users3.dao.UsersDto;
53 import net.avcompris.examples.users3.dao.UsersDtoQuery;
54 import net.avcompris.examples.users3.query.UserFiltering;
55 import net.avcompris.examples.users3.query.UserFilterings;
56
57 @Service
58 public final class UsersServiceImpl extends AbstractServiceImpl implements UsersService {
59
60 private static final Log logger = LogFactory.getLog(UsersServiceImpl.class);
61
62 private static final UsersDtoQuery.SortBy[] DEFAULT_SORT_BYS = new UsersDtoQuery.SortBy[] {
63 UsersDtoQuery.SortBy.SORT_BY_USERNAME };
64
65 private static final UsersDtoQuery.Expand[] DEFAULT_EXPANDS = new UsersDtoQuery.Expand[] {
66 UsersDtoQuery.Expand.EXPAND_ALL };
67
68 private final AuthDao authDao;
69 private final UsersDao usersDao;
70
71 private static final SortBysParser<SortBy> SORT_BYS_PARSER = new SortBysParser<SortBy>(SortBy.class,
72 SortBy.values());
73
74 @Autowired
75 public UsersServiceImpl(final Permissions permissions, final Clock clock,
76 final UsersDao usersDao,
77 final AuthDao authDao) {
78
79 super(permissions, clock);
80
81 this.authDao = checkNotNull(authDao, "authDao");
82 this.usersDao = checkNotNull(usersDao, "usersDao");
83 }
84
85 private static UsersDtoQuery.SortBy[] sortBys2Dto(final UsersQuery.SortBy... sortBys) {
86
87 if (sortBys == null || sortBys.length == 0) {
88 return DEFAULT_SORT_BYS;
89 }
90
91 final UsersDtoQuery.SortBy[] dto = new UsersDtoQuery.SortBy[sortBys.length];
92
93 for (int i = 0; i < sortBys.length; ++i) {
94 dto[i] = sortBy2Dto(sortBys[i]);
95 }
96
97 return dto;
98 }
99
100 private static UsersDtoQuery.SortBy sortBy2Dto(final UsersQuery.SortBy sortBy) {
101
102 checkNotNull(sortBy, "sortBy");
103
104 switch (sortBy) {
105 case SORT_BY_USERNAME:
106 return UsersDtoQuery.SortBy.SORT_BY_USERNAME;
107 case SORT_BY_USERNAME_DESC:
108 return UsersDtoQuery.SortBy.SORT_BY_USERNAME_DESC;
109 case SORT_BY_ROLE:
110 return UsersDtoQuery.SortBy.SORT_BY_ROLENAME;
111 case SORT_BY_ROLE_DESC:
112 return UsersDtoQuery.SortBy.SORT_BY_ROLENAME_DESC;
113 case SORT_BY_ENABLED:
114 return UsersDtoQuery.SortBy.SORT_BY_ENABLED;
115 case SORT_BY_ENABLED_DESC:
116 return UsersDtoQuery.SortBy.SORT_BY_ENABLED_DESC;
117 case SORT_BY_CREATED_AT:
118 return UsersDtoQuery.SortBy.SORT_BY_CREATED_AT;
119 case SORT_BY_CREATED_AT_DESC:
120 return UsersDtoQuery.SortBy.SORT_BY_CREATED_AT_DESC;
121 case SORT_BY_UPDATED_AT:
122 return UsersDtoQuery.SortBy.SORT_BY_UPDATED_AT;
123 case SORT_BY_UPDATED_AT_DESC:
124 return UsersDtoQuery.SortBy.SORT_BY_UPDATED_AT_DESC;
125 case SORT_BY_LAST_ACTIVE_AT:
126 return UsersDtoQuery.SortBy.SORT_BY_LAST_ACTIVE_AT;
127 case SORT_BY_LAST_ACTIVE_AT_DESC:
128 return UsersDtoQuery.SortBy.SORT_BY_LAST_ACTIVE_AT_DESC;
129 case SORT_BY_REVISION:
130 return UsersDtoQuery.SortBy.SORT_BY_REVISION;
131 case SORT_BY_REVISION_DESC:
132 return UsersDtoQuery.SortBy.SORT_BY_REVISION_DESC;
133 default:
134 throw new NotImplementedException("sortBy: " + sortBy);
135 }
136 }
137
138 private static UsersDtoQuery.Expand[] expands2Dto(final UsersQuery.Expand... expands) {
139
140 if (expands == null || expands.length == 0) {
141 return DEFAULT_EXPANDS;
142 }
143
144 throw new NotImplementedException("");
145 }
146
147 @Override
148 public UsersInfo getUsers(final String correlationId, final User user, @Nullable final UsersQuery query)
149 throws ServiceException {
150
151 checkNotNull(correlationId, "correlationId");
152 checkNotNull(user, "user");
153
154 LogFactory.setCorrelationId(correlationId);
155
156
157
158 permissions.assertAuthorized(correlationId, user, "query", query);
159
160
161
162 return privateGetUsers(query);
163 }
164
165 private UsersInfo privateGetUsers(@Nullable final UsersQuery query) throws ServiceException {
166
167 final long startMs = System.currentTimeMillis();
168
169 final int start = getQueryStart(query, 0);
170 final int limit = getQueryLimit(query, 10);
171
172 final UsersDtoQuery.Expand[] expandDtos;
173
174 if (query == null) {
175 expandDtos = DEFAULT_EXPANDS;
176 } else {
177 expandDtos = expands2Dto(query.getExpands());
178 }
179
180 final UsersDtoQuery dtoQuery = instantiate(UsersDtoQuery.class)
181 .setFiltering(query != null ? query.getFiltering() : null)
182 .setSortBys(query != null ? sortBys2Dto(query.getSortBys()) : DEFAULT_SORT_BYS)
183 .setExpands(expandDtos)
184 .setStart(start)
185 .setLimit(limit);
186
187 final UsersDto usersDto = wrap(()
188
189 -> usersDao.getUsers(dtoQuery));
190
191 final MutableUsersInfo result = instantiate(MutableUsersInfo.class)
192 .setStart(start)
193 .setLimit(limit)
194 .setSize(usersDto.getResults().length)
195 .setTotal(usersDto.getTotal())
196 .setSqlWhereClause(usersDto.getSqlWhereClause());
197
198 for (final UserDto userDto : usersDto.getResults()) {
199
200 result.addToResults(dto2UserInfo(userDto));
201 }
202
203 final int tookMs = (int) (System.currentTimeMillis() - startMs);
204
205 return result.setTookMs(tookMs);
206 }
207
208 private static UserInfo dto2UserInfo(final UserDto dto) {
209
210 checkNotNull(dto, "dto");
211
212 return instantiate(MutableUserInfo.class)
213 .setUsername(dto.getUsername())
214 .setRole(Role.valueOf(dto.getRolename()))
215 .setPreferredLang(dto.getPreferredLang())
216 .setPreferredTimeZone(dto.getPreferredTimeZone())
217 .setCreatedAt(toDateTimeHolder(dto.getCreatedAt()))
218 .setUpdatedAt(toDateTimeHolder(dto.getUpdatedAt()))
219 .setRevision(dto.getRevision())
220 .setLastActiveAt(toDateTimeHolderOrNull(dto.getLastActiveAt()))
221 .setEnabled(dto.isEnabled());
222 }
223
224 @Override
225 public UserInfo createUser(final String correlationId, final User user, final String username,
226 final UserCreate create) throws ServiceException {
227
228 checkNotNull(user, "user");
229 checkNotNull(correlationId, "correlationId");
230 checkNotNull(username, "username");
231 checkNotNull(create, "create");
232
233 LogFactory.setCorrelationId(correlationId);
234
235
236
237 permissions.assertAuthorized(correlationId, user, "create", create);
238
239
240
241 return privateCreateUser(correlationId, user, username, create);
242 }
243
244 private UserInfo privateCreateUser(final String correlationId, final User user, final String username,
245 final UserCreate create) throws ServiceException {
246
247 final String password = create.getPassword();
248
249 assertValidUsername(username);
250 assertNonSuperadminUsername(username);
251
252 if (password != null) {
253
254 assertValidPassword(password);
255 }
256
257 final Role role = create.getRole();
258
259 if (role == Role.ANONYMOUS || role == Role.SUPERADMIN) {
260
261 throw new InvalidRoleException("role: " + role);
262 }
263
264 assertRoleCanManageRole(user.getRole(), role);
265
266 try {
267
268 usersDao.createUser(
269 username,
270 role.getRolename(),
271 create.getPreferredLang(),
272 create.getPreferredTimeZone(),
273 create.isEnabled());
274
275 if (password == null) {
276
277 authDao.removeUserPassword(username);
278
279 } else {
280
281 authDao.setUserPassword(username, password);
282 }
283
284 } catch (final IOException | SQLException e) {
285
286 throw new DaoException(e);
287
288 } catch (final DuplicateEntityException e) {
289
290 throw new UsernameAlreadyExistsException(username, e);
291 }
292
293 return privateGetUser(username);
294 }
295
296 @Override
297 public UserInfo getUser(final String correlationId, final User user, final String username)
298 throws ServiceException {
299
300 checkNotNull(correlationId, "correlationId");
301 checkNotNull(user, "user");
302 checkNotNull(username, "username");
303
304 LogFactory.setCorrelationId(correlationId);
305
306
307
308 permissions.assertAuthorized(correlationId, user, "username", username);
309
310
311
312 return privateGetUser(username);
313 }
314
315 @Override
316 public boolean hasUser(final String correlationId, final User user, final String username) throws ServiceException {
317
318 checkNotNull(correlationId, "correlationId");
319 checkNotNull(user, "user");
320 checkNotNull(username, "username");
321
322 LogFactory.setCorrelationId(correlationId);
323
324
325
326 permissions.assertAuthorized(correlationId, user, "username", username);
327
328
329
330 assertValidUsername(username);
331
332 final UserDto dto = wrap(()
333
334 -> usersDao.getUser(username));
335
336 return dto != null;
337 }
338
339 private UserInfo privateGetUser(final String username) throws ServiceException {
340
341 checkNotNull(username, "username");
342
343 assertValidUsername(username);
344
345 final UserDto dto = wrap(()
346
347 -> usersDao.getUser(username));
348
349 if (dto == null) {
350
351 throw new UsernameNotFoundException(username);
352 }
353
354 return dto2UserInfo(dto);
355 }
356
357 @Override
358 public UserInfo getUserMe(final String correlationId, final User user) throws ServiceException {
359
360 checkNotNull(correlationId, "correlationId");
361 checkNotNull(user, "user");
362
363 LogFactory.setCorrelationId(correlationId);
364
365
366
367 permissions.assertAuthorized(correlationId, user);
368
369
370
371 return privateGetUser(user.getUsername());
372 }
373
374 @Override
375 @Nullable
376 public UsersQuery validateUsersQuery(final String correlationId, final User user, @Nullable final String q,
377 @Nullable final String sort, @Nullable final Integer start, @Nullable final Integer limit,
378 @Nullable final String expand) throws ServiceException {
379
380 checkNotNull(correlationId, "correlationId");
381 checkNotNull(user, "user");
382
383 LogFactory.setCorrelationId(correlationId);
384
385
386
387 permissions.assertAuthorized(correlationId, user);
388
389
390
391 if (isBlank(q) && isBlank(sort) && start == null && limit == null && isBlank(expand)) {
392 return null;
393 }
394
395 final UsersQuery query = instantiate(UsersQuery.class);
396
397 if (start != null) {
398
399 if (start < 0) {
400 throw new InvalidQueryFilteringException("Query.start should be >= 0, but was: " + start);
401 }
402
403 query.setStart(start);
404 }
405
406 if (limit != null) {
407
408 if (limit < 0) {
409 throw new InvalidQueryFilteringException("Query.limit should be >= 0, but was: " + limit);
410 }
411
412 query.setLimit(limit);
413 }
414
415 if (!isBlank(q)) {
416
417 if (q.length() > 1000) {
418 throw new InvalidQueryFilteringException(
419 "Query should be at most 1,000 characters, but was: " + q.length());
420 }
421
422 final UserFilterings filterings = FilteringsFactory.instantiate(UserFilterings.class);
423
424 final UserFiltering filtering;
425
426 try {
427
428 filtering = filterings.parse(q);
429
430 } catch (final FilterSyntaxException e) {
431
432 throw new InvalidQueryFilteringException(e);
433 }
434
435 query.setFiltering(filtering);
436 }
437
438 if (!isBlank(sort)) {
439
440 query.setSortBys(SORT_BYS_PARSER.parse(sort));
441 }
442
443 if (!isBlank(expand)) {
444
445 throw new NotImplementedException("expand: " + expand);
446 }
447
448 return query;
449 }
450
451 @Override
452 public UserInfo updateUserMe(final String correlationId, final User user, final UserUpdate update)
453 throws ServiceException {
454
455 checkNotNull(correlationId, "correlationId");
456 checkNotNull(user, "user");
457 checkNotNull(update, "update");
458
459 LogFactory.setCorrelationId(correlationId);
460
461
462
463 permissions.assertAuthorized(correlationId, user);
464
465
466
467 final String username = user.getUsername();
468
469 if (logger.isInfoEnabled()) {
470 logger.info("updateUserMe(): username: " + username + ", " + update + "...");
471 }
472
473 @Nullable
474 final String password = update.getPassword();
475
476 final UserDto toDto = wrap(() -> {
477
478 final Role role = extractUpdateRole(update, () ->
479 getUserMe(correlationId, user).getRole());
480
481 final boolean enabled = extractUpdateEnabled(update, () ->
482 usersDao.getUser(username).isEnabled());
483
484 usersDao.updateUser(username,
485 role.getRolename(),
486 update.getPreferredLang(),
487 update.getPreferredTimeZone(),
488 enabled,
489 update.getFromRevision());
490
491 if (password != null) {
492
493 authDao.setUserPassword(username, password);
494 }
495
496 return usersDao.getUser(username);
497
498 });
499
500 if (logger.isInfoEnabled()) {
501 logger.info("updateUserMe(): Ending.");
502 }
503
504 return dto2UserInfo(toDto);
505 }
506
507 private Role extractUpdateRole(final UserUpdate update, final Action<Role> action)
508 throws IOException, SQLException, ServiceException {
509
510 final Role role;
511
512 if (update.getRole() != null) {
513
514 role = update.getRole();
515
516 } else {
517
518 role = action.execute();
519 }
520
521 if (role == Role.ANONYMOUS || role == Role.SUPERADMIN) {
522
523 throw new InvalidRoleException("role: " + role);
524 }
525
526 return role;
527 }
528
529 private boolean extractUpdateEnabled(final UserUpdate update, final Action<Boolean> action)
530 throws IOException, SQLException, ServiceException {
531
532 if (update.isEnabled() != null) {
533
534 return update.isEnabled();
535 }
536
537 return action.execute();
538 }
539
540 @Override
541 public UserInfo updateUser(final String correlationId, final User user, final String username,
542 final UserUpdate update) throws ServiceException {
543
544 checkNotNull(correlationId, "correlationId");
545 checkNotNull(user, "user");
546 checkNotNull(username, "username");
547 checkNotNull(update, "update");
548
549 LogFactory.setCorrelationId(correlationId);
550
551
552
553 permissions.assertAuthorized(correlationId, user);
554
555
556
557 if (logger.isInfoEnabled()) {
558 logger.info("updateUser(): " + update + "...");
559 }
560
561 final UserInfo updated = privateUpdateUser(correlationId, user, username, update);
562
563 if (logger.isInfoEnabled()) {
564 logger.info("updateUser(): Ending.");
565 }
566
567 return updated;
568 }
569
570 private static void assertRoleCanManageRole(final EnumRole manager, final Role managed)
571 throws UnauthorizedException {
572
573 checkNotNull(manager, "manager");
574 checkNotNull(managed, "managed");
575
576 if (!((Role) manager).canManage(managed)) {
577
578 throw new UnauthorizedException(
579 "Role: " + manager.getRolename() + " cannot manage role: " + managed.getRolename());
580 }
581 }
582
583 public UserInfo privateUpdateUser(final String correlationId, final User user, final String username,
584 final UserUpdate update) throws ServiceException {
585
586 @Nullable
587 final String password = update.getPassword();
588
589 final Role oldRole = privateGetUser(username).getRole();
590
591 assertRoleCanManageRole(user.getRole(), oldRole);
592
593 final Role role = wrap(() -> extractUpdateRole(update, () -> oldRole));
594
595 assertRoleCanManageRole(user.getRole(), role);
596
597 final boolean enabled = wrap(() -> extractUpdateEnabled(update, () ->
598 usersDao.getUser(username).isEnabled()));
599
600 final UserDto toDto = wrap(() -> {
601
602 usersDao.updateUser(username,
603 role.getRolename(),
604 update.getPreferredLang(),
605 update.getPreferredTimeZone(),
606 enabled,
607 update.getFromRevision());
608
609 if (password != null) {
610
611 authDao.setUserPassword(username, password);
612 }
613
614 return usersDao.getUser(username);
615
616 });
617
618 return dto2UserInfo(toDto);
619 }
620
621 @Override
622 public void deleteUser(final String correlationId, final User user, final String username) throws ServiceException {
623
624 checkNotNull(correlationId, "correlationId");
625 checkNotNull(user, "user");
626 checkNotNull(username, "username");
627
628 LogFactory.setCorrelationId(correlationId);
629
630
631
632 permissions.assertAuthorized(correlationId, user);
633
634
635
636 privateDeleteUser(user, username);
637 }
638
639 private void privateDeleteUser(final User user, final String username) throws ServiceException {
640
641
642
643
644
645
646
647 assertNonSuperadminUsername(username);
648
649 if (user.getUsername().contentEquals(username)) {
650 throw new UnauthorizedException("A user may not delete itself: " + username);
651 }
652
653 wrap(()
654
655 -> usersDao.deleteUser(username));
656 }
657 }