/*
 * Data Hub Service (DHuS) - For Space data distribution.
 * Copyright (C) 2013,2014,2015 GAEL Systems
 *
 * This file is part of DHuS software sources.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */
package fr.gael.dhus.service;

import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.hibernate.Criteria;
import org.hibernate.FetchMode;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Projections;
import org.quartz.SchedulerException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.crypto.codec.Hex;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import com.google.gson.ExclusionStrategy;
import com.google.gson.FieldAttributes;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

import fr.gael.dhus.database.dao.AccessRestrictionDao;
import fr.gael.dhus.database.dao.CollectionDao;
import fr.gael.dhus.database.dao.CountryDao;
import fr.gael.dhus.database.dao.ProductDao;
import fr.gael.dhus.database.dao.SearchDao;
import fr.gael.dhus.database.dao.UserDao;
import fr.gael.dhus.database.object.Collection;
import fr.gael.dhus.database.object.Country;
import fr.gael.dhus.database.object.Product;
import fr.gael.dhus.database.object.Role;
import fr.gael.dhus.database.object.Search;
import fr.gael.dhus.database.object.User;
import fr.gael.dhus.database.object.User.PasswordEncryption;
import fr.gael.dhus.database.object.restriction.AccessRestriction;
import fr.gael.dhus.messaging.mail.MailServer;
import fr.gael.dhus.service.exception.EmailNotSentException;
import fr.gael.dhus.service.exception.MalformedEmailException;
import fr.gael.dhus.service.exception.ProductNotExistingException;
import fr.gael.dhus.service.exception.RequiredFieldMissingException;
import fr.gael.dhus.service.exception.RootNotModifiableException;
import fr.gael.dhus.service.exception.UserBadEncryptionException;
import fr.gael.dhus.service.exception.UserBadOldPasswordException;
import fr.gael.dhus.service.exception.UserNotExistingException;
import fr.gael.dhus.service.exception.UsernameBadCharacterException;
import fr.gael.dhus.service.job.JobScheduler;
import fr.gael.dhus.spring.context.SecurityContextProvider;
import fr.gael.dhus.system.config.ConfigurationManager;

/**
 * User Service provides connected clients with a set of method to interact with
 * it.
 */
@Service
public class UserService extends WebService
{
   private static final Logger LOGGER = LogManager.getLogger(UserService.class);

   @Autowired
   private SearchDao searchDao;

   @Autowired
   private CountryDao countryDao;

   @Autowired
   private UserDao userDao;

   @Autowired
   private CollectionDao collectionDao;

   @Autowired
   private ProductDao productDao;

   @Autowired
   private AccessRestrictionDao accessRestrictionDao;

   @Autowired
   private ConfigurationManager cfgManager;

   @Autowired
   private MailServer mailer;

   @Autowired
   private JobScheduler scheduler;

   @Autowired
   private SecurityService securityService;

   @Autowired
   private CacheManager cacheManager;

   /**
    * Pattern for username checking
    */
   private static Pattern USERNAME_PATTERN = Pattern.compile ("^[a-zA-Z0-9\\._\\-]+$");

   /**
    * Pattern for email checking
    * Note: This pattern contains all the possible characters in an e-mail.
    * DHuS shall restrict these mail characters to enhance mailing security...
    * As far as mail servers already avoid a large part of possible mailing
    * hacks, no security breach is expected even if all the character are
    * authorized in DHuS...
    */
   private static Pattern EMAIL_PATTERN = Pattern.compile (
      "^[a-zA-Z0-9!#$%\\x26'*+/=?^_`{|}~-]+" +
      "(?:\\.[a-zA-Z0-9!#$%\\x26'*+/=?^_`{|}~-]+)*" +
      "@" +
      "(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\\.)+"  +
      "[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$");

   /**
    * Return user corresponding to given id.
    *
    * @param id User id.
    * @throws RootNotModifiableException
    */
   @PreAuthorize ("hasAnyRole('ROLE_USER_MANAGER','ROLE_DATA_MANAGER','ROLE_SYSTEM_MANAGER')")
   @Transactional (readOnly=true, propagation=Propagation.REQUIRED)
   @Cacheable (value = "user", key = "#id")
   public User getUser (String id) throws RootNotModifiableException
   {
      User u = userDao.read (id);
      checkRoot (u);
      return u;
   }

