/**
* Copyright (C) 2011 Brian Ferris <bdferris@onebusaway.org>
* Copyright (C) 2012 Google, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.onebusaway.users.impl;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import org.onebusaway.container.cache.Cacheable;
import org.onebusaway.container.cache.CacheableArgument;
import org.onebusaway.users.client.model.UserBean;
import org.onebusaway.users.client.model.UserIndexBean;
import org.onebusaway.users.model.User;
import org.onebusaway.users.model.UserIndex;
import org.onebusaway.users.model.UserIndexKey;
import org.onebusaway.users.model.UserPropertiesV1;
import org.onebusaway.users.model.UserRole;
import org.onebusaway.users.services.StandardAuthoritiesService;
import org.onebusaway.users.services.UserDao;
import org.onebusaway.users.services.UserIndexTypes;
import org.onebusaway.users.services.UserPropertiesMigration;
import org.onebusaway.users.services.UserPropertiesMigrationStatus;
import org.onebusaway.users.services.UserPropertiesService;
import org.onebusaway.users.services.UserService;
import org.onebusaway.users.services.internal.UserIndexRegistrationService;
import org.onebusaway.users.services.internal.UserRegistration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.providers.encoding.PasswordEncoder;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@Component
public class UserServiceImpl implements UserService {
private UserDao _userDao;
private StandardAuthoritiesService _authoritiesService;
private UserPropertiesMigration _userPropertiesMigration;
private UserPropertiesService _userPropertiesService;
private UserIndexRegistrationService _userIndexRegistrationService;
private PasswordEncoder _passwordEncoder;
private ExecutorService _executors;
private Object _deleteStaleUsersLock = new Object();
private Future<?> _deleteStaleUsersTask;
@Autowired
public void setUserDao(UserDao dao) {
_userDao = dao;
}
@Autowired
public void setAuthoritiesService(
StandardAuthoritiesService authoritiesService) {
_authoritiesService = authoritiesService;
}
@Autowired
public void setUserPropertiesService(
UserPropertiesService userPropertiesService) {
_userPropertiesService = userPropertiesService;
}
@Autowired
public void setUserIndexRegistrationService(
UserIndexRegistrationService userIndexRegistrationService) {
_userIndexRegistrationService = userIndexRegistrationService;
}
@Autowired
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
_passwordEncoder = passwordEncoder;
}
@PostConstruct
public void start() {
_executors = Executors.newSingleThreadExecutor();
}
@PreDestroy
public void stop() {
_executors.shutdownNow();
}
/****
* {@link UserService} Interface
****/
@Override
public int getNumberOfUsers() {
return _userDao.getNumberOfUsers();
}
@Override
public List<Integer> getAllUserIds() {
return _userDao.getAllUserIds();
}
@Override
public List<Integer> getAllUserIdsInRange(int offset, int limit) {
return _userDao.getAllUserIdsInRange(offset, limit);
}
@Override
public int getNumberOfAdmins() {
UserRole admin = _authoritiesService.getAdministratorRole();
return _userDao.getNumberOfUsersWithRole(admin);
}
@Override
public User getUserForId(int userId) {
return _userDao.getUserForId(userId);
}
@Override
public UserBean getUserAsBean(User user) {
UserBean bean = new UserBean();
bean.setUserId(Integer.toString(user.getId()));
UserRole anonymous = _authoritiesService.getAnonymousRole();
boolean isAnonymous = user.getRoles().contains(anonymous);
bean.setAnonymous(isAnonymous);
UserRole admin = _authoritiesService.getAdministratorRole();
boolean isAdmin = user.getRoles().contains(admin);
bean.setAdmin(isAdmin);
List<UserIndexBean> indices = new ArrayList<UserIndexBean>();
bean.setIndices(indices);
for (UserIndex index : user.getUserIndices()) {
UserIndexKey key = index.getId();
UserIndexBean indexBean = new UserIndexBean();
indexBean.setType(key.getType());
indexBean.setValue(key.getValue());
indices.add(indexBean);
}
_userPropertiesService.getUserAsBean(user, bean);
return bean;
}
@Override
public UserBean getAnonymousUser() {
UserBean bean = new UserBean();
bean.setUserId("-1");
bean.setAnonymous(true);
bean.setAdmin(false);
_userPropertiesService.getAnonymousUserAsBean(bean);
return bean;
}
@Override
public void resetUser(User user) {
_userPropertiesService.resetUser(user);
}
@Override
public void deleteUser(User user) {
_userDao.deleteUser(user);
}
@Override
public boolean isAnonymous(User user) {
return user.getRoles().contains(_authoritiesService.getAnonymousRole());
}
@Override
public boolean isAdministrator(User user) {
return user.getRoles().contains(_authoritiesService.getAdministratorRole());
}
@Override
public void enableAdminRoleForUser(User user, boolean onlyIfNoOtherAdmins) {
UserRole adminRole = _authoritiesService.getUserRoleForName(StandardAuthoritiesService.ADMINISTRATOR);
if (onlyIfNoOtherAdmins) {
int count = _userDao.getNumberOfUsersWithRole(adminRole);
if (count > 0)
return;
}
Set<UserRole> roles = user.getRoles();
if (roles.add(adminRole))
_userDao.saveOrUpdateUser(user);
}
public void disableAdminRoleForUser(User user, boolean onlyIfOtherAdmins) {
UserRole adminRole = _authoritiesService.getUserRoleForName(StandardAuthoritiesService.ADMINISTRATOR);
if (onlyIfOtherAdmins) {
int count = _userDao.getNumberOfUsersWithRole(adminRole);
if (count < 2)
return;
}
Set<UserRole> roles = user.getRoles();
if (roles.remove(adminRole))
_userDao.saveOrUpdateUser(user);
}
@Override
public List<String> getUserIndexKeyValuesForKeyType(String keyType) {
return _userDao.getUserIndexKeyValuesForKeyType(keyType);
}
@Override
public UserIndex getOrCreateUserForIndexKey(UserIndexKey key,
String credentials, boolean isAnonymous) {
UserIndex userIndex = _userDao.getUserIndexForId(key);
if (userIndex == null) {
User user = new User();
user.setCreationTime(new Date());
user.setTemporary(true);
user.setProperties(new UserPropertiesV1());
Set<UserRole> roles = new HashSet<UserRole>();
if (isAnonymous)
roles.add(_authoritiesService.getAnonymousRole());
else
roles.add(_authoritiesService.getUserRole());
user.setRoles(roles);
userIndex = new UserIndex();
userIndex.setId(key);
userIndex.setCredentials(credentials);
userIndex.setUser(user);
user.getUserIndices().add(userIndex);
_userDao.saveOrUpdateUser(user);
}
return userIndex;
}
@Override
public UserIndex getOrCreateUserForUsernameAndPassword(String username,
String password) {
String credentials = _passwordEncoder.encodePassword(password, username);
UserIndexKey key = new UserIndexKey(UserIndexTypes.USERNAME, username);
return getOrCreateUserForIndexKey(key, credentials, false);
}
@Override
public UserIndex getUserIndexForId(UserIndexKey key) {
return _userDao.getUserIndexForId(key);
}
@Override
public UserIndex addUserIndexToUser(User user, UserIndexKey key,
String credentials) {
UserIndex index = new UserIndex();
index.setId(key);
index.setCredentials(credentials);
index.setUser(user);
user.getUserIndices().add(index);
_userDao.saveOrUpdateUser(user);
return index;
}
@Override
public void removeUserIndexForUser(User user, UserIndexKey key) {
for (UserIndex index : user.getUserIndices()) {
if (index.getId().equals(key)) {
_userDao.deleteUserIndex(index);
index.setUser(null);
user.getUserIndices().remove(index);
_userDao.saveOrUpdateUser(user);
return;
}
}
}
@Override
public void setCredentialsForUserIndex(UserIndex userIndex, String credentials) {
userIndex.setCredentials(credentials);
_userDao.saveOrUpdateUserIndex(userIndex);
}
@Override
public void setPasswordForUsernameUserIndex(UserIndex userIndex,
String password) {
UserIndexKey id = userIndex.getId();
if (!UserIndexTypes.USERNAME.equals(id.getType()))
throw new IllegalArgumentException("expected UserIndex of type "
+ UserIndexTypes.USERNAME);
String credentials = _passwordEncoder.encodePassword(password,
id.getValue());
setCredentialsForUserIndex(userIndex, credentials);
}
@Override
public void mergeUsers(User sourceUser, User targetUser) {
if (sourceUser.equals(targetUser))
return;
if (sourceUser.getCreationTime().before(targetUser.getCreationTime()))
targetUser.setCreationTime(sourceUser.getCreationTime());
_userPropertiesService.mergeProperties(sourceUser, targetUser);
mergeRoles(sourceUser, targetUser);
List<UserIndex> indices = new ArrayList<UserIndex>(
sourceUser.getUserIndices());
for (UserIndex index : indices) {
index.setUser(targetUser);
targetUser.getUserIndices().add(index);
}
sourceUser.getUserIndices().clear();
_userDao.saveOrUpdateUsers(sourceUser, targetUser);
deleteUser(sourceUser);
}
@Override
public String registerPhoneNumber(User user, String phoneNumber) {
int code = (int) (Math.random() * 8999 + 1000);
String codeAsString = Integer.toString(code);
phoneNumber = PhoneNumberLibrary.normalizePhoneNumber(phoneNumber);
UserIndexKey key = new UserIndexKey(UserIndexTypes.PHONE_NUMBER,
phoneNumber);
_userIndexRegistrationService.setRegistrationForUserIndexKey(key,
user.getId(), codeAsString);
return codeAsString;
}
@Override
public boolean hasPhoneNumberRegistration(UserIndexKey userIndexKey) {
return _userIndexRegistrationService.hasRegistrationForUserIndexKey(userIndexKey);
}
@Override
public UserIndex completePhoneNumberRegistration(UserIndex userIndex,
String registrationCode) {
UserRegistration registration = _userIndexRegistrationService.getRegistrationForUserIndexKey(userIndex.getId());
if (registration == null)
return null;
String expectedCode = registration.getRegistrationCode();
if (!expectedCode.equals(registrationCode))
return null;
/**
* At this point, we have a valid registration code. We may safely clear the
* registration
*/
_userIndexRegistrationService.clearRegistrationForUserIndexKey(userIndex.getId());
User targetUser = _userDao.getUserForId(registration.getUserId());
if (targetUser == null)
return null;
User phoneUser = userIndex.getUser();
/**
* If the user index is already registered, our work is done
*/
if (phoneUser.equals(targetUser))
return userIndex;
/**
* If the phone user only has one index (the phoneNumber index), we merge
* the phone user with the registration target user. Otherwise, we keep the
* phone user, but just transfer the phone index
*/
if (phoneUser.getUserIndices().size() == 1) {
mergeUsers(userIndex.getUser(), targetUser);
} else {
userIndex.setUser(targetUser);
targetUser.getUserIndices().add(userIndex);
phoneUser.getUserIndices().remove(userIndex);
_userDao.saveOrUpdateUsers(phoneUser, targetUser);
}
// Refresh the user index
return getUserIndexForId(userIndex.getId());
}
@Override
public void clearPhoneNumberRegistration(UserIndexKey userIndexKey) {
_userIndexRegistrationService.clearRegistrationForUserIndexKey(userIndexKey);
}
@Override
public void startUserPropertiesMigration() {
_userPropertiesMigration.startUserPropertiesBulkMigration(_userPropertiesService.getUserPropertiesType());
}
@Override
public UserPropertiesMigrationStatus getUserPropertiesMigrationStatus() {
return _userPropertiesMigration.getUserPropertiesBulkMigrationStatus();
}
@Override
public void deleteStaleUsers() {
synchronized (_deleteStaleUsersLock) {
if (_deleteStaleUsersTask != null && !_deleteStaleUsersTask.isDone()) {
return;
}
Calendar c = Calendar.getInstance();
c.add(Calendar.MONTH, -1);
Date lastAccessTime = c.getTime();
_deleteStaleUsersTask = _executors.submit(new DeleteStaleUsersTask(
lastAccessTime));
}
}
@Override
public boolean isDeletingStaleUsers() {
synchronized (_deleteStaleUsersLock) {
return _deleteStaleUsersTask != null && !_deleteStaleUsersTask.isDone();
}
}
@Override
public void cancelDeleteStaleUsers() {
synchronized (_deleteStaleUsersLock) {
if (_deleteStaleUsersTask != null && !_deleteStaleUsersTask.isDone()) {
_deleteStaleUsersTask.cancel(true);
_deleteStaleUsersTask = null;
}
}
}
@Override
public long getNumberOfStaleUsers() {
Calendar c = Calendar.getInstance();
c.add(Calendar.MONTH, -1);
Date lastAccessTime = c.getTime();
return _userDao.getNumberOfStaleUsers(lastAccessTime);
}
/****
* Private Methods
****/
private void mergeRoles(User sourceUser, User targetUser) {
Set<UserRole> roles = new HashSet<UserRole>();
roles.addAll(sourceUser.getRoles());
roles.addAll(targetUser.getRoles());
UserRole anon = _authoritiesService.getAnonymousRole();
UserRole user = _authoritiesService.getUserRole();
if (roles.contains(user))
roles.remove(anon);
targetUser.setRoles(roles);
}
@Cacheable
@Transactional
@Override
public Long getMinApiRequestIntervalForKey(String key,
@CacheableArgument(cacheRefreshIndicator = true) boolean forceRefresh) {
UserIndexKey indexKey = new UserIndexKey(UserIndexTypes.API_KEY, key);
UserIndex userIndex = getUserIndexForId(indexKey);
if (userIndex == null) {
return null;
}
User user = userIndex.getUser();
UserBean bean = getUserAsBean(user);
return bean.getMinApiRequestInterval();
}
/**
* Unfortunately, deleting a user is a somewhat complex operation, so we can
* do it in bulk (TODO: maybe someone can figure out a clever cascading bulk
* delete that plays well with all the caches / etc).
*
* @param lastAccessTime
*/
private void deleteStaleUsers(Date lastAccessTime) {
while (true) {
List<Integer> userIds = _userDao.getStaleUserIdsInRange(lastAccessTime,
0, 100);
if (userIds.isEmpty()) {
return;
}
for (int userId : userIds) {
if (Thread.interrupted() ) {
return;
}
User user = _userDao.getUserForId(userId);
if (user != null) {
_userDao.deleteUser(user);
}
Thread.yield();
}
}
}
private class DeleteStaleUsersTask implements Runnable {
private Date _lastAccessTime;
public DeleteStaleUsersTask(Date lastAccessTime) {
_lastAccessTime = lastAccessTime;
}
@Override
public void run() {
deleteStaleUsers(_lastAccessTime);
}
}
}