/*
* 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 fr.gael.dhus.DHuS;
import fr.gael.dhus.database.dao.ConfigurationDao;
import fr.gael.dhus.database.dao.UserDao;
import fr.gael.dhus.database.dao.interfaces.DHusDumpException;
import fr.gael.dhus.database.object.User;
import fr.gael.dhus.database.object.User.PasswordEncryption;
import fr.gael.dhus.database.object.config.Configuration;
import fr.gael.dhus.database.object.config.search.SolrConfiguration;
import fr.gael.dhus.service.exception.UserBadEncryptionException;
import fr.gael.dhus.system.config.ConfigurationManager;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Properties;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.comparator.NameFileComparator;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.embedded.EmbeddedSolrServer;
import org.apache.solr.core.CoreContainer;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hsqldb.lib.tar.DbBackupMain;
import org.hsqldb.lib.tar.TarMalformatException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Caching;
import org.springframework.orm.hibernate3.HibernateCallback;
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;
@Service
public class SystemService extends WebService
{
private static final String BACKUP_DATABASE_NAME = "database";
private static final String BACKUP_INDEX_NAME = "index";
private static final Logger LOGGER = LogManager.getLogger(SystemService.class);
public static final String RESTORATION_PROPERTIES =
"dhus-restoration-system.properties";
private static int SOLR_VERSION=4;
@Autowired
private ConfigurationDao cfgDao;
@Autowired
private UserDao userDao;
@Autowired
private ConfigurationManager cfgManager;
@PreAuthorize ("hasRole('ROLE_SYSTEM_MANAGER')")
@Transactional (readOnly=true, propagation=Propagation.REQUIRED)
public Configuration getCurrentConfiguration ()
{
return cfgDao.getCurrentConfiguration ();
}
@PreAuthorize ("hasRole('ROLE_SYSTEM_MANAGER')")
@Transactional (readOnly=false, propagation=Propagation.REQUIRED)
public Configuration saveSystemSettings (Configuration cfg) throws
IllegalArgumentException, IllegalAccessException,
InvocationTargetException, CloneNotSupportedException
{
Configuration db_cfg = cfgDao.getCurrentConfiguration ();
cfg = cfg.completeWith (db_cfg);
db_cfg.setCronConfiguration (cfg.getCronConfiguration ());
db_cfg.setGuiConfiguration (cfg.getGuiConfiguration ());
db_cfg.setMessagingConfiguration (cfg.getMessagingConfiguration ());
db_cfg.setNetworkConfiguration (cfg.getNetworkConfiguration ());
db_cfg.setProductConfiguration (cfg.getProductConfiguration ());
db_cfg.setSearchConfiguration (cfg.getSearchConfiguration ());
db_cfg.setServerConfiguration (cfg.getServerConfiguration ());
db_cfg.setSystemConfiguration (cfg.getSystemConfiguration ());
cfgDao.update (db_cfg);
return cfgDao.getCurrentConfiguration ();
}
@PreAuthorize ("hasRole('ROLE_SYSTEM_MANAGER')")
@Transactional (readOnly=false, propagation=Propagation.REQUIRED)
public Configuration resetToDefaultConfiguration () throws Exception
{
cfgManager.reloadConfiguration ();
return cfgDao.getCurrentConfiguration ();
}
@PreAuthorize ("hasRole('ROLE_SYSTEM_MANAGER')")
@Transactional (readOnly=false, propagation=Propagation.REQUIRED)
@Caching (evict = {
@CacheEvict (value = "user", allEntries = true),
@CacheEvict (value = "userByName", allEntries = true)})
public void changeRootPassword (String new_pwd, String old_pwd)
{
User root =
userDao.getByName (
cfgManager.getAdministratorConfiguration ().getName ());
PasswordEncryption encryption = root.getPasswordEncryption ();
if (encryption != PasswordEncryption.NONE)
{
try
{
MessageDigest md = MessageDigest.getInstance(
encryption.getAlgorithmKey());
old_pwd = new String(
Hex.encode(md.digest(old_pwd.getBytes("UTF-8"))));
}
catch (Exception e)
{
throw new UserBadEncryptionException (
"There was an error while encrypting password of root user",
e);
}
}
if ( (old_pwd == null) || ("".equals (old_pwd)) ||
( !root.getPassword ().equals (old_pwd)))
throw new SecurityException ("Wrong password.");
if ( (new_pwd == null) || "".equals (new_pwd.trim ()))
throw new SecurityException ("New password cannot be empty.");
String password = new_pwd.trim ();
root.setPassword (password);
userDao.update (root);
}
@PreAuthorize ("hasRole('ROLE_SYSTEM_MANAGER')")
public List<Date> getDumpDatabaseList ()
{
List<Date>timestamps = new ArrayList<Date> ();
File path_file = new File (cfgManager.getDatabaseConfiguration ()
.getDumpPath ());
File[]lst=path_file.listFiles (new FilenameFilter()
{
@Override
public boolean accept (File dir, String name)
{
if (name.startsWith ("dump-"))
return true;
return false;
}
});
if (lst == null)
{
return timestamps;
}
for (File f:lst)
{
String stimesamp = f.getName ().replaceAll ("dump-(.*)", "$1");
long timestamp = Long.parseLong (stimesamp);
Date date = new Date (timestamp);
timestamps.add (date);
}
Collections.sort (timestamps, Collections.reverseOrder());
return timestamps;
}
/**
* Restores the desired dump of the database and Solr index. To restore the
* system must be stopped.This method produces the properties file to
* generates new restored database and Solr index.
* @param date of the dump to restore.
* @throws DHusDumpException if date does not corresponds to an
* existing dump.
*/
public void restoreDumpDatabase (Date date)
{
File retorationDir = new File (
cfgManager.getDatabaseConfiguration ().getDumpPath (),
String.format ("dump-%020d", date.getTime ()));
if ( !(retorationDir.exists () && retorationDir.isDirectory ()))
{
throw new DHusDumpException ("Dump of \"" + date + "\" not found");
}
try
{
String path = retorationDir.getAbsolutePath ();
SolrConfiguration solrConfig = cfgManager.getSolrConfiguration ();
FileWriter writer = new FileWriter (RESTORATION_PROPERTIES);
// Database
writer.append ("dhus.db.backup=")
.append (path).append ("/")
.append (BACKUP_DATABASE_NAME).append (".tar.gz");
writer.append ('\n');
writer.append ("dhus.db.location=").append (getDBDirectory ());
writer.append ('\n');
// Solr index
writer.append ("dhus.solr.backup.name=").append (BACKUP_INDEX_NAME);
writer.append ('\n');
writer.append ("dhus.solr.backup.location=").append (path);
writer.append ('\n');
writer.append ("dhus.solr.core.name=").append (solrConfig.getCore ());
writer.append ('\n');
writer.append ("dhus.solr.home=").append (solrConfig.getPath ());
writer.append ('\n');
writer.flush ();
writer.close ();
}
catch (IOException e)
{
LOGGER.warn("Can not perform restoration.", e);
return;
}
DHuS.stop (8);
}
private String getDBDirectory ()
{
String hsqlpath = cfgManager.getDatabaseConfiguration ().getPath ();
File db =
new File (hsqlpath.replace ('/', File.separatorChar)).getParentFile ();
return db.getPath ();
}
/**
* Generate a backup of DHuS system (database and Solr index)
*/
public void dumpDatabase()
{
Date date = new Date ();
String dirName = String.format ("dump-%020d", date.getTime ());
File dir = new File (
cfgManager.getDatabaseConfiguration ().getDumpPath (), dirName);
if ( !(dir.mkdirs ()))
{
LOGGER.error("Can not create directory to save backup system.");
return;
}
String path = dir.getAbsolutePath ();
if ( !(backupDatabase (path) && backupSolr (path)))
{
LOGGER.warn("Deleting invalid backup system...");
try
{
FileUtils.deleteDirectory (dir);
}
catch (IOException e)
{
LOGGER.error("Can not delete invalid backup system: " +
path + ". Please delete it manually.");
}
}
}
/**
* Performs a backup of database.
* @param backupDirectory directory where put the backup.
* @return true if backup is successful, otherwise false.
*/
@Transactional (readOnly=false, propagation=Propagation.REQUIRED)
private boolean backupDatabase (final String backupDirectory)
{
return userDao.getHibernateTemplate ().execute (
new HibernateCallback<Boolean> ()
{
@Override
public Boolean doInHibernate (Session session) throws
HibernateException, SQLException
{
String backup = backupDirectory + "/" +
BACKUP_DATABASE_NAME + ".tar.gz";
String sql = "BACKUP DATABASE TO '" + backup +
"' NOT BLOCKING";
try
{
session.createSQLQuery (sql).executeUpdate ();
}
catch (HibernateException e)
{
return Boolean.FALSE;
}
return Boolean.TRUE;
}
});
}
/**
* Performs a backup of Solr index.
* @param backupDirectory directory where put the backup.
* @return true if backup is successful, otherwise false.
*/
private boolean backupSolr (final String backupDirectory)
{
StringBuilder request = new StringBuilder ();
request.append (cfgManager.getServerConfiguration ().getUrl ());
request.append ("/solr/dhus/replication?");
request.append ("command=backup&location=").append (backupDirectory);
request.append ("&name=").append (BACKUP_INDEX_NAME);
try
{
URL url = new URL (request.toString ());
HttpURLConnection con = (HttpURLConnection) url.openConnection ();
InputStream input = con.getInputStream ();
StringBuilder response = new StringBuilder ();
byte[] buff = new byte[1024];
int length;
while ((length = input.read (buff)) != -1)
{
response.append (new String (buff, 0, length));
}
input.close ();
con.disconnect ();
LOGGER.debug(response.toString ());
}
catch (IOException e)
{
return Boolean.FALSE;
}
return Boolean.TRUE;
}
public void cleanDumpDatabase(int keepno)
{
File[]dumps = new File(
cfgManager.getDatabaseConfiguration ().getDumpPath ())
.listFiles(new FilenameFilter()
{
@Override
public boolean accept(File path, String name)
{
if (name.startsWith("dump-"))
return true;
return false;
}
});
if ((dumps!=null) && (dumps.length > keepno))
{
Arrays.sort(dumps, NameFileComparator.NAME_COMPARATOR);
int last = dumps.length - keepno;
for (int index=0; index<last; index++)
{
File dir = dumps[index];
try
{
Date date = new Date (Long.parseLong (dir.getName ()
.replaceAll ("dump-(.*)", "$1")));
LOGGER.info("Cleaned dump of " + date);
FileUtils.deleteDirectory(dir);
}
catch (IOException e)
{
LOGGER.warn("Cannot delete directory " + dir.getPath() + " (" +
e.getMessage() + ")");
}
}
}
}
/**
* Restores DHuS in a previous state.
*/
public static boolean restore ()
{
File restoreConfig = new File (RESTORATION_PROPERTIES);
if (restoreConfig.exists () && restoreConfig.isFile ())
{
LOGGER.info("Performing restoration DHuS system...");
try (FileInputStream stream = new FileInputStream (restoreConfig))
{
Properties properties = new Properties ();
properties.load (stream);
restoreDatabase (properties);
restoreSolrIndex (properties);
}
catch (UnsupportedOperationException e)
{
LOGGER.error("Incomplete DHuS restoration file.", e);
// DHuS integrity database and Solr index
System.setProperty ("Archive.check", "true");
}
catch (Exception e)
{
LOGGER.fatal("Restoration failure.", e);
return false;
}
finally
{
restoreConfig.delete ();
}
}
return true;
}
/**
* Performs database restoration.
* No need of transaction here: DBmain is called before starting datasource.
*
* @param properties properties containing arguments to execute the restoration.
*/
private static void restoreDatabase (Properties properties) throws
IOException, TarMalformatException
{
String backup = properties.getProperty ("dhus.db.backup");
String location = properties.getProperty ("dhus.db.location");
if (backup == null || location == null)
{
throw new UnsupportedOperationException ();
}
FileUtils.deleteDirectory (new File(location));
String[] args = {"--extract", backup, location};
DbBackupMain.main (args);
LOGGER.info("Database restored.");
}
private static void restoreSolrIndex (Properties properties) throws
IOException, SolrServerException
{
if (SOLR_VERSION==4)
restoreSolr4Index(properties);
else
restoreSolr5Index(properties);
}
/**
* Performs Solr restoration.
*
* @param properties properties containing arguments to execute the restoration.
*/
private static void restoreSolr5Index (Properties properties) throws
IOException, SolrServerException
{
String solrHome = properties.getProperty ("dhus.solr.home");
String coreName = properties.getProperty ("dhus.solr.core.name");
final String name = properties.getProperty ("dhus.solr.backup.name");
final String location = properties.getProperty (
"dhus.solr.backup.location");
if (solrHome == null || coreName == null || name == null ||
location == null)
{
throw new UnsupportedOperationException ();
}
System.setProperty ("solr.solr.home", solrHome);
CoreContainer core = new CoreContainer (solrHome);
EmbeddedSolrServer server = new EmbeddedSolrServer (core, coreName);
try
{
server.getCoreContainer ().load ();
SolrQuery query = new SolrQuery();
query.setRequestHandler("/replication");
query.set("command", "restore");
query.set("name", name);
query.set("location", location);
server.query(query);
LOGGER.info("SolR indexes restored.");
}
finally
{
server.close();
}
}
/**
* Performs Solr restoration.
*
* @param properties properties containing arguments to execute the restoration.
*/
private static void restoreSolr4Index (Properties properties) throws
IOException, SolrServerException
{
String solr_home = properties.getProperty ("dhus.solr.home");
String core_name = properties.getProperty ("dhus.solr.core.name");
final String name = properties.getProperty ("dhus.solr.backup.name");
final String location = properties.getProperty (
"dhus.solr.backup.location");
if (solr_home==null || core_name==null || name==null || location==null)
throw new UnsupportedOperationException ();
System.setProperty ("solr.solr.home", solr_home);
File index_path = new File (location, "snapshot."+name);
File target_path = Paths.get (solr_home, core_name, "data", name).toFile ();
if (!index_path.exists())
throw new UnsupportedOperationException (
"solr source to restore not found (" + index_path + ").");
if (!target_path.exists())
throw new UnsupportedOperationException (
"solr restore path not found (" + target_path + ").");
FileUtils.cleanDirectory(target_path);
FileUtils.copyDirectory(index_path, target_path);
LOGGER.info("SolR indexes restored.");
}
}