   @Transactional (readOnly=true, propagation=Propagation.REQUIRED)
   public User getUserNoCache (String id)
   {
      User u = userDao.read (id);
      return u;
   }

   /**
    * Return user corresponding to given user name.
    *
    * @param name User name.
    * @throws RootNotModifiableException
    */
   @PreAuthorize ("hasAnyRole('ROLE_USER_MANAGER','ROLE_DATA_MANAGER','ROLE_SYSTEM_MANAGER')")
   @Transactional (readOnly=true, propagation=Propagation.REQUIRED)
   @Cacheable (value = "userByName", key = "#name?.toLowerCase()")
   public User getUserByName (String name) throws RootNotModifiableException
   {
      User u = this.getUserNoCheck (name);
      checkRoot (u);
      return u;
   }

   @Transactional (readOnly=true, propagation=Propagation.REQUIRED)
   @Cacheable (value = "userByName", key = "#name?.toLowerCase()")
   public User getUserNoCheck (String name)
   {
      return userDao.getByName (name);
   }

   /**
    * Get all users corresponding to given filter.
    *
    * @param filter
    * @return All users corresponding to given filter.
    */
   @PreAuthorize ("hasAnyRole('ROLE_USER_MANAGER','ROLE_SYSTEM_MANAGER')")
   @Transactional (readOnly=true, propagation=Propagation.REQUIRED)
   public Iterator<User> getUsers (String filter, int skip)
   {
      return userDao.scrollNotDeleted (filter, skip);
   }

   /**
    * Retrieves corresponding users at the given criteria.
    *
    * @param criteria criteria contains filter and order of required collection.
    * @param skip     number of skipped valid results.
    * @param top      max of valid results.
    * @return a list of {@link User}
    */
   @Transactional(readOnly = true)
   public List<User> getUsers (DetachedCriteria criteria, int skip, int top)
   {
      if (criteria == null)
      {
         criteria = DetachedCriteria.forClass (User.class);
      }
      criteria.setFetchMode("roles", FetchMode.SELECT);
      criteria.setFetchMode("restrictions", FetchMode.SELECT);
      List<User> result = userDao.listCriteria (criteria, skip, top);
      return result;
   }

   /**
    * Counts corresponding users at the given criteria.
    *
    * @param criteria criteria contains filter of required collection.
    * @return number of corresponding users.
    */
   @Transactional(readOnly = true)
   public int countUsers (DetachedCriteria criteria)
   {
      if (criteria == null)
      {
         criteria = DetachedCriteria.forClass (User.class);
      }
      criteria.setResultTransformer (Criteria.DISTINCT_ROOT_ENTITY);
      criteria.setProjection (Projections.rowCount ());
      return userDao.count (criteria);
   }

   @PreAuthorize ("hasRole('ROLE_STATS')")
   @Transactional (readOnly=true, propagation=Propagation.REQUIRED)
   public Iterator<User> getAllUsers (String filter, int skip)
   {
      return userDao.scrollAll (filter, skip);
   }

   @PreAuthorize ("hasRole('ROLE_USER_MANAGER')")
   @Transactional (readOnly=true, propagation=Propagation.REQUIRED)
   public Iterator<User> getUsersForDataRight (String filter, int skip)
   {
      return userDao.scrollForDataRight (filter, skip);
   }

   /**
    * Create given User, after checking required fields.
    *
    * @param user
    * @throws RequiredFieldMissingException
    * @throws RootNotModifiableException
    */
   @PreAuthorize ("hasRole('ROLE_USER_MANAGER')")
   @Transactional (readOnly=false, propagation=Propagation.REQUIRED)
   public void createUser (User user) throws RequiredFieldMissingException,
      RootNotModifiableException, EmailNotSentException
   {
      systemCreateUser(user);
   }

   /**
    * Create given User, after checking required fields.
    * No @PreAuthorize.
    *
    * @param user
    * @throws RequiredFieldMissingException
    * @throws RootNotModifiableException
    */
   @Transactional(readOnly=false)
   @CacheEvict(value = "userByName", key = "#user?.getUsername().toLowerCase()")
   public void systemCreateUser(User user) throws RequiredFieldMissingException,
      RootNotModifiableException, EmailNotSentException
   {
      checkRequiredFields(user);
      checkRoot(user);
      userDao.create(user);
   }

