/*
* Data Hub Service (DHuS) - For Space data distribution.
* Copyright (C) 2016 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.sync.impl;
import fr.gael.dhus.database.object.Role;
import fr.gael.dhus.database.object.SynchronizerConf;
import fr.gael.dhus.database.object.User;
import fr.gael.dhus.database.object.User.PasswordEncryption;
import fr.gael.dhus.database.object.restriction.LockedAccessRestriction;
import fr.gael.dhus.olingo.ODataClient;
import fr.gael.dhus.olingo.v1.entityset.SystemRoleEntitySet;
import fr.gael.dhus.olingo.v1.entityset.UserEntitySet;
import fr.gael.dhus.service.ISynchronizerService;
import fr.gael.dhus.service.UserService;
import fr.gael.dhus.service.exception.RequiredFieldMissingException;
import fr.gael.dhus.service.exception.RootNotModifiableException;
import fr.gael.dhus.service.exception.UsernameBadCharacterException;
import fr.gael.dhus.spring.context.ApplicationContextProvider;
import fr.gael.dhus.sync.Synchronizer;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.collections.SetUtils;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.olingo.odata2.api.ep.entry.ODataEntry;
import org.apache.olingo.odata2.api.ep.feed.ODataFeed;
import org.apache.olingo.odata2.api.exception.ODataException;
import org.hibernate.exception.LockAcquisitionException;
import org.springframework.dao.CannotAcquireLockException;
import org.springframework.web.util.UriUtils;
/**
* Synchronizes users through the OData user API.
*/
public class ODataUserSynchronizer extends Synchronizer
{
/** Log. */
private static final Logger LOGGER = LogManager.getLogger(ODataUserSynchronizer.class);
/** Synchronizer Service, to save the */
private static final ISynchronizerService SYNC_SERVICE =
ApplicationContextProvider.getBean (ISynchronizerService.class);
/** User Service, to create user objects. */
private static final UserService USER_SERVICE =
ApplicationContextProvider.getBean(UserService.class);
/** An {@link ODataClient} configured to query another DHuS OData service. */
private final ODataClient client;
/** Credentials: username. */
private final String serviceUser;
/** Credentials: password. */
private final String servicePass;
/** Current offset in remote's user list ($skip parameter) */
private int skip;
/** Size of a Page (number of users to retrieve at once, $top parameter). */
private int pageSize;
/** Force user synchronizer, without checking creation date. */
private boolean force;
/**
* Creates a new UserSynchronizer.
*
* @param sc configuration for this synchronizer.
*
* @throws java.io.IOException
* @throws org.apache.olingo.odata2.api.exception.ODataException
*/
public ODataUserSynchronizer(SynchronizerConf sc) throws IOException, ODataException
{
super(sc);
// Checks if required configuration is set
String urilit = sc.getConfig("service_uri");
serviceUser = sc.getConfig("service_username");
servicePass = sc.getConfig("service_password");
if (urilit == null || urilit.isEmpty())
{
throw new IllegalStateException("`service_uri` is not set");
}
try
{
client = new ODataClient(urilit, serviceUser, servicePass);
}
catch (URISyntaxException e)
{
throw new IllegalStateException("`service_uri` is malformed");
}
String skip = sc.getConfig("skip");
if (skip != null && !skip.isEmpty())
{
this.skip = Integer.parseInt(skip);
}
else
{
this.skip = 0;
}
String page_size = sc.getConfig("page_size");
if (page_size != null && !page_size.isEmpty())
{
pageSize = Integer.decode(page_size);
}
else
{
pageSize = 500;
}
String cfgForce = sc.getConfig("force");
if (cfgForce != null && !cfgForce.isEmpty())
{
force = Boolean.parseBoolean (cfgForce);
}
else
{
force = false;
}
}
/** Prints log line prefixed with the sync ID. */
private void log(Level level, String message)
{
LOGGER.log(level, "UserSync#" + getId() + ' ' + message);
}
/** Prints log line prefixed with the sync ID. */
private void log(Level level, String message, Throwable ex)
{
LOGGER.log(level, "UserSync#" + getId() + ' ' + message, ex);
}
/** Logs how much time an OData command consumed. */
private ODataFeed readFeedLogPerf(String query, Map<String, String>params)
throws IOException, ODataException, InterruptedException
{
long delta_time = System.currentTimeMillis();
ODataFeed feed = client.readFeed(query, params);
delta_time = System.currentTimeMillis() - delta_time;
log(Level.DEBUG, "query(" + query + ") done in " + delta_time + "ms");
return feed;
}
@Override
public boolean synchronize() throws InterruptedException
{
log(Level.INFO, "started");
int created = 0, updated = 0;
try
{
// Makes query parameters
Map<String, String> query_param = new HashMap<>();
if (skip != 0)
{
query_param.put("$skip", String.valueOf(skip));
}
query_param.put("$top", String.valueOf(pageSize));
log(Level.DEBUG, "Querying users from " + skip + " to " + (skip + pageSize));
ODataFeed userfeed = readFeedLogPerf("/Users", query_param);
// For each entry, creates a DataBase Object
for (ODataEntry pdt: userfeed.getEntries())
{
String username = null;
try
{
Map<String, Object> props = pdt.getProperties();
username = (String)props.get(UserEntitySet.USERNAME);
String email = (String)props.get(UserEntitySet.EMAIL);
String firstname = (String)props.get(UserEntitySet.FIRSTNAME);
String lastname = (String)props.get(UserEntitySet.LASTNAME);
String country = (String)props.get(UserEntitySet.COUNTRY);
String domain = (String)props.get(UserEntitySet.DOMAIN);
String subdomain = (String)props.get(UserEntitySet.SUBDOMAIN);
String usage = (String)props.get(UserEntitySet.USAGE);
String subusage = (String)props.get(UserEntitySet.SUBUSAGE);
String phone = (String)props.get(UserEntitySet.PHONE);
String address = (String)props.get(UserEntitySet.ADDRESS);
String hash = (String)props.get(UserEntitySet.HASH);
String password = (String)props.get(UserEntitySet.PASSWORD);
Date creation = ((GregorianCalendar)props.get(UserEntitySet.CREATED)).getTime();
// Uses the Scheme encoder as it is the most restrictives, it only allows
// '+' (forbidden char in usernames, so it's ok)
// '-' (forbidden char in usernames, so it's ok)
// '.' (allowed char in usernames, not problematic)
// the alphanumeric character class (a-z A-Z 0-9)
String encoded_username = UriUtils.encodeScheme(username, "UTF-8");
// Retrieves Roles
String roleq = String.format("/Users('%s')/SystemRoles", encoded_username);
ODataFeed userrole = readFeedLogPerf(roleq, null);
List<ODataEntry> roles = userrole.getEntries();
List<Role> new_roles = new ArrayList<>();
for (ODataEntry role: roles)
{
String rolename = (String)role.getProperties().get(SystemRoleEntitySet.NAME);
new_roles.add(Role.valueOf(rolename));
}
// Has restriction?
String restricq = String.format("/Users('%s')/Restrictions", encoded_username);
ODataFeed userrestric = readFeedLogPerf(restricq, null);
boolean has_restriction = !userrestric.getEntries().isEmpty();
// Reads user in database, may be null
User user = USER_SERVICE.getUserNoCheck(username);
// Updates existing user
if (user != null && (force || creation.equals(user.getCreated())))
{
boolean changed = false;
// I wish users had their `Updated` field exposed on OData
if (!username.equals(user.getUsername()))
{
user.setUsername(username);
changed = true;
}
if (email == null && user.getEmail() != null ||
email != null && !email.equals(user.getEmail()))
{
user.setEmail(email);
changed = true;
}
if (firstname == null && user.getFirstname() != null ||
firstname != null && !firstname.equals(user.getFirstname()))
{
user.setFirstname(firstname);
changed = true;
}
if (lastname == null && user.getLastname() != null ||
lastname != null && !lastname.equals(user.getLastname()))
{
user.setLastname(lastname);
changed = true;
}
if (country == null && user.getCountry() != null ||
country != null && !country.equals(user.getCountry()))
{
user.setCountry(country);
changed = true;
}
if (domain == null && user.getDomain() != null ||
domain != null && !domain.equals(user.getDomain()))
{
user.setDomain(domain);
changed = true;
}
if (subdomain == null && user.getSubDomain() != null ||
subdomain != null && !subdomain.equals(user.getSubDomain()))
{
user.setSubDomain(subdomain);
changed = true;
}
if (usage == null && user.getUsage() != null ||
usage != null && !usage.equals(user.getUsage()))
{
user.setUsage(usage);
changed = true;
}
if (subusage == null && user.getSubUsage() != null ||
subusage != null && !subusage.equals(user.getSubUsage()))
{
user.setSubUsage(subusage);
changed = true;
}
if (phone == null && user.getPhone() != null ||
phone != null && !phone.equals(user.getPhone()))
{
user.setPhone(phone);
changed = true;
}
if (address == null && user.getAddress() != null ||
address != null && !address.equals(user.getAddress()))
{
user.setAddress(address);
changed = true;
}
if (password == null && user.getPassword()!= null ||
password != null && !password.equals(user.getPassword()))
{
if (hash == null)
hash = PasswordEncryption.NONE.getAlgorithmKey ();
user.setEncryptedPassword(password,
User.PasswordEncryption.fromAlgorithm (hash));
changed = true;
}
//user.setPasswordEncryption(User.PasswordEncryption.valueOf(hash));
if (!SetUtils.isEqualSet(user.getRoles(), new_roles))
{
user.setRoles(new_roles);
changed = true;
}
if (has_restriction != !user.getRestrictions().isEmpty())
{
if (has_restriction)
{
user.addRestriction(new LockedAccessRestriction());
}
else
{
user.setRestrictions(Collections.EMPTY_SET);
}
changed = true;
}
if (changed)
{
log(Level.DEBUG, "Updating user " + user.getUsername());
USER_SERVICE.systemUpdateUser(user);
updated++;
}
}
// Creates new user
else if (user == null)
{
user = new User();
user.setUsername(username);
user.setEmail(email);
user.setFirstname(firstname);
user.setLastname(lastname);
user.setCountry(country);
user.setDomain(domain);
user.setSubDomain(subdomain);
user.setUsage(usage);
user.setSubUsage(subusage);
user.setPhone(phone);
user.setAddress(address);
if (hash == null)
hash = PasswordEncryption.NONE.getAlgorithmKey ();
user.setEncryptedPassword(password,
User.PasswordEncryption.fromAlgorithm (hash));
user.setCreated(creation);
user.setRoles(new_roles);
if (has_restriction)
{
user.addRestriction(new LockedAccessRestriction());
}
log(Level.DEBUG, "Creating new user " + user.getUsername());
USER_SERVICE.systemCreateUser(user);
created++;
}
else
{
log(Level.ERROR, "Namesake '" + username + "' detected!");
}
}
catch (RootNotModifiableException e) { } // Ignored exception
catch (RequiredFieldMissingException | UsernameBadCharacterException ex)
{
log(Level.ERROR, "Cannot create user '" + username + "'", ex);
}
catch (IOException | ODataException ex)
{
log(Level.ERROR, "OData failure on user '" + username + "'", ex);
}
this.skip++;
}
// This is the end, resets `skip` to 0
if (userfeed.getEntries().size() < pageSize) {
this.skip = 0;
}
}
catch (IOException | ODataException ex)
{
log(Level.ERROR, "OData failure", ex);
}
catch (LockAcquisitionException | CannotAcquireLockException e)
{
throw new InterruptedException(e.getMessage());
}
finally
{
StringBuilder sb = new StringBuilder("done: ");
sb.append(created).append(" new Users, ");
sb.append(updated).append(" updated Users, ");
sb.append(" from ").append(this.client.getServiceRoot());
log(Level.INFO, sb.toString());
this.syncConf.setConfig("skip", String.valueOf(skip));
SYNC_SERVICE.saveSynchronizer(this);
}
return false;
}
@Override
public String toString()
{
return "OData User Synchronizer on " + syncConf.getConfig("service_uri");
}
}