/*
* Copyright 2012 Nodeable 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 com.streamreduce.core.service;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.mongodb.BasicDBObject;
import com.mongodb.DB;
import com.mongodb.DBCollection;
import com.streamreduce.Constants;
import com.streamreduce.InvalidUserAliasException;
import com.streamreduce.core.dao.AccountDAO;
import com.streamreduce.core.dao.DAODatasourceType;
import com.streamreduce.core.dao.EventLogDAO;
import com.streamreduce.core.dao.GenericCollectionDAO;
import com.streamreduce.core.dao.RoleDAO;
import com.streamreduce.core.dao.UserDAO;
import com.streamreduce.core.event.EventId;
import com.streamreduce.core.model.Account;
import com.streamreduce.core.model.Event;
import com.streamreduce.core.model.EventLog;
import com.streamreduce.core.model.Role;
import com.streamreduce.core.model.User;
import com.streamreduce.core.service.exception.AccountNotFoundException;
import com.streamreduce.core.service.exception.UserNotFoundException;
import com.streamreduce.core.service.exception.UsernameUnavailableException;
import com.streamreduce.security.Roles;
import com.streamreduce.util.MessageUtils;
import net.sf.json.JSONObject;
import org.bson.types.ObjectId;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
@Service("userService")
public class UserServiceImpl extends AbstractService implements UserService {
@Autowired
private UserDAO userDAO;
@Autowired
private RoleDAO roleDAO;
@Autowired
private AccountDAO accountDAO;
@Autowired
private EventLogDAO eventLogDAO;
@Autowired
private GenericCollectionDAO genericCollectionDAO;
@Autowired
private SearchService searchService;
@Autowired
private EventService eventService;
@Autowired
private MessageService messageService;
private Cache<String, User> superUserCache =
CacheBuilder.newBuilder().maximumSize(1).expireAfterAccess(5, TimeUnit.MINUTES).build();
@Override
public boolean isUsernameAvailable(String name) {
User u = userDAO.findUser(name);
return u == null;
}
@Override
public boolean isAliasAvailable(Account account, String alias) {
return userDAO.findUserForAlias(account, alias) == null;
}
@Override
public Account getAccount(ObjectId accountId) throws AccountNotFoundException {
if (accountId == null) {
throw new AccountNotFoundException("Passed account ID was null.");
}
Account a = accountDAO.get(accountId);
if (a == null) {
throw new AccountNotFoundException(accountId.toString());
}
// Create the event stream entry
eventService.createEvent(EventId.READ, a, null);
return a;
}
@Override
public Set<Role> getAccountRoles(ObjectId accountId) throws AccountNotFoundException {
return roleDAO.findAccountRoles(accountId);
}
@Override
public User getUserById(ObjectId userId) throws UserNotFoundException {
User u = userDAO.get(userId);
if (u == null) {
throw new UserNotFoundException(userId.toString());
}
// Create the event stream entry
eventService.createEvent(EventId.READ, u, null);
return u;
}
public List<User> getUsersById(Set<ObjectId> userIds) {
List<User> userList = new ArrayList<>();
for (ObjectId id : userIds) {
try {
userList.add(getUserById(id));
} catch (UserNotFoundException e) {
// whatever...
}
}
return userList;
}
@Override
public User getUserById(ObjectId userId, Account account) throws UserNotFoundException {
User u = userDAO.get(userId);
User user = userDAO.findUserForUsername(account, u.getUsername());
if (user == null) {
throw new UserNotFoundException(u.getUsername());
}
// Create the event stream entry
eventService.createEvent(EventId.READ, u, null);
return user;
}
@Override
public User getUserByAuthenticationToken(String authToken) throws UserNotFoundException {
User user = userDAO.findByAuthToken(authToken);
if (user == null) {
throw new UserNotFoundException(authToken);
}
// Create the event stream entry
eventService.createEvent(EventId.READ, user, null);
return user;
}
@Override
public User getUser(String username, Account account) throws UserNotFoundException {
User u = userDAO.findUserForUsername(account, username);
if (u == null) {
throw new UserNotFoundException(username);
}
// Create the event stream entry
eventService.createEvent(EventId.READ, u, null);
return u;
}
@Override
public User getUser(String username) throws UserNotFoundException {
User u = userDAO.findUser(username);
if (u == null) {
throw new UserNotFoundException(username);
}
// Create the event stream entry
eventService.createEvent(EventId.READ, u, null);
return u;
}
@Override
public User getTargetUser(Account account, String name) throws UserNotFoundException {
User u = userDAO.findUserInAccount(account, name);
if (u == null) {
throw new UserNotFoundException(name);
}
// Create the event stream entry
eventService.createEvent(EventId.READ, u, null);
return u;
}
/**
* Create an account. Also assign the default product
*
* @param account - properly formed account to create
* @return - the account with the accountId populated
*/
@Override
public Account createAccount(Account account) {
accountDAO.save(account);
// bootstrap the metric inbox
createMetricInbox(account);
// once the inbox has been created, start a river so that we can search on it
searchService.createRiverForAccount(account);
// Bootstrap sample messages
messageService.copyArchivedMessagesToInbox(account);
// Create the event stream entry
eventService.createEvent(EventId.CREATE, account, null);
return account;
}
@Override
public void updateAccount(Account account) {
accountDAO.save(account);
// Create the event stream entry
eventService.createEvent(EventId.UPDATE, account, null);
}
@Override
public void updateUser(User user) {
validateUserAlias(user);
userDAO.save(user);
// Create the event stream entry
eventService.createEvent(EventId.UPDATE, user, null);
}
@Override
public void resetUserPassword(User user, boolean mobile) {
userDAO.save(user);
// Create the event stream entry
eventService.createEvent(EventId.USER_PASSWORD_RESET_REQUEST, user, null);
}
@Override
public void deleteAccount(ObjectId accountId) {
try {
// you can't delete the super user account
User superUser = getSuperUser();
if (superUser.getAccount().getId().equals(accountId)) {
logger.error("You can not delete the Nodeable account");
return;
}
Account toBeDeleted = getAccount(accountId);
// Create the event stream entry
eventService.createEvent(EventId.DELETE, toBeDeleted, null);
accountDAO.deleteById(accountId);
} catch (AccountNotFoundException anf) {
logger.error("AccountNotFoundException", anf);
}
}
@Override
public void deleteUser(User user) {
// you can't delete the super user
if (user.getUsername().equals(Constants.NODEABLE_SUPER_USERNAME)) {
logger.error("You can not delete the Nodebelly user");
return;
}
// Create the event stream entry
Event event = eventService.createEvent(EventId.DELETE, user, null);
if (user.getAccount() != null) {
// Send a user is being deleted message
messageService.sendNodeableAccountMessage(event, user.getAccount(), null);
}
// TODO: remove resoruces they own?
userDAO.deleteById(user.getId());
}
@Override
public User getUserFromInvite(String inviteKey, String accountId) throws UserNotFoundException {
User u = userDAO.findInvitedUser(inviteKey, accountId);
if (u == null) {
throw new UserNotFoundException(inviteKey + "+" + accountId);
}
// Create the event stream entry
eventService.createEvent(EventId.READ, u, null);
return u;
}
@Override
public User getUserFromSignupKey(String signupKey, String userId) throws UserNotFoundException {
User u = userDAO.findUser(signupKey, userId);
if (u == null) {
throw new UserNotFoundException(signupKey + "+" + userId);
}
// Create the event stream entry
eventService.createEvent(EventId.READ, u, null);
return u;
}
/**
* User is already reserved via the #createUserRequest or #createInviteUserRequest calls,
* this just finishes by firing the event and saving the user again if any changes were made
*
* @param user
* @return
*/
@Override
public User createUser(User user) {
user.setUserStatus(User.UserStatus.ACTIVATED);
validateUserAlias(user);
userDAO.save(user);
// Create the event stream entry
Event event = eventService.createEvent(EventId.CREATE, user, null);
// Create a user created message
messageService.sendNodeableAccountMessage(event, user.getAccount(), null);
return user;
}
/**
* A new user has requested to sign up. This also sets the AccountOriginator value to true.
*
* @param user
* @return
* @throws UsernameUnavailableException
*/
@Override
public User createUserRequest(User user) throws UsernameUnavailableException {
if (isUsernameAvailable(user.getUsername())) {
user.setAccountOriginator(true);
validateUserAlias(user);
userDAO.save(user);
} else {
throw new UsernameUnavailableException(user.getUsername());
}
handleUserRequest(user, false);
return user;
}
@Override
public void recreateUserRequest(User user) throws UserNotFoundException {
handleUserRequest(user, true);
}
private void handleUserRequest(User user, boolean isRecreate) {
Map<String, Object> metadata = new HashMap<>();
metadata.put("userRequestIsNew", !isRecreate);
// Create the event stream entry
eventService.createEvent(EventId.CREATE_USER_REQUEST, user, metadata);
}
@Override
public void deleteUserInvite(User user) throws UserNotFoundException {
userDAO.delete(user);
// Create the event stream entry
eventService.createEvent(EventId.DELETE_USER_INVITE_REQUEST, user, null);
}
@Override
public void deleteUsersForAccount(Account account) {
for (User user : allUsersForAccount(account)) {
deleteUser(user);
}
}
@Override
public List<User> allUsersForAccount(Account account) {
return userDAO.forAccount(account);
}
@Override
/**
* SOBA-1401 Does not include disabled users (this is our psuedo "deleted" status)
*/
public List<User> allEnabledUsersForAccount(Account account) {
return userDAO.allEnabledUsersForAccount(account);
}
@Override
public Set<Role> getUserRoles() {
Set<Role> roles = new HashSet<>();
Role r = roleDAO.findRole(Roles.USER_ROLE);
roles.add(r);
return roles;
}
@Override
public Set<Role> getAdminRoles() {
Set<Role> roles = new HashSet<>();
Role r = roleDAO.findRole(Roles.ADMIN_ROLE);
roles.add(r);
r = roleDAO.findRole(Roles.USER_ROLE);
roles.add(r);
return roles;
}
@Override
public List<Account> getAccounts() {
return accountDAO.find().asList();
}
@Override
public void addRole(User user, ObjectId roleId) throws UserNotFoundException {
Role role = roleDAO.get(roleId);
// don't allow dupes if the same Id
if (!(user.getRoles().contains(role))) {
user.addRole(role);
updateUser(user);
}
}
@Override
public void removeRole(User user, ObjectId roleId) throws UserNotFoundException {
Set<Role> newRoles = new HashSet<>();
Set<Role> roleSet = user.getRoles();
for (Role r : roleSet) {
if (!(r.getId().equals(roleId))) {
newRoles.add(r);
}
}
user.setRoles(newRoles);
updateUser(user);
}
@Override
public ObjectId addToEventLog(User user, String name, JSONObject value, Long timestamp) {
EventLog eventLog = new EventLog.Builder()
.user(user)
.keyValue(name, value, timestamp)
.build();
eventLogDAO.save(eventLog);
return eventLog.getId();
// Fire event?
}
@Override
public void deletePendingUser(User user) {
userDAO.delete(user);
// do not fire event, this user has no resources
}
/**
* <p>Returns a copy of a User object representing the Nodeable SuperUser. A cached copy of the SuperUser user from
* the data tier is used as the base object from all copies. Periodically this cached copy is expired and retrieved
* again from the data tier.</p>
* <p/>
* <p>Callers of this method are allowed to modify the User object returned for purposes such as sending a message
* from the SuperUser into a specific account. However, instances of the SuperUser User should not be saved with
* {@link UserService#updateUser(com.streamreduce.core.model.User)} unless the intention is to modify the authoritative
* persisted SuperUser user.</p>
*
* @return A copy of the SuperUser object
*/
@Override
public User getSuperUser() {
try {
User cachedSuperUser = superUserCache.get("superuser", new Callable<User>() {
@Override
public User call() throws Exception {
return userDAO.findUser(Constants.NODEABLE_SUPER_USERNAME);
}
});
return new User.Builder(cachedSuperUser).build();
} catch (ExecutionException e) {
logger.warn("Exception encountered when accessing superUser from cache. Falling back to UserDAO", e);
return userDAO.findUser(Constants.NODEABLE_SUPER_USERNAME);
}
}
@Override
public User getSystemUser() {
return userDAO.findUser(Constants.NODEABLE_SYSTEM_USERNAME);
}
@Override
public User getAccountAdmin(Account account) {
List<User> users = allUsersForAccount(account);
for (User user : users) {
for (Role role : user.getRoles()) {
if (role.getName().equals(Roles.ADMIN_ROLE)) {
return user;
}
}
}
return null;
}
/**
* SOBA-1617 -- bootstrap the Metric collections with proper indexes
*
* @param account - a valid account
*/
private void createMetricInbox(Account account) {
// create a bogus object
DB db = genericCollectionDAO.getDatabase(DAODatasourceType.MESSAGE);
DBCollection collection = db.getCollection(MessageUtils.getMetricInboxPath(account));
BasicDBObject dummyObj = new BasicDBObject();
collection.insert(dummyObj);
// add indexes
collection.ensureIndex("metricGranularity");
collection.ensureIndex("metricName");
// remove bogus object
collection.remove(dummyObj);
}
@Override
public void handleInitialInsightForAccount(Account account) {
account.setConfigValue(Account.ConfigKey.RECIEVED_INSIGHTS, true);
updateAccount(account);
}
/**
* Tests that a user alias is filled with only alphanumeric characters, "-", and "_"
*
* @param user - The user whose alias is being tested
* @throws IllegalArgumentException if user.getAlias contains characters other than alphanumeric characters, "_" and
* "-"
*/
void validateUserAlias(User user) {
if (!User.isValidUserAlias(user.getAlias())) {
throw new InvalidUserAliasException("User alias contains characters that aren't alphanumeric, dashes, or " +
"underscores");
}
}
}