   /**
    * Create given User as temporary User, after checking required fields.
    *
    * @param user
    * @throws RequiredFieldMissingException
    * @throws RootNotModifiableException
    */
   @Transactional (readOnly=false, propagation=Propagation.REQUIRED)
   public void createTmpUser (User user) throws RequiredFieldMissingException,
      RootNotModifiableException, EmailNotSentException
   {
      checkRequiredFields (user);
      checkRoot (user);
      userDao.createTmpUser (user);
   }

   @Transactional (readOnly=true, propagation=Propagation.REQUIRED)
   public Country getCountry (long id)
   {
      return countryDao.read (id);
   }

   @Transactional (readOnly=false, propagation=Propagation.REQUIRED)
   public void validateTmpUser (String code)
   {
      User u = userDao.getUserFromUserCode (code);
      if (u != null && userDao.isTmpUser (u))
      {
         userDao.registerTmpUser (u);

         // update cache entries
         Cache cache = cacheManager.getCache("user");
         if (cache != null)
         {
            synchronized (cache)
            {
               if (cache.get(u.getUUID()) != null)
               {
                  cache.put(u.getUUID(), u);
               }
            }
         }

         cache = cacheManager.getCache("userByName");
         if (cache != null)
         {
            synchronized (cache)
            {
               if (cache.get(u.getUsername()) != null)
               {
                  cache.put(u.getUsername(), u);
               }
            }
         }
      }
   }

   @Transactional (readOnly=true, propagation=Propagation.REQUIRED)
   public boolean checkUserCodeForPasswordReset(String code)
   {
      return userDao.getUserFromUserCode (code) != null;
   }

   @Transactional (readOnly=false, propagation=Propagation.REQUIRED)
   @Caching (evict = {
      @CacheEvict(value = "user", allEntries = true),
      @CacheEvict(value = "userByName", allEntries = true),
      @CacheEvict(value = "json_user", allEntries = true)})
   public void resetPassword(String code, String new_password)
      throws RootNotModifiableException, RequiredFieldMissingException,
         EmailNotSentException
   {
      User u = userDao.getUserFromUserCode (code);
      if (u == null)
      {
         throw new UserNotExistingException ();
      }
      checkRoot (u);

      u.setPassword (new_password);

      checkRequiredFields (u);
      userDao.update (u);
   }

