/*
* Copyright 2012-2013, CMM, University of Queensland.
*
* This file is part of Paul.
*
* Paul is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Paul 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Paul. If not, see <http://www.gnu.org/licenses/>.
*/
package au.edu.uq.cmm.paul.servlet;
import java.io.File;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.sql.BatchUpdateException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.NoResultException;
import javax.persistence.RollbackException;
import javax.persistence.TypedQuery;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import au.edu.uq.cmm.eccles.EcclesProxyConfiguration;
import au.edu.uq.cmm.eccles.StaticEcclesProxyConfiguration;
import au.edu.uq.cmm.paul.GrabberFacilityConfig;
import au.edu.uq.cmm.paul.Paul;
import au.edu.uq.cmm.paul.PaulConfiguration;
import au.edu.uq.cmm.paul.PaulFacilityMapper;
import au.edu.uq.cmm.paul.StaticPaulConfiguration;
import au.edu.uq.cmm.paul.StaticPaulFacilities;
import au.edu.uq.cmm.paul.StaticPaulFacility;
import au.edu.uq.cmm.paul.status.DatafileTemplate;
import au.edu.uq.cmm.paul.status.Facility;
import au.edu.uq.cmm.paul.watcher.UncPathnameMapper;
public class ConfigurationManager {
private static class Configs{
private PaulConfiguration config;
private EcclesProxyConfiguration proxyConfig;
private int facilityCount;
public Configs(Configs configs) {
this(configs.config, configs.proxyConfig, configs.facilityCount);
}
public Configs(PaulConfiguration config,
EcclesProxyConfiguration proxyConfig,
int facilityCount) {
super();
this.config = config;
this.proxyConfig = proxyConfig;
this.facilityCount = facilityCount;
}
public boolean incomplete() {
return this.config == null || this.proxyConfig == null || this.facilityCount == 0;
}
@Override
public String toString() {
return "Configs [config=" + config + ", proxyConfig=" + proxyConfig
+ ", facilityCount=" + facilityCount + "]";
}
}
private static final Logger LOG = LoggerFactory.getLogger(ConfigurationManager.class);
private Configs activeConfigs;
private Configs latestConfigs;
private StaticPaulConfiguration staticConfig;
private StaticEcclesProxyConfiguration staticProxyConfig;
private EntityManagerFactory entityManagerFactory;
private StaticPaulFacilities staticFacilities;
private UncPathnameMapper uncNameMapper;
public ConfigurationManager(Paul services, EntityManagerFactory entityManagerFactory,
StaticPaulConfiguration staticConfig,
StaticEcclesProxyConfiguration staticProxyConfig,
StaticPaulFacilities staticFacilities) {
this.entityManagerFactory = Objects.requireNonNull(entityManagerFactory);
this.staticConfig = staticConfig;
this.staticProxyConfig = staticProxyConfig;
this.staticFacilities = staticFacilities;
activeConfigs = loadConfigurations();
if (activeConfigs.incomplete()) {
activeConfigs = doResetConfigurations(activeConfigs.facilityCount == 0);
}
latestConfigs = new Configs(activeConfigs);
uncNameMapper = Objects.requireNonNull(services.getUncNameMapper());
}
public PaulConfiguration getActiveConfig() {
return activeConfigs.config;
}
public PaulConfiguration getLatestConfig() {
return latestConfigs.config;
}
public EcclesProxyConfiguration getActiveProxyConfig() {
return activeConfigs.proxyConfig;
}
public EcclesProxyConfiguration getLatestProxyConfig() {
return latestConfigs.proxyConfig;
}
public void resetConfigurations() {
latestConfigs = doResetConfigurations(false);
}
private Configs loadConfigurations() {
Configs configs = new Configs(
PaulConfiguration.load(entityManagerFactory),
EcclesProxyConfiguration.load(entityManagerFactory),
PaulFacilityMapper.getFacilityCount(entityManagerFactory));
LOG.info("Loaded configs from database: " + configs);
return configs;
}
private Configs doResetConfigurations(boolean reloadFacilities) {
LOG.info("Resetting from static configurations");
LOG.info("Static grabber configs: " + staticConfig);
LOG.info("Static proxy configs: " + staticProxyConfig);
EntityManager em = entityManagerFactory.createEntityManager();
try {
PaulConfiguration newConfig = new PaulConfiguration(staticConfig);
EcclesProxyConfiguration newProxyConfig = new EcclesProxyConfiguration(staticProxyConfig);
// FIXME - we do two transactions. Combining the two transactions
// into one transaction is gives constraint errors when adding the new
// facilities. (The fix is to version the configurations.)
em.getTransaction().begin();
try {
PaulConfiguration oldConfig = em.
createQuery("from PaulConfiguration", PaulConfiguration.class).
getSingleResult();
em.remove(oldConfig);
} catch (NoResultException ex) {
// OK
}
try {
EcclesProxyConfiguration oldProxyConfig = em.
createQuery("from EcclesProxyConfiguration", EcclesProxyConfiguration.class).
getSingleResult();
em.remove(oldProxyConfig);
} catch (NoResultException ex) {
// OK
}
if (reloadFacilities) {
List<Facility> facilities = em.
createQuery("from Facility", Facility.class).
getResultList();
for (Facility facility : facilities) {
em.remove(facility);
}
}
em.getTransaction().commit();
// Second transaction
em.getTransaction().begin();
em.persist(newConfig);
em.persist(newProxyConfig);
if (reloadFacilities) {
for (StaticPaulFacility staticFacility : staticFacilities.getFacilities()) {
Facility facility = new Facility(staticFacility);
em.persist(facility);
}
}
em.getTransaction().commit();
return new Configs(newConfig, newProxyConfig,
PaulFacilityMapper.getFacilityCount(entityManagerFactory));
} catch (UnknownHostException ex) {
LOG.error("Reset failed", ex);
return null;
} finally {
em.close();
}
}
public ValidationResult<Facility> createFacility(Map<?, ?> params) {
EntityManager em = entityManagerFactory.createEntityManager();
try {
em.getTransaction().begin();
Facility facility = new Facility();
Map<String, String> diags = buildFacility(facility, params, em);
if (diags.isEmpty()) {
em.persist(facility);
em.getTransaction().commit();
} else {
em.getTransaction().rollback();
}
return new ValidationResult<Facility>(diags, facility);
} catch (RollbackException ex) {
diagnoseRollback(ex);
throw ex;
} finally {
em.close();
}
}
public ValidationResult<Facility> updateFacility(String facilityName, Map<?, ?> params) {
EntityManager em = entityManagerFactory.createEntityManager();
try {
em.getTransaction().begin();
TypedQuery<Facility> query = em.createQuery(
"from Facility f where f.facilityName = :name",
Facility.class);
query.setParameter("name", facilityName);
Facility facility = query.getSingleResult();
Map<String, String> diags = buildFacility(facility, params, em);
if (diags.isEmpty()) {
em.getTransaction().commit();
} else {
em.getTransaction().rollback();
}
return new ValidationResult<Facility>(diags, facility);
} catch (RollbackException ex) {
diagnoseRollback(ex);
throw ex;
} finally {
em.close();
}
}
public void deleteFacility(String facilityName) {
EntityManager em = entityManagerFactory.createEntityManager();
try {
em.getTransaction().begin();
TypedQuery<Facility> query = em.createQuery(
"from Facility f where f.facilityName = :name",
Facility.class);
query.setParameter("name", facilityName);
Facility facility = query.getSingleResult();
em.remove(facility);
em.getTransaction().commit();
} catch (RollbackException ex) {
diagnoseRollback(ex);
throw ex;
} finally {
em.close();
}
}
private void diagnoseRollback(RollbackException ex) {
Throwable e = ex;
while (e.getCause() != null) {
e = e.getCause();
}
if (e instanceof BatchUpdateException) {
LOG.error("update failed - next is",
((BatchUpdateException) e).getNextException());
} else {
LOG.error("update failed - cause is", e);
}
}
public Map<String, String> buildFacility(Facility res, Map<?, ?> params, EntityManager em) {
Map<String, String> diags = new HashMap<String, String>();
String facilityName = getNonEmptyString(params, "facilityName", diags);
String localHostId = getStringOrNull(params, "localHostId", diags);
String address = getStringOrNull(params, "address", diags);
checkFacilityNameUnique(facilityName, res.getId(), diags, em);
res.setMultiplexed(getBoolean(params, "multiplexed", diags));
if (address != null) {
checkAddressability(address, localHostId, res.getId(),
res.isMultiplexed(), diags, em);
}
checkLocalHostIdUnique(localHostId, res.getId(), diags, em);
if (address == null && localHostId == null) {
addDiag(diags, "localHostId",
"the local host id must be non-empty if address is empty");
}
res.setFacilityDescription(getStringOrNull(params, "facilityDescription", diags));
res.setAccessName(getStringOrNull(params, "accessName", diags));
res.setAccessPassword(getStringOrNull(params, "accessPassword", diags));
String folderName = getNonEmptyString(params, "folderName", diags);
res.setFolderName(folderName);
checkFoldername(folderName, res.getId(), diags, em);
res.setDriveName(getStringOrNull(params, "driveName", diags));
if (res.getDriveName() != null && !res.getDriveName().matches("[A-Z]")) {
addDiag(diags, "driveName",
"the drive name must be a single uppercase letter");
}
res.setFileSettlingTime(getInteger(params, "fileSettlingTime", diags));
if (res.getFileSettlingTime() < 0) {
addDiag(diags, "fileSettlingTime",
"the file setting time cannot be negative");
}
res.setCaseInsensitive(getBoolean(params, "caseInsensitive", diags));
res.setUserOperated(getBoolean(params, "userOperated", diags));
res.setUseFileLocks(getBoolean(params, "useFileLocks", diags));
res.setUseFullScreen(getBoolean(params, "useFullScreen", diags));
res.setUseTimer(getBoolean(params, "useTimer", diags));
res.setDisabled(getBoolean(params, "disabled", diags));
String arg = getNonEmptyString(params, "fileArrivalMode", diags);
if (arg != null) {
try {
res.setFileArrivalMode(GrabberFacilityConfig.FileArrivalMode.valueOf(arg));
} catch (IllegalArgumentException ex) {
addDiag(diags, "fileArrivalMode", "unrecognized mode '" + arg + "'");
}
}
List<DatafileTemplate> templates = new LinkedList<DatafileTemplate>();
int last = getInteger(params, "lastTemplate", diags);
for (int i = 1; i <= last; i++) {
String baseName = "template" + i;
// If we have no parameters starting with the basename, skip.
boolean found = false;
for (Object paramName : params.keySet()) {
if (paramName.toString().startsWith(baseName)) {
found = true;
break;
}
}
if (!found) {
continue;
}
if (getStringOrNull(params, baseName + "filePattern", diags) == null &&
getStringOrNull(params, baseName + "suffix", diags) == null &&
getStringOrNull(params, baseName + "mimeType", diags) == null) {
// This is a blank template ... ignore it.
continue;
}
// We have a template 'i' ...
DatafileTemplate template = new DatafileTemplate();
template.setFilePattern(getNonEmptyString(params, baseName + "filePattern", diags));
template.setSuffix(getNonEmptyString(params, baseName + "suffix", diags));
template.setMimeType(getNonEmptyString(params, baseName + "mimeType", diags));
if (getStringOrNull(params, baseName + "minimumSize", diags) != null) {
int size = getInteger(params, baseName + "minimumSize", diags);
if (size < 0) {
addDiag(diags, baseName + "minimumSize",
"minimum size cannot be negative");
} else {
template.setMinimumSize(size);
}
}
template.setOptional(getBoolean(params, baseName+ "optional", diags));
for (DatafileTemplate existing : templates) {
if (template.getFilePattern() != null &&
template.getFilePattern().equals(existing.getFilePattern())) {
addDiag(diags, baseName + "filePattern",
"this file pattern has already been used");
}
}
templates.add(template);
}
res.setDatafileTemplates(templates);
// Set the key attributes at the end after we've done the uniqueness checks.
// If we do them earlier, they may trigger DB level constraint errors due
// premature updates. But we DO need to do them even if the checks fail
// so that the (possibly) bad values show up in the form.
res.setFacilityName(facilityName);
res.setAddress(address);
res.setLocalHostId(localHostId);
return diags;
}
private void checkFoldername(String folderName, Long id,
Map<String, String> diags, EntityManager em) {
if (folderName == null) {
return;
}
File local = this.uncNameMapper.mapUncPathname(folderName);
if (local == null) {
addDiag(diags, "folderName",
"this isn't the name of a configured Samba share");
return;
}
TypedQuery<Object[]> query;
if (id == null) {
query = em.createQuery(
"select f.facilityName, f.folderName from Facility f ",
Object[].class);
} else {
query = em.createQuery(
"select f.facilityName, f.folderName from Facility f " +
"where f.id != :id",
Object[].class);
query.setParameter("id", id.longValue());
}
for (Object[] res : query.getResultList()) {
if (folderName.equalsIgnoreCase((String) res[1])) {
addDiag(diags, "folderName",
"this Samba share is already used for Facility '" +
res[0] + "'");
return;
}
File local2 = this.uncNameMapper.mapUncPathname((String) res[1]);
if (local.toString().startsWith(local2.toString()) ||
local2.toString().startsWith(local.toString())) {
addDiag(diags, "folderName",
"the directory tree for this Samba share overlaps with " +
"the directory tree for Facility '" + res[0] + "'");
return;
}
}
}
private void checkAddressability(String address, String localHostId, Long id,
boolean multiplexed, Map<String, String> diags, EntityManager em) {
// Check that the supplied address is valiid / resolves.
InetAddress inetAddr;
try {
inetAddr = InetAddress.getByName(address);
} catch (UnknownHostException ex) {
addDiag(diags, "address", ex.getMessage());
return;
}
// If this facility has no local host id, then its address must
// be unique. We need to extract all other facility addresses
// with no associated local host id, resolve them and compare the
// IP addresses.
TypedQuery<Object[]> query;
if (id == null) {
query = em.createQuery(
"select f.facilityName, f.address, f.multiplexed from Facility f",
Object[].class);
} else {
query = em.createQuery(
"select f.facilityName, f.address, f.multiplexed from Facility f " +
"where f.id != :id",
Object[].class);
query.setParameter("id", id.longValue());
}
List<Object[]> others = query.getResultList();
for (Object[] other : others) {
try {
InetAddress otherAddr = InetAddress.getByName((String) other[1]);
Boolean otherMultiplexed = (Boolean) other[2];
if (otherAddr.equals(inetAddr) && /* compares IP addresses */
!(multiplexed && otherMultiplexed)) {
addDiag(diags, "address",
"address also used by facility '" + other[0] + "'. " +
"Resolve the address conflict or mark " +
"both facilities as 'multiplexed'");
}
} catch (UnknownHostException ex) {
// We cannot report this to the user ...
LOG.warn("Cannot resolve hostname / address " + other[1] +
" for facility '" + other[0] + "'");
}
}
}
private void checkFacilityNameUnique(String name, Long id,
Map<String, String> diags, EntityManager em) {
TypedQuery<String> query;
if (id == null) {
query = em.createQuery(
"select f.facilityName from Facility f " +
"where f.facilityName = :name",
String.class);
} else {
query = em.createQuery(
"select f.facilityName from Facility f " +
"where f.facilityName = :name and f.id != :id",
String.class);
query.setParameter("id", id.longValue());
}
query.setParameter("name", name);
List<String> names = query.getResultList();
if (!names.isEmpty()) {
addDiag(diags, "facilityName", "facility name '" + name +
"' already used for another facility");
}
}
private void checkLocalHostIdUnique(String localHostId, Long id,
Map<String, String> diags, EntityManager em) {
TypedQuery<String> query;
if (id == null) {
query = em.createQuery(
"select f.facilityName from Facility f " +
"where f.localHostId = :localHostId",
String.class);
} else {
query = em.createQuery(
"select f.facilityName from Facility f " +
"where f.localHostId = :localHostId and f.id != :id",
String.class);
query.setParameter("id", id.longValue());
}
query.setParameter("localHostId", localHostId);
List<String> names = query.getResultList();
if (!names.isEmpty()) {
addDiag(diags, "localHostId", "local host id '" + localHostId +
"' already assigned to facility '" + names.get(0) + "'");
}
}
private void addDiag(Map<String, String> diags, String key,
String message) {
if (diags.get(key) == null) {
diags.put(key, message);
}
}
private String getStringOrNull(Map<?, ?> params, String key, Map<String, String> diags) {
String str = getString(params, key, diags, false);
if (str != null && str.isEmpty()) {
return null;
} else {
return str;
}
}
private String getString(Map<?, ?> params, String key,
Map<String, String> diags, boolean canBeMissing) {
String[] values = (String[]) params.get(key);
if (values != null && values.length > 0) {
return values[0].trim();
} else {
if (!canBeMissing) {
diags.put(key, "field is missing");
}
return null;
}
}
private String getNonEmptyString(Map<?, ?> params, String key, Map<String, String> diags) {
String str = getString(params, key, diags, false);
if (str != null && str.isEmpty()) {
addDiag(diags, key, "this field must not be empty");
}
return str;
}
private int getInteger(Map<?, ?> params, String key, Map<String, String> diags) {
String str = getNonEmptyString(params, key, diags);
try {
return Integer.parseInt(str);
} catch (NumberFormatException ex) {
addDiag(diags, key, "this value is not a valid integer");
return 0;
}
}
private boolean getBoolean(Map<?, ?> params, String key, Map<String, String> diags) {
String str = getString(params, key, diags, true);
if (str == null || str.isEmpty() || str.equalsIgnoreCase("false")) {
return false;
} else if (str.equalsIgnoreCase("true")) {
return true;
} else {
addDiag(diags, key, "this value must be 'true' or 'false' / ''");
return false;
}
}
}