   /**
    * Update given User, after checking required fields.
    *
    * @param user
    * @throws RootNotModifiableException
    * @throws RequiredFieldMissingException
    */
   @PreAuthorize ("hasRole('ROLE_USER_MANAGER')")
   @Transactional (readOnly=false, propagation=Propagation.REQUIRED)
   @Caching(evict = {
      @CacheEvict(value = "user", key = "#user?.getUUID ()"),
      @CacheEvict(value = "userByName", key = "#user?.username.toLowerCase()"),
      @CacheEvict(value = "json_user", key = "#user")})
   public void updateUser (User user) throws RootNotModifiableException,
      RequiredFieldMissingException
   {
      User u = userDao.read (user.getUUID ());
      boolean updateRoles = user.getRoles ().size () != u.getRoles ().size ();
      if (!updateRoles)
      {
         int roleFound = 0;
         for (Role r : u.getRoles ())
         {
            if (user.getRoles ().contains (r))
            {
               roleFound++;
            }
         }
         updateRoles = roleFound != user.getRoles ().size ();
      }
      checkRoot (u);
      u.setUsername (user.getUsername ());
      u.setFirstname (user.getFirstname ());
      u.setLastname (user.getLastname ());
      u.setAddress (user.getAddress ());
      u.setCountry (user.getCountry ());
      u.setEmail (user.getEmail ());
      u.setPhone (user.getPhone ());
      u.setRoles (user.getRoles ());
      u.setUsage (user.getUsage ());
      u.setSubUsage (user.getSubUsage ());
      u.setDomain (user.getDomain ());
      u.setSubDomain (user.getSubDomain ());
      if (user.getPassword() != null)
      {
         // If password is null, it means client forgot to set it up.
         // it should never been set to null.
         u.setEncryptedPassword(user.getPassword(),
            user.getPasswordEncryption());
      }

      Set<AccessRestriction> restrictions = user.getRestrictions ();
      Set<AccessRestriction> restrictionsToDelete = u.getRestrictions ();
      if (u.getRestrictions () != null && user.getRestrictions () != null)
      {
         for (AccessRestriction oldOne : u.getRestrictions ())
         {
            for (AccessRestriction newOne : user.getRestrictions ())
            {
               if (oldOne.getBlockingReason ().equals (
                  newOne.getBlockingReason ()))
               {
                  restrictions.remove (newOne);
                  restrictions.add (oldOne);
                  restrictionsToDelete.remove (oldOne);
               }
               continue;
            }
         }
      }

      u.setRestrictions (restrictions);
      checkRequiredFields (u);
      userDao.update (u);

      if ((restrictions != null && !restrictions.isEmpty ()) || updateRoles)
      {
         SecurityContextProvider.forceLogout (u.getUsername ());
      }

      if (restrictionsToDelete != null)
      {
         for (AccessRestriction restriction : restrictionsToDelete)
         {
            accessRestrictionDao.delete (restriction);
         }
      }

      // Fix to mail user when admin updates his account
      // Temp : to move in mail class after
       LOGGER.debug("User " + u.getUsername () +
       " Updated.");

       if (cfgManager.getMailConfiguration ().isOnUserUpdate ())
       {
          String email = u.getEmail ();
          // Do not send mail to system admin : never used
          if (cfgManager.getAdministratorConfiguration ().getName ()
                .equals (u.getUsername ()) && (email==null))
             email = "[email protected]";

          LOGGER.debug("Sending email to " + email);
          if (email == null)
             throw new UnsupportedOperationException (
                "Missing Email in configuration: " +
                 "Cannot inform modified user \"" + u.getUsername () + ".");

          String message = new String (
             "Dear " + getUserWelcome (u) + ",\n\nYour account on " +
             cfgManager.getNameConfiguration ().getShortName () +
             " has been updated by an administrator:\n" + u.toString () + "\n" +
             "For help requests please write to: " +
             cfgManager.getSupportConfiguration ().getMail () + "\n\n"+
             "Kind regards,\n" +
             cfgManager.getSupportConfiguration ().getName () + ".\n" +
             cfgManager.getServerConfiguration ().getExternalUrl ());
          String subject = new String ("Account " + u.getUsername () +
                " updated");
          try
          {
             mailer.send  (email, null, null, subject, message);
          }
          catch (Exception e)
          {
             throw new EmailNotSentException (
                "Cannot send email to " + email, e);
          }
          LOGGER.debug("email sent.");
       }

   }

   /**
    * Update given User, after checking required fields.
    * @param user
    * @throws RootNotModifiableException
    * @throws RequiredFieldMissingException
    */
   @Transactional(readOnly=false)
   public void systemUpdateUser(User user) throws RootNotModifiableException,
         RequiredFieldMissingException
   {
      checkRoot (user);
      userDao.update(user); // FIXME reproduce updateUser()?
   }

   /**
    * Delete user corresponding to given id.
    *
    * @param uuid User id.
    * @throws RootNotModifiableException
    */
   @PreAuthorize ("hasRole('ROLE_USER_MANAGER')")
   @Transactional (readOnly=false, propagation=Propagation.REQUIRED)
   @Caching (evict = {
      @CacheEvict(value = "user", allEntries = true),
      @CacheEvict(value = "userByName", allEntries = true),
      @CacheEvict(value = "json_user", allEntries = true)})
   public void deleteUser (String uuid) throws RootNotModifiableException,
      EmailNotSentException
   {
      User u = userDao.read (uuid);
      checkRoot (u);
      SecurityContextProvider.forceLogout (u.getUsername ());
      userDao.removeUser (u);
   }

   /**
    * Cout number of users corresponding to filter.
    *
    * @param filter
    * @return Number of users corresponding to filter.
    */
   @PreAuthorize ("hasAnyRole('ROLE_USER_MANAGER','ROLE_DATA_MANAGER','ROLE_SYSTEM_MANAGER')")
   @Transactional (readOnly=true, propagation=Propagation.REQUIRED)
   public int count (String filter)
   {
      return userDao.countNotDeleted (filter);
   }

   @PreAuthorize ("hasRole('ROLE_STATS')")
   @Transactional (readOnly=true, propagation=Propagation.REQUIRED)
   public int countAll (String filter)
   {
      return userDao.countAll (filter);
   }

   @PreAuthorize ("hasAnyRole('ROLE_USER_MANAGER','ROLE_DATA_MANAGER')")
   @Transactional (readOnly=true, propagation=Propagation.REQUIRED)
   public int countForDataRight (String filter)
   {
      return userDao.countForDataRight (filter);
   }

   @PreAuthorize ("isAuthenticated ()")
   @Transactional (readOnly=true, propagation=Propagation.REQUIRED)
   public List<AccessRestriction> getRestrictions (String user_uuid)
   {
      return new ArrayList<> (userDao.read (user_uuid).getRestrictions ());
   }

   /**
    * THIS METHOD IS NOT SAFE: IT MUST BE REMOVED.
    * TODO: manage access by page.
    * @param user_uuid
    * @return
    */
   @PreAuthorize ("hasRole('ROLE_DATA_MANAGER')")
   @Transactional (readOnly=true, propagation=Propagation.REQUIRED)
   public List<Long> getAuthorizedProducts (String user_uuid)
   {
      return productDao.getAuthorizedProducts (user_uuid);
   }

   @PreAuthorize ("hasRole('ROLE_DATA_MANAGER')")
   @Transactional (readOnly=true, propagation=Propagation.REQUIRED)
   public List<String> getAuthorizedCollections (String user_uuid)
   {
      return collectionDao.getAuthorizedCollections (user_uuid);
   }

   @PreAuthorize ("hasRole('ROLE_DATA_MANAGER')")
   @Transactional (readOnly=false, propagation=Propagation.REQUIRED)
   public void addAccessToCollections (String user_uuid, List<String> collection_uuids)
      throws RootNotModifiableException
   {
      User user = userDao.read (user_uuid);
      checkRoot (user);
      // database
      for (String collectionUUID : collection_uuids)
      {
         Collection collection = collectionDao.read (collectionUUID);
         userDao.addAccessToCollection (user, collection);
      }
   }

   @PreAuthorize ("hasRole('ROLE_DATA_MANAGER')")
   @Transactional (readOnly=false, propagation=Propagation.REQUIRED)
   public void removeAccessToCollections (String user_uuid,
         List<String> collection_uuids) throws RootNotModifiableException
   {
      User user = userDao.read (user_uuid);
      checkRoot (user);
      for (String collectionUUID : collection_uuids)
      {
         Collection collection = collectionDao.read(collectionUUID);
         userDao.removeAccessToCollection (user.getUUID(), collection);
      }
   }

   @PreAuthorize ("hasRole('ROLE_DATA_MANAGER')")
   public void addAccessToProducts (Long user_id, List<Long> product_ids) throws
         RootNotModifiableException
   {
      // TODO to remove
   }

   @PreAuthorize ("hasRole('ROLE_DATA_MANAGER')")
   public void removeAccessToProducts (Long user_id, List<Long> product_ids)
         throws RootNotModifiableException
   {
      // TODO to remove
   }


   /**
    * Update given User, after checking required fields.
    *
    * @param user
    * @throws RootNotModifiableException
    * @throws RequiredFieldMissingException
    */
   @PreAuthorize ("isAuthenticated ()")
   @Transactional (readOnly=false, propagation=Propagation.REQUIRED)
   @Caching (evict = {
      @CacheEvict(value = "user", key = "#user.getUUID ()"),
      @CacheEvict(value = "userByName", key = "#user.username.toLowerCase()"),
      @CacheEvict(value = "json_user", key = "#user")})
   public void selfUpdateUser (User user) throws RootNotModifiableException,
      RequiredFieldMissingException, EmailNotSentException
   {
      User u = userDao.read (user.getUUID ());
      checkRoot (u);
      u.setEmail (user.getEmail ());
      u.setFirstname (user.getFirstname ());
      u.setLastname (user.getLastname ());
      u.setAddress (user.getAddress ());
      u.setPhone (user.getPhone ());
      u.setCountry (user.getCountry ());
      u.setUsage (user.getUsage ());
      u.setSubUsage (user.getSubUsage ());
      u.setDomain (user.getDomain ());
      u.setSubDomain (user.getSubDomain ());

      checkRequiredFields (u);
      userDao.update (u);
   }

   @PreAuthorize ("isAuthenticated ()")
   @Transactional (readOnly=false, propagation=Propagation.REQUIRED)
   @Caching (evict = {
      @CacheEvict(value = "user", allEntries = true),
      @CacheEvict(value = "userByName", allEntries = true),
      @CacheEvict(value = "json_user", allEntries = true)})
   public void selfChangePassword (String uuid, String old_password,
         String new_password) throws RootNotModifiableException,
         RequiredFieldMissingException, EmailNotSentException,
         UserBadOldPasswordException
   {
      User u = userDao.read (uuid);
      checkRoot (u);

      //encrypt old password to compare
      PasswordEncryption encryption = u.getPasswordEncryption ();
      if (encryption != PasswordEncryption.NONE) // when configurable
      {
         try
         {
            MessageDigest md =
                  MessageDigest.getInstance(encryption.getAlgorithmKey());
            old_password = new String(
                  Hex.encode(md.digest(old_password.getBytes("UTF-8"))));
         }
         catch (Exception e)
         {
            throw new UserBadEncryptionException (
                  "There was an error while encrypting password of user " +
                        u.getUsername (), e);
         }
      }

      if (! u.getPassword ().equals(old_password))
      {
         throw new UserBadOldPasswordException("Old password is not correct.");
      }

      u.setPassword (new_password);

      checkRequiredFields (u);
      userDao.update (u);
   }

   @PreAuthorize ("hasRole('ROLE_SEARCH')")
   @Transactional (readOnly=false, propagation=Propagation.REQUIRED)
   public void storeUserSearch (String uuid, String search, String footprint,
         HashMap<String, String> advanced, String complete)
   {
      User u = userDao.read (uuid);
      if (u == null)
      {
         throw new UserNotExistingException ();
      }
      for (Search s : u.getPreferences ().getSearches ())
      {
         if (s.getComplete ().equals(complete))
         {
            return;
         }
      }
      userDao.storeUserSearch (u, search, footprint, advanced, complete);
   }

   @PreAuthorize ("hasRole('ROLE_SEARCH')")
   @Transactional (readOnly=false, propagation=Propagation.REQUIRED)
   public void removeUserSearch (String u_uuid, String uuid)
   {
      User u = userDao.read (u_uuid);
      if (u == null)
      {
         throw new UserNotExistingException ();
      }
      userDao.removeUserSearch (u, uuid);
   }

   @PreAuthorize ("hasRole('ROLE_SEARCH')")
   @Transactional (readOnly=false, propagation=Propagation.REQUIRED)
   public void activateUserSearchNotification (String uuid, boolean notify)
   {
      userDao.activateUserSearchNotification (uuid, notify);
   }

   @PreAuthorize ("hasRole('ROLE_SEARCH')")
   @Transactional (readOnly=true, propagation=Propagation.REQUIRED)
   public int countUserSearches (String uuid)
   {
      User u = userDao.read (uuid);
      if (u == null)
      {
         throw new UserNotExistingException ();
      }
      List<Search> searches = userDao.getUserSearches(u);
      return searches != null ? searches.size () : 0;
   }

   @PreAuthorize ("hasRole('ROLE_SEARCH')")
   @Transactional (readOnly=true, propagation=Propagation.REQUIRED)
   public int countUploadedProducts (String uuid)
   {
      User u = userDao.read (uuid);
      if (u == null)
      {
         throw new UserNotExistingException ();
      }
      List<Product> uploadeds = productDao.getUploadedProducts (u);
      return uploadeds != null ? uploadeds.size () : 0;
   }

   @PreAuthorize ("hasRole('ROLE_SEARCH')")
   @Transactional (readOnly=false, propagation=Propagation.REQUIRED)
   public void clearSavedSearches (String uuid)
   {
      User u = userDao.read (uuid);
      if (u == null)
      {
         throw new UserNotExistingException ();
      }
      userDao.clearUserSearches(u);
   }

   @PreAuthorize ("hasRole('ROLE_SEARCH')")
   @Transactional (readOnly=true, propagation=Propagation.REQUIRED)
   public List<Search> getAllUserSearches (String uuid)
   {
      User u = userDao.read (uuid);
      if (u == null)
      {
         throw new UserNotExistingException ();
      }
      return userDao.getUserSearches(u);
   }

   @PreAuthorize ("hasRole('ROLE_SEARCH')")
   public Date getNextScheduleSearch() throws SchedulerException
   {
      return scheduler.getNextSearchesJobSchedule ();
   }

   @PreAuthorize ("hasRole('ROLE_SEARCH')")
   @Transactional (readOnly=true, propagation=Propagation.REQUIRED)
   public List<Search> scrollSearchesOfUser (String uuid, int skip, int top)
   {
      User u = userDao.read (uuid);
      if (u == null)
      {
         throw new UserNotExistingException ();
      }
      return searchDao.scrollSearchesOfUser (u, skip, top);
   }

   @PreAuthorize ("hasRole('ROLE_UPLOAD')")
   @Transactional (readOnly=true, propagation=Propagation.REQUIRED)
   public List<Product> getUploadedProducts(String uuid, int skip, int top)
            throws UserNotExistingException, ProductNotExistingException
   {
      User user = userDao.read (uuid);
      if (user == null)
      {
         throw new UserNotExistingException();
      }
      return productDao.scrollUploadedProducts (user, skip, top);
   }

   @PreAuthorize ("hasRole('ROLE_UPLOAD')")
   @Transactional (readOnly=true, propagation=Propagation.REQUIRED)
   public Set<String> getUploadedProductsIdentifiers (String uuid) throws
         UserNotExistingException, ProductNotExistingException
   {
      User user = userDao.read (uuid);
      if (user == null)
      {
         throw new UserNotExistingException();
      }
      List<Product> products = productDao.getUploadedProducts (user);
      Set<String> prods = new HashSet<String> ();
      for (Product p : products)
      {
         prods.add(p.getIdentifier ());
      }
      return prods;
   }

   @Transactional (readOnly=true, propagation=Propagation.REQUIRED)
   public void forgotPassword (User user, String baseuri)
      throws UserNotExistingException, RootNotModifiableException,
         EmailNotSentException
   {
      checkRoot (user);
      User checked = userDao.getByName (user.getUsername ());
      if (checked == null || !checked.getEmail ().toLowerCase ().
               equals (user.getEmail ().toLowerCase ()))
      {
         throw new UserNotExistingException ("No user can be found for this " +
                        "username/mail combination");
      }

      String message = "Dear " + getUserWelcome (checked) +",\n\n" +
            "Please follow this link to set a new password in the " +
            cfgManager.getNameConfiguration ().getShortName () +" system:\n" +
            cfgManager.getServerConfiguration ().getExternalUrl () + baseuri +
            userDao.computeUserCode (checked) + "\n\n"  +

            "For help requests please write to: " +
            cfgManager.getSupportConfiguration ().getMail () + "\n\n" +
            "Kind regards.\n" +
            cfgManager.getSupportConfiguration ().getName () + ".\n" +
            cfgManager.getServerConfiguration ().getExternalUrl ();

      String subject = "User password reset";

      try
      {
         mailer.send  (checked.getEmail (), null, null, subject, message);
      }
      catch (Exception e)
      {
         throw new EmailNotSentException (
            "Cannot send email to " + checked.getEmail (), e);
      }
   }

   @Transactional (readOnly=true, propagation=Propagation.REQUIRED)
   private void checkRoot (User user) throws RootNotModifiableException
   {
      if (user == null) return;
      if (userDao.isRootUser (user))
      {
         throw new RootNotModifiableException ("Root cannot be modified");
      }
   }

   @Transactional (readOnly=true, propagation=Propagation.REQUIRED)
   private void checkRequiredFields (User user)
      throws RequiredFieldMissingException, UsernameBadCharacterException,
      MalformedEmailException
   {
      if (user.getUsername () == null ||
         user.getUsername ().trim ().isEmpty () ||
         user.getPassword () == null ||
         user.getPassword ().trim ().isEmpty () || user.getEmail () == null ||
         user.getEmail ().trim ().isEmpty ())
      {
         throw new RequiredFieldMissingException (
            "At least one required field is empty.");
      }
      // Test username allowed chars [a-zA-Z0-9]
      if (!USERNAME_PATTERN.matcher (user.getUsername ()).find ())
      {
         throw new UsernameBadCharacterException (
            "At least one forbidden character has been detected in username.");
      }
      // Test email field
      if (!EMAIL_PATTERN.matcher (user.getEmail ()).find ())
      {
         throw new MalformedEmailException (
            "Email is not well formed.");
      }
   }

   @Transactional (readOnly=true, propagation=Propagation.REQUIRED)
   private String getUserWelcome (User u)
   {
      String firstname = u.getUsername ();
      String lastname = "";
      if (u.getFirstname () != null && !u.getFirstname().trim ().isEmpty ())
      {
         firstname = u.getFirstname ();
         if (u.getLastname () != null && !u.getLastname().trim ().isEmpty ())
            lastname = " " + u.getLastname ();
      }
      return firstname + lastname;
   }

   @Transactional (readOnly=true, propagation=Propagation.REQUIRED)
   public String getPublicDataUserUUID ()
   {
      return userDao.getPublicData ().getUUID ();
   }

   @Transactional (readOnly=true, propagation=Propagation.REQUIRED)
   public List<Country> getCountries ()
   {
      return countryDao.readAll ();
   }

   @PreAuthorize ("isAuthenticated ()")
   @Transactional (readOnly=true, propagation=Propagation.REQUIRED)
   public User getCurrentUserInformation () throws RootNotModifiableException
   {
      User u = securityService.getCurrentUser ();
      if (u == null) return null;
      return getUserByName(u.getUUID ());
   }

   /**
    * Facility method to easily provide user content with resolved lazy fields
    * to be able to serialize. The method takes care of the possible cycles
    * such as "users->pref->filescanners->collections->users" ...
    * It also removes possible huge product list from collections.
    *
    * @param u the user to resolve.
    * @return the resolved user.
    */
   @Transactional (readOnly=true, propagation=Propagation.REQUIRED)
   @Cacheable (value = "json_user", key = "#u")
   public User resolveUser (User u)
   {
      u = userDao.read(u.getUUID());
      Gson gson = new GsonBuilder().setExclusionStrategies (
         new ExclusionStrategy()
         {
            public boolean shouldSkipClass(Class<?> clazz)
            {
               // Avoid huge number of products in collection
               return clazz==Product.class;
            }
            /**
             * Custom field exclusion goes here
             */
            public boolean shouldSkipField(FieldAttributes f)
            {
               // Avoid cycles caused by collection tree and user/auth users...
               return f.getName().equals("authorizedUsers") ||
                      f.getName().equals("parent") ||
                      f.getName().equals("subCollections");

            }
         }).serializeNulls().create();
      String users_string = gson.toJson(u);
      return gson.fromJson(users_string, User.class);
   }

    /*
    * Get all non deleted users corresponding to given filter from the specified offset and limit.
    * @param filter
    * @param skip
    * @param top
    * @return
    */
   @PreAuthorize ("hasRole('ROLE_USER_MANAGER')")
   @Transactional (readOnly=true, propagation=Propagation.REQUIRED)
   public Iterator<User> getUsersByFilter (String filter, int skip)
   {
      return userDao.scrollNotDeletedByFilter (filter, skip);
   }

   /**
    * Cout number of users corresponding to filter.
    *
    * @param filter
    * @return Number of users corresponding to filter.
    */
   @PreAuthorize ("hasAnyRole('ROLE_USER_MANAGER','ROLE_DATA_MANAGER')")
   @Transactional (readOnly=true, propagation=Propagation.REQUIRED)
   public int countByFilter (String filter)
   {
      return userDao.countNotDeletedByFilter (filter);
   }

   /**
    * Finds a referenced country in ISO norm.
    * @param country name, alpha2 or alpha3 of country.
    * @return true, if country name, alpha2 or alpha is referenced in ISO norme.
    */
   @Transactional (readOnly = true)
   public Country getCountry (String country)
   {
      switch (country.length ())
      {
         case 2:
            return countryDao.getCountryByAlpha2 (country);
         case 3:
            return countryDao.getCountryByAlpha3 (country);
         default:
            return countryDao.getCountryByName (country);
      }
   }
}