Dépôt officiel du code source de l'ERP OpenConcerto
Rev 177 | Blame | Compare with Previous | Last modification | View Log | RSS feed
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 2011-2019 OpenConcerto, by ILM Informatique. All rights reserved.
*
* The contents of this file are subject to the terms of the GNU General Public License Version 3
* only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
* copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each file.
*/
package org.openconcerto.sql;
import org.openconcerto.sql.element.SQLElementDirectory;
import org.openconcerto.sql.element.SQLElementNamesFromXML;
import org.openconcerto.sql.model.DBRoot;
import org.openconcerto.sql.model.DBStructureItem;
import org.openconcerto.sql.model.DBSystemRoot;
import org.openconcerto.sql.model.FieldMapper;
import org.openconcerto.sql.model.HierarchyLevel;
import org.openconcerto.sql.model.SQLBase;
import org.openconcerto.sql.model.SQLDataSource;
import org.openconcerto.sql.model.SQLFilter;
import org.openconcerto.sql.model.SQLRow;
import org.openconcerto.sql.model.SQLServer;
import org.openconcerto.sql.model.SQLSystem;
import org.openconcerto.sql.request.SQLFieldTranslator;
import org.openconcerto.sql.users.UserManager;
import org.openconcerto.sql.users.rights.UserRightsManager;
import org.openconcerto.utils.BaseDirs;
import org.openconcerto.utils.CollectionUtils;
import org.openconcerto.utils.FileUtils;
import org.openconcerto.utils.LogUtils;
import org.openconcerto.utils.MultipleOutputStream;
import org.openconcerto.utils.NetUtils;
import org.openconcerto.utils.ProductInfo;
import org.openconcerto.utils.RTInterruptedException;
import org.openconcerto.utils.ReflectUtils;
import org.openconcerto.utils.StreamUtils;
import org.openconcerto.utils.Tuple2;
import org.openconcerto.utils.Value;
import org.openconcerto.utils.cc.IClosure;
import org.openconcerto.utils.cc.IPredicate;
import org.openconcerto.utils.i18n.TranslationManager;
import java.awt.Dialog.ModalityType;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import java.util.Objects;
import java.util.Properties;
import java.util.ResourceBundle.Control;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.logging.FileHandler;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.Session;
import net.jcip.annotations.GuardedBy;
import net.jcip.annotations.ThreadSafe;
/**
* A configuration which takes its values primarily from Properties. You should also subclass its
* different protected get*() methods. Used properties :
* <dl>
* <dt>server.ip</dt>
* <dd>ip address of the SQL server</dd>
* <dt>server.driver</dt>
* <dd>the RDBMS, see {@link org.openconcerto.sql.model.SQLDataSource#DRIVERS}</dd>
* <dt>server.login</dt>
* <dd>the login</dd>
* <dt>server.password</dt>
* <dd>the password</dd>
* <dt>server.base</dt>
* <dd>the database (only used for systems where the root level is not SQLBase)</dd>
* <dt>base.root</dt>
* <dd>the name of the DBRoot</dd>
* <dt>customer</dt>
* <dd>used to find the default base and the mapping</dd>
* <dt>JDBC_CONNECTION*</dt>
* <dd>see {@link #JDBC_CONNECTION}</dd>
* </dl>
*
* @author Sylvain CUAZ
* @see #getShowAs()
*/
@ThreadSafe
public class PropsConfiguration extends Configuration {
/**
* Properties prefixed with this string will be passed to the datasource as connection
* properties.
*/
public static final String JDBC_CONNECTION = "jdbc.connection.";
public static final String LOG = "log.level.";
/**
* If this system property is set to <code>true</code> then {@link #setupLogging(String)} will
* redirect {@link System#err} and {@link System#out}.
*/
public static final String REDIRECT_TO_FILE = "redirectToFile";
/**
* Must be one of {@link StandardStreamsDest}.
*/
public static final String STD_STREAMS_DESTINATION = "stdStreamsDest";
// properties cannot contain null, so to be able to override a default, a non-null value
// meaning empty must be chosen (as setProperty(name, null) is the same as remove(name) i.e.
// get the value from the default properties)
public static final String EMPTY_PROP_VALUE;
protected static enum FileMode {
IN_JAR, NORMAL_FILE
};
// eg 2009-03/26_thursday : ordered and grouped by month
private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM/dd_EEEE");
public static String getHostname() {
final InetAddress addr;
try {
addr = InetAddress.getLocalHost();
} catch (final UnknownHostException e) {
e.printStackTrace();
return "local";
}
return addr.getHostName();
}
protected static Properties create(final InputStream f, final Properties defaults) throws IOException {
final Properties props = new Properties(defaults);
if (f != null) {
props.load(f);
f.close();
}
return props;
}
public static final Properties DEFAULTS;
static {
DEFAULTS = new Properties();
final File wd = new File(System.getProperty("user.dir"));
DEFAULTS.setProperty("wd", wd.getPath());
DEFAULTS.setProperty("customer", "test");
DEFAULTS.setProperty("server.ip", "127.0.0.1");
DEFAULTS.setProperty("server.login", "root");
EMPTY_PROP_VALUE = "";
assert EMPTY_PROP_VALUE != null;
}
private final Properties props;
// sql tree
@GuardedBy("treeLock")
private SQLServer server;
@GuardedBy("treeLock")
private DBSystemRoot sysRoot;
@GuardedBy("treeLock")
private DBRoot root;
// created from root
@GuardedBy("treeLock")
private UserManager uMngr;
@GuardedBy("treeLock")
private UserRightsManager urMngr;
// rest
@GuardedBy("restLock")
private ProductInfo productInfo;
@GuardedBy("restLock")
private SQLFilter filter;
private final Addable<SQLElementDirectory> directory;
@GuardedBy("restLock")
private File wd;
@GuardedBy("restLock")
private BaseDirs baseDirs;
@GuardedBy("restLock")
private File logDir;
@GuardedBy("restLock")
private Locale locale = Locale.getDefault();
private final boolean inIDE;
// split sql tree and the rest since creating the tree is costly
// and nodes are inter-dependant, while the rest is mostly fast
// different instances, otherwise lock every Conf instances
private final Object treeLock = new String("treeLock");
private final Object restLock = new String("everythingElseLock");
// SSL
@GuardedBy("treeLock")
private Session conn;
@GuardedBy("treeLock")
private int tunnelLocalPort = -1;
@GuardedBy("treeLock")
private Thread sslThread;
private FieldMapper fieldMapper;
@GuardedBy("treeLock")
private boolean destroyed;
public PropsConfiguration() throws IOException {
this(new File("fwk_SQL.properties"), DEFAULTS);
}
/**
* Creates a new setup.
*
* @param f the file from which to load.
* @param defaults the defaults, can be <code>null</code>.
* @throws IOException if an error occurs while reading f.
*/
public PropsConfiguration(final File f, final Properties defaults) throws IOException {
this(new FileInputStream(f), defaults);
}
public PropsConfiguration(final InputStream f, final Properties defaults) throws IOException {
this(create(f, defaults));
}
public PropsConfiguration(final Properties props) {
this.props = props;
// SQLElementDirectory is thread-safe
this.directory = new Addable<SQLElementDirectory>() {
@Override
protected SQLElementDirectory create() {
final SQLElementDirectory res = createDirectory();
res.setTranslator(createTranslator(res));
return res;
}
@Override
protected void add(SQLElementDirectory obj, Configuration conf) {
obj.putAll(conf.getDirectory());
}
@Override
protected void destroy(Future<SQLElementDirectory> future) {
super.destroy(future);
try {
future.get().destroy();
} catch (Exception e) {
throw new IllegalStateException("Couldn't destroy the directory", e);
}
}
};
this.inIDE = Boolean.getBoolean("inIDE");
this.setUp();
}
@Override
public void destroy() {
synchronized (this.treeLock) {
if (this.destroyed)
return;
this.destroyed = true;
if (this.server != null) {
this.server.destroy();
}
closeSSLConnection();
if (this.uMngr != null) {
UserManager.getSingletonManager().clearInstanceIfSame(this.uMngr);
this.uMngr.destroy();
}
if (this.urMngr != null) {
UserRightsManager.getSingletonManager().clearInstanceIfSame(this.urMngr);
this.urMngr.destroy();
}
}
this.directory.destroy();
super.destroy();
}
public final boolean isDestroyed() {
synchronized (this.treeLock) {
return this.destroyed;
}
}
private final void checkDestroyed() {
checkDestroyed(this.isDestroyed());
}
static private final void checkDestroyed(final boolean d) {
if (d)
throw new IllegalStateException("Destroyed");
}
public final boolean isInIDE() {
return this.inIDE;
}
public final String getProperty(final String name) {
return this.props.getProperty(name);
}
public final String getProperty(final String name, final String def) {
return this.props.getProperty(name, def);
}
// since null aren't allowed, null means remove
protected final void setProperty(final String name, final String val) {
if (val == null)
this.props.remove(name);
else
this.props.setProperty(name, val);
}
protected final void setProductInfo(final ProductInfo productInfo) {
synchronized (this.restLock) {
this.productInfo = productInfo;
}
}
private void setUp() {
synchronized (this.treeLock) {
this.destroyed = false;
this.server = null;
this.sysRoot = null;
this.root = null;
}
synchronized (this.restLock) {
this.setProductInfo(ProductInfo.getInstance());
this.setFilter(null);
}
}
public final SQLSystem getSystem() {
return SQLSystem.get(this.getProperty("server.driver"));
}
protected String getLogin() {
return this.getProperty("server.login");
}
protected String getPassword() {
return this.getProperty("server.password");
}
public String getDefaultBase() {
final boolean rootIsBase = this.getSystem().getDBRootLevel().equals(HierarchyLevel.SQLBASE);
return rootIsBase ? this.getRootName() : this.getSystemRootName();
}
private final String toClassName(final String rsrcName) {
if (rsrcName.charAt(0) == '/')
return rsrcName.substring(1).replace('/', '.');
else
return this.getResourceWD().getPackage().getName() + '.' + rsrcName.replace('/', '.');
}
/**
* Return the correct stream depending on file mode. If file mode is
* {@link FileMode#NORMAL_FILE} it will first check if a file named <code>name</code> exists,
* otherwise it will look in the jar.
*
* @param name name of the stream, eg /ilm/f.xml.
* @return the corresponding stream, or <code>null</code> if not found.
*/
public final InputStream getStream(final String name) {
final File f = getFile(name);
if (mustUseClassloader(f)) {
return getResourceWD().getResourceAsStream(name);
} else
try {
return new FileInputStream(f);
} catch (final FileNotFoundException e) {
return null;
}
}
// the "working directory" where relative names are resolved
protected Class<? extends PropsConfiguration> getResourceWD() {
return this.getClass();
}
private File getFile(final String name) {
return new File(name.startsWith("/") ? name.substring(1) : name);
}
private boolean mustUseClassloader(final File f) {
return this.getFileMode() == FileMode.IN_JAR || !f.exists();
}
public final String getResource(final String name) {
final File f = getFile(name);
if (mustUseClassloader(f)) {
return this.getResourceWD().getResource(name).toExternalForm();
} else {
return f.getAbsolutePath();
}
}
protected FileMode getFileMode() {
return FileMode.IN_JAR;
}
protected final DBRoot createRoot() {
final Value<String> rootName = getRootNameValue();
if (rootName.hasValue())
return this.getSystemRoot().getRoot(rootName.getValue());
else
throw new NullPointerException("no rootname");
}
// return null, if none desired
protected UserRightsManager createUserRightsManager(final DBRoot root) {
return UserRightsManager.getSingletonManager().setInstanceIfNone(root);
}
public String getRootName() {
return this.getProperty("base.root", EMPTY_PROP_VALUE);
}
public final Value<String> getRootNameValue() {
final String res = getRootName();
return res == null || EMPTY_PROP_VALUE.equals(res) ? Value.<String> getNone() : Value.getSome(res);
}
protected SQLFilter createFilter() {
return SQLFilter.create(this.getSystemRoot(), getDirectory());
}
public String getWanHostAndPort() {
final String wanAddr = getProperty("server.wan.addr");
final String wanPort = getProperty("server.wan.port", "22");
return wanAddr + ":" + wanPort;
}
public final boolean isUsingSSH() {
synchronized (this.treeLock) {
return this.sslThread != null;
}
}
public final int getTunnelLocalPort() {
synchronized (this.treeLock) {
return this.tunnelLocalPort;
}
}
public final boolean hasWANProperties() {
final String wanAddr = getProperty("server.wan.addr");
final String wanPort = getProperty("server.wan.port");
return hasWANProperties(wanAddr, wanPort);
}
private final boolean hasWANProperties(String wanAddr, String wanPort) {
return wanAddr != null && wanPort != null;
}
protected SQLServer createServer() {
final String wanAddr = getProperty("server.wan.addr");
final String wanPort = getProperty("server.wan.port");
if (!hasWANProperties(wanAddr, wanPort))
return doCreateServer();
final Tuple2<String, Integer> serverAndPort = parseServerAddressAndPort();
final String serverAndPortString = serverAndPort.get0() + ":" + serverAndPort.get1();
// if wanAddr is specified, always include it in ID, that way if we connect through the LAN
// or through the WAN we have the same ID
final String serverID = "tunnel to " + wanAddr + ":" + wanPort + " then " + serverAndPortString;
final Logger log = Log.get();
Exception origExn = null;
if (!"true".equals(getProperty("server.wan.only"))) {
try {
final SQLServer defaultServer = doCreateServer(serverAndPortString, null, serverID);
// works since all ds params are provided by doCreateServer()
defaultServer.getSystemRoot(getSystemRootName());
// ok
log.config("using " + defaultServer);
return defaultServer;
} catch (final RuntimeException e) {
origExn = e;
// on essaye par SSL
log.config(e.getLocalizedMessage());
}
assert origExn != null;
}
final SQLServer serverThruSSL;
try {
log.info("Connecting with SSL to " + wanAddr + ":" + wanPort);
this.openSSLConnection(wanAddr, Integer.valueOf(wanPort), serverAndPort.get0(), serverAndPort.get1());
serverThruSSL = doCreateServer("localhost:" + this.tunnelLocalPort, null, serverID);
serverThruSSL.getSystemRoot(getSystemRootName());
} catch (final Exception e) {
// even if the tunnel was set up successfully, close it if the SQLServer couldn't be
// created, that way the next time createServer() is called, we can start again the
// whole process (e.g. checking properties, retrying non-WAN server) without having to
// worry about a lingering tunnel.
this.closeSSLConnection();
// no datasource will remain that uses the port, so forget about it and find a new one
// the next time
this.tunnelLocalPort = -1;
final IllegalStateException exn = new IllegalStateException("Couldn't connect to the DB through SSL", e);
if (origExn != null)
exn.addSuppressed(origExn);
throw exn;
}
return serverThruSSL;
}
// TODO add and use server.port
public final Tuple2<String, Integer> parseServerAddressAndPort() {
final String serverPropVal = getProperty("server.ip");
final List<String> serverAndPort = Arrays.asList(serverPropVal.split(":"));
if (serverAndPort.size() != 2)
throw new IllegalStateException("Not in 'host:port' format : " + serverPropVal);
return Tuple2.create(serverAndPort.get(0), Integer.valueOf(serverAndPort.get(1)));
}
protected final void reconnectSSL() throws Exception {
synchronized (this.treeLock) {
// already destroyed or still OK
if (this.conn == null || this.conn.isConnected())
return;
Log.get().log(Level.WARNING, "SSL disconnected, trying to reconnect");
final String wanAddr = getProperty("server.wan.addr");
final String wanPort = getProperty("server.wan.port");
final Tuple2<String, Integer> serverAndPort = parseServerAddressAndPort();
try {
// cannot just call connect() again, as Session is one-time use
// http://flyingjxswithjava.blogspot.fr/2015/03/comjcraftjschjschexception-packet.html
this.openSSLConnection(wanAddr, Integer.valueOf(wanPort), serverAndPort.get0(), serverAndPort.get1());
Log.get().log(Level.WARNING, "SSL successfully reconnected to " + this.conn.getHost() + ":" + this.conn.getPort());
} catch (Exception e) {
// don't leave an SSL connection without a tunnel
this.conn.disconnect();
throw e;
}
}
}
private SQLServer doCreateServer() {
return doCreateServer(null);
}
private SQLServer doCreateServer(final String id) {
return doCreateServer(this.getProperty("server.ip"), null, id);
}
private SQLServer doCreateServer(final String host, final String port, final String id) {
// give login/password as its often the case that they are the same for all the bases of a
// server (mandated for MySQL : when the graph is built, it needs access to all the bases)
final SQLServer res = new SQLServer(getSystem(), host, port, getLogin(), getPassword(), new IClosure<DBSystemRoot>() {
@Override
public void executeChecked(final DBSystemRoot input) {
input.setRootsToMap(getRootsToMap());
initSystemRoot(input);
}
}, new IClosure<SQLDataSource>() {
@Override
public void executeChecked(final SQLDataSource input) {
initDS(input);
}
});
if (id != null)
res.setID(id);
return res;
}
private void openSSLConnection(final String addr, final int port, final String tunnelRemoteHost, final int tunnelRemotePort) {
checkDestroyed();
final String username = getSSLUserName();
final String pass = getSSLPassword();
final boolean reconnect = this.tunnelLocalPort > 0;
boolean isAuthenticated = false;
final JSch jsch = new JSch();
try {
if (pass == null) {
final ByteArrayOutputStream out = new ByteArrayOutputStream(700);
final String name = username + "_dsa";
final InputStream in = getClass().getResourceAsStream(name);
if (in == null)
throw new IllegalStateException("Missing private key " + getClass().getCanonicalName() + "/" + name);
StreamUtils.copy(in, out);
in.close();
jsch.addIdentity(username, out.toByteArray(), null, null);
}
this.conn = jsch.getSession(username, addr, port);
if (pass != null)
this.conn.setPassword(pass);
final Properties config = new Properties();
// Set StrictHostKeyChecking property to no to avoid UnknownHostKey issue
config.put("StrictHostKeyChecking", "no");
// *2 gain
config.put("compression.s2c", "zlib@openssh.com,zlib,none");
config.put("compression.c2s", "zlib@openssh.com,zlib,none");
this.conn.setConfig(config);
// wait no more than 6 seconds for TCP connection
this.conn.setTimeout(6000);
// ATTN for now this just calls setTimeout()
this.conn.setServerAliveInterval(6000);
this.conn.setServerAliveCountMax(1);
this.conn.connect();
afterSSLConnect(this.conn);
isAuthenticated = true;
// keep same local port so that we don't have to change the datasource
final int localPort = reconnect ? this.tunnelLocalPort : NetUtils.findFreePort(5436);
try {
Log.get().info("Creating SSL tunnel from local port " + localPort + " to remote " + tunnelRemoteHost + ":" + tunnelRemotePort);
this.conn.setPortForwardingL(localPort, tunnelRemoteHost, tunnelRemotePort);
} catch (final Exception e1) {
throw new IllegalStateException("Impossible de créer le tunnel sécurisé", e1);
}
assert reconnect == (this.sslThread != null);
if (!reconnect) {
this.tunnelLocalPort = localPort;
// With ServerAliveInterval & ServerAliveCount, the connection should disconnect
// itself in case of network failure, so we try to reconnect it
final Thread t = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(2000);
try {
reconnectSSL();
} catch (Exception e) {
// re-try later
Log.get().log(Level.WARNING, "Error while checking SSL connection", e);
}
} catch (InterruptedException e) {
// used by destroy()
break;
}
}
}
});
t.setDaemon(true);
t.setName("SSL connection watcher");
t.start();
this.sslThread = t;
}
assert this.sslThread != null;
} catch (final Exception e) {
throw new IllegalStateException("Connection failed", e);
}
if (!isAuthenticated)
throw new IllegalStateException("Authentication failed.");
}
protected void afterSSLConnect(Session conn) {
}
public String getSSLUserName() {
return this.getProperty("server.wan.user");
}
protected String getSSLPassword() {
return this.getProperty("server.wan.password");
}
private void closeSSLConnection() {
synchronized (this.treeLock) {
if (this.sslThread != null) {
this.sslThread.interrupt();
this.sslThread = null;
}
if (this.conn != null) {
this.conn.disconnect();
this.conn = null;
}
}
}
// the result can be modified (avoid that each subclass recreates an instance)
// but it can be null (meaning map all)
protected Collection<String> getRootsToMap() {
final String rootsToMap = getProperty("systemRoot.rootsToMap");
if ("*".equals(rootsToMap))
return null;
final Set<String> res = new HashSet<String>();
final Value<String> rootName = getRootNameValue();
if (rootName.hasValue())
res.add(rootName.getValue());
if (rootsToMap != null)
res.addAll(SQLRow.toList(rootsToMap));
return res;
}
// the result can be modified (avoid that each subclass recreates an instance)
protected List<String> getRootPath() {
return new ArrayList<String>(SQLRow.toList(getProperty("systemRoot.rootPath", "")));
}
public String getSystemRootName() {
return this.getProperty("systemRoot");
}
protected DBSystemRoot createSystemRoot() {
// all ds params specified by createServer()
final DBSystemRoot res = this.getServer(false).getSystemRoot(this.getSystemRootName());
setupSystemRoot(res, true);
return res;
}
// to be called after having a data source
protected final void setupSystemRoot(final DBSystemRoot res) {
this.setupSystemRoot(res, false);
}
private void setupSystemRoot(final DBSystemRoot res, final boolean brandNew) {
if (!brandNew)
res.unsetRootPath();
// handle case when the root is not yet created
if (res.getChildrenNames().contains(this.getRootName()))
res.setDefaultRoot(this.getRootName());
for (final String root : getRootPath()) {
// not all the items of the path may exist in every databases (eg Controle.Common)
if (res.getChildrenNames().contains(root))
res.appendToRootPath(root);
}
}
// called at the end of the DBSystemRoot constructor (before having a data source)
protected void initSystemRoot(DBSystemRoot input) {
}
protected void initDS(final SQLDataSource ds) {
ds.setCacheEnabled(true);
// supported by postgreSQL from 9.1-901, see also Connection#setClientInfo
// also supported by MS SQL
final String appID = getAppID();
if (appID != null)
ds.addConnectionProperty("ApplicationName", appID);
propIterate(new IClosure<String>() {
@Override
public void executeChecked(final String propName) {
final String jdbcName = propName.substring(JDBC_CONNECTION.length());
ds.addConnectionProperty(jdbcName, PropsConfiguration.this.getProperty(propName));
}
}, JDBC_CONNECTION);
}
public final void propIterate(final IClosure<String> cl, final String startsWith) {
this.propIterate(cl, new IPredicate<String>() {
@Override
public boolean evaluateChecked(final String propName) {
return propName.startsWith(startsWith);
}
});
}
/**
* Apply <code>cl</code> for each property that matches <code>filter</code>.
*
* @param cl what to do for each found property.
* @param filter which property to use.
*/
public final void propIterate(final IClosure<String> cl, final IPredicate<? super String> filter) {
for (final String propName : this.props.stringPropertyNames()) {
if (filter.evaluateChecked(propName)) {
cl.executeChecked(propName);
}
}
}
public final Set<String> getPropertyNames() {
return this.props.stringPropertyNames();
}
/**
* For each property starting with {@link #LOG}, set the level of the specified logger to the
* property's value. Eg if there's "log.level.=FINE", the root logger will be set to log FINE
* messages.
*/
public final void setLoggersLevel() {
this.propIterate(new IClosure<String>() {
@Override
public void executeChecked(final String propName) {
final String logName = propName.substring(LOG.length());
LogUtils.getLogger(logName).setLevel(Level.parse(getProperty(propName)));
}
}, LOG);
}
public void setupLogging() {
this.setupLogging("logs");
}
public final void setupLogging(final String dirName) {
this.setupLogging(dirName, getStandardStreamsDestination());
}
protected final StandardStreamsDest getStandardStreamsDestination() {
String propVal = System.getProperty(STD_STREAMS_DESTINATION);
if (propVal == null)
propVal = this.getProperty(STD_STREAMS_DESTINATION);
final StandardStreamsDest res;
if (propVal != null)
res = StandardStreamsDest.valueOf(propVal);
else
res = getStandardStreamsDestinationDefault();
return res;
}
// used if neither system property nor configuration property are set
protected StandardStreamsDest getStandardStreamsDestinationDefault() {
return Boolean.getBoolean(REDIRECT_TO_FILE) ? StandardStreamsDest.ALSO_TO_FILE : StandardStreamsDest.DEFAULT;
}
protected DateFormat getLogDateFormat() {
return DATE_FORMAT;
}
private final File getValidLogDir(final String dirName) {
final File logDir;
try {
final File softLogDir = new File(this.getWD() + "/" + dirName + "/" + getHostname() + "-" + System.getProperty("user.name"));
// don't throw an exception if this fails, we'll fall back to homeLogDir
softLogDir.mkdirs();
if (softLogDir.canWrite()) {
logDir = softLogDir;
} else {
final File homeLogDir = new File(System.getProperty("user.home") + "/." + this.getAppName() + "/" + dirName);
FileUtils.mkdir_p(homeLogDir);
if (homeLogDir.canWrite())
logDir = homeLogDir;
else
throw new IOException("Home log directory not writeable : " + homeLogDir);
}
assert logDir.exists() && logDir.canWrite();
System.out.println("Log directory: " + logDir.getAbsolutePath());
} catch (final IOException e) {
throw new IllegalStateException("unable to create log dir", e);
}
return logDir;
}
static public enum StandardStreamsDest {
DEFAULT(true), ONLY_TO_FILE(false), ALSO_TO_FILE(true);
private final boolean hasDefaultStreams;
private StandardStreamsDest(boolean hasDefaultStreams) {
this.hasDefaultStreams = hasDefaultStreams;
}
public final boolean hasDefaultStreams() {
return this.hasDefaultStreams;
}
}
public final void setupLogging(final String dirName, final StandardStreamsDest stdRedirect) {
final File logDir;
synchronized (this.restLock) {
if (this.logDir != null)
throw new IllegalStateException("Already set to " + this.logDir);
logDir = getValidLogDir(dirName);
this.logDir = logDir;
}
final String logNameBase = this.getAppName() + "_" + getLogDateFormat().format(new Date());
// must be done before setUpConsoleHandler(), otherwise log output not redirected
if (stdRedirect != StandardStreamsDest.DEFAULT) {
final File logFile = new File(logDir, (logNameBase + ".txt"));
try {
FileUtils.mkdir_p(logFile.getParentFile());
System.out.println("Standard output and error file: " + logFile.getAbsolutePath());
final OutputStream fileOut = new FileOutputStream(logFile, true);
final OutputStream out, err;
if (this.isInIDE() || stdRedirect != StandardStreamsDest.ONLY_TO_FILE) {
System.out.println("Redirecting standard output to file and console");
out = new MultipleOutputStream(fileOut, System.out);
System.out.println("Redirecting error output to file and console");
err = new MultipleOutputStream(fileOut, System.err);
} else {
System.out.println("Redirecting standard output to file");
out = fileOut;
System.out.println("Redirecting error output to file");
err = fileOut;
}
System.setErr(new PrintStream(new BufferedOutputStream(err, 128), true));
System.setOut(new PrintStream(new BufferedOutputStream(out, 128), true));
// Takes about 350ms so run it async
new Thread(new Runnable() {
@Override
public void run() {
try {
FileUtils.ln(logFile, new File(logDir, "last.log"));
} catch (final IOException e) {
// the link is not important
e.printStackTrace();
}
}
}).start();
} catch (final Exception e) {
throw new IllegalStateException("Redirection des sorties standards impossible", e);
}
} else {
System.out.println("Standard streams not redirected to file");
}
// removes default
LogUtils.rmRootHandlers();
// add console handler
LogUtils.setUpConsoleHandler();
// add file handler (supports concurrent launches, doesn't depend on date)
try {
final File logFile = new File(logDir, this.getAppName() + "-%u-age%g.log");
FileUtils.mkdir_p(logFile.getParentFile());
System.out.println("Logger logs: " + logFile.getAbsolutePath());
// 2 files of at most 5M, each new launch append
// if multiple concurrent launches %u is used
final FileHandler fh = new FileHandler(logFile.getPath(), 5 * 1024 * 1024, 2, true);
fh.setFormatter(new SimpleFormatter());
Logger.getLogger("").addHandler(fh);
} catch (final Exception e) {
throw new IllegalStateException("Enregistrement du Logger désactivé", e);
}
this.setLoggersLevel();
}
public final File getLogDir() {
synchronized (this.restLock) {
return this.logDir;
}
}
public void tearDownLogging() {
this.tearDownLogging(Boolean.getBoolean(REDIRECT_TO_FILE));
}
public void tearDownLogging(final boolean redirectToFile) {
LogUtils.rmRootHandlers();
if (redirectToFile) {
System.out.close();
System.err.close();
}
}
protected SQLElementDirectory createDirectory() {
return new SQLElementDirectory(getSystemRoot());
}
// Use resource name to be able to use absolute (beginning with /) or relative path (to this
// class)
protected List<String> getMappings() {
return Arrays.asList("mapping", "mapping-" + this.getProperty("customer"));
}
protected SQLFieldTranslator createTranslator(final SQLElementDirectory dir) {
final List<String> mappings = getMappings();
if (mappings.size() == 0)
throw new IllegalStateException("empty mappings");
final SQLFieldTranslator trns = new SQLFieldTranslator(this.getRoot(), dir);
// perhaps listen to UserProps (as in TM)
return loadTranslations(trns, this.getRoot(), mappings);
}
protected final SQLFieldTranslator loadTranslations(final SQLFieldTranslator trns, final DBRoot root, final List<String> mappings) {
final Locale locale = this.getLocale();
final Control cntrl = TranslationManager.getControl();
boolean found = false;
// better to have a translation in the correct language than a translation for the correct
// customer in the wrong language
final String fakeBaseName = "";
for (Locale targetLocale = locale; targetLocale != null && !found; targetLocale = cntrl.getFallbackLocale(fakeBaseName, targetLocale)) {
final List<Locale> langs = cntrl.getCandidateLocales(fakeBaseName, targetLocale);
// SQLFieldTranslator overwrite, so we need to load from general to specific
final ListIterator<Locale> listIterator = CollectionUtils.getListIterator(langs, true);
while (listIterator.hasNext()) {
final Locale lang = listIterator.next();
final SQLElementNamesFromXML elemNames = new SQLElementNamesFromXML(lang);
found |= loadTranslations(trns, PropsConfiguration.class.getResourceAsStream(cntrl.toBundleName("mapping", lang) + ".xml"), root, elemNames);
for (final String m : mappings) {
final String bundleName = cntrl.toBundleName(m, lang);
found |= loadTranslations(trns, this.getStream(bundleName + ".xml"), root, elemNames);
final Class<? extends TranslatorFiller> loadedClass = ReflectUtils.getSubclass(toClassName(bundleName), TranslatorFiller.class);
if (loadedClass != null) {
try {
ReflectUtils.createInstance(loadedClass, this).fill(trns);
} catch (Exception e) {
Log.get().log(Level.WARNING, "Couldn't use " + loadedClass, e);
}
}
}
}
}
return trns;
}
@FunctionalInterface
static public interface TranslatorFiller {
void fill(final SQLFieldTranslator t);
}
static public abstract class AbstractTranslatorFiller implements TranslatorFiller {
private final PropsConfiguration conf;
public AbstractTranslatorFiller(final PropsConfiguration conf) {
this.conf = conf;
}
protected final PropsConfiguration getConf() {
return this.conf;
}
}
private final boolean loadTranslations(final SQLFieldTranslator trns, final InputStream in, final DBRoot root, final SQLElementNamesFromXML elemNames) {
final boolean res = in != null;
// do not force to have one mapping for each client and each locale
if (res)
trns.load(root, in, elemNames);
return res;
}
protected File createWD() {
return new File(this.getProperty("wd"));
}
protected BaseDirs createBaseDirs() {
return BaseDirs.create(getProductInfo(), getAppVariant());
}
// *** add
/**
* Add the passed Configuration to this. If an item is not already created, this method won't,
* instead the item to add will be stored. Also items of this won't be replaced by those of
* <code>conf</code>.
*
* @param conf the conf to add.
*/
@Override
public final Configuration add(final Configuration conf) {
this.directory.add(conf);
return this;
}
private abstract class Addable<T> {
@GuardedBy("this")
private boolean destroyed;
@GuardedBy("this")
private final List<Configuration> toAdd;
@GuardedBy("this")
private Future<T> f;
protected Addable() {
super();
synchronized (this) {
this.toAdd = new ArrayList<Configuration>();
this.f = null;
this.destroyed = false;
}
}
public final void add(final Configuration conf) {
final boolean computeStarted;
synchronized (this) {
computeStarted = isComputeStarted();
if (!computeStarted)
this.toAdd.add(conf);
}
if (computeStarted) {
// T must be thread-safe
add(this.get(), conf);
}
}
// synchronize on this (and not some private lock) to allow callers to do something before
// the result changes
protected final boolean isComputeStarted() {
synchronized (this) {
return this.f != null;
}
}
public final T get() {
// result
final Future<T> future;
// to run
final FutureTask<T> futureTask;
synchronized (this) {
checkDestroyed(this.destroyed);
if (this.f == null) {
final List<Configuration> l = new ArrayList<Configuration>(this.toAdd);
this.toAdd.clear();
futureTask = new FutureTask<T>(new Callable<T>() {
@Override
public T call() throws Exception {
final T res = create();
// don't call alien code with lock
assert !Thread.holdsLock(Addable.this);
for (final Configuration s : l) {
// deadlock if get() is called (will hang on future.get())
add(res, s);
}
return res;
}
});
this.f = futureTask;
future = futureTask;
} else {
futureTask = null;
future = this.f;
}
}
if (futureTask != null)
futureTask.run();
try {
return future.get();
} catch (InterruptedException e) {
throw new RTInterruptedException(e);
} catch (ExecutionException e) {
throw new IllegalStateException(e);
}
}
protected abstract T create();
protected abstract void add(final T obj, final Configuration conf);
public final void destroy() {
final Future<T> future;
synchronized (this) {
this.destroyed = true;
future = this.f;
}
if (future != null)
destroy(future);
}
// nothing by default
protected void destroy(final Future<T> future) {
}
}
// *** getters
@Override
public final ShowAs getShowAs() {
return this.getDirectory().getShowAs();
}
@Override
public final SQLBase getBase() {
return this.getNode(SQLBase.class);
}
@Override
public final DBRoot getRoot() {
synchronized (this.treeLock) {
checkDestroyed();
if (this.root == null)
this.setRoot(this.createRoot());
return this.root;
}
}
@Override
public final UserManager getUserManager() {
synchronized (this.treeLock) {
getRoot();
return this.uMngr;
}
}
@Override
public final UserRightsManager getUserRightsManager() {
synchronized (this.treeLock) {
getRoot();
return this.urMngr;
}
}
@Override
public final DBSystemRoot getSystemRoot() {
synchronized (this.treeLock) {
checkDestroyed();
if (this.sysRoot == null)
this.sysRoot = this.createSystemRoot();
return this.sysRoot;
}
}
public final Thread createDBCheckThread(final JFrame mainFrame, final Runnable quitRunnable) {
final DBSystemRoot sysRoot = this.getSystemRoot();
final String quit = "Quitter le logiciel";
final JOptionPane optionPane = new JOptionPane("Impossible de contacter la base. Cette fenêtre se fermera dès le rétablissement de la connexion. Sinon vous pouvez quitter le logiciel.",
JOptionPane.DEFAULT_OPTION, JOptionPane.ERROR_MESSAGE, null, new Object[] { quit }, null);
final JDialog dialog = optionPane.createDialog(mainFrame, "Erreur de connexion");
dialog.setModalityType(ModalityType.APPLICATION_MODAL);
// can only be closed by us (if connection is restored) or by choosing quit
dialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
dialog.addComponentListener(new ComponentAdapter() {
@Override
public void componentHidden(ComponentEvent e) {
if (optionPane.getValue().equals(quit))
quitRunnable.run();
else
// only the thread can change the dialog, otherwise pb would be incoherent
dialog.setVisible(true);
}
});
dialog.pack();
final Thread dbConn = new Thread(new Runnable() {
@Override
public void run() {
boolean pb = false;
while (true) {
try {
Thread.sleep((pb ? 4 : 20) * 1000);
} catch (InterruptedException e1) {
// ignore
e1.printStackTrace();
}
try {
sysRoot.getDataSource().validateDBConnectivity();
if (pb) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
// don't use setVisible(false) otherwise
// componentHidden() will be called
dialog.dispose();
}
});
pb = false;
}
} catch (Exception e) {
if (!pb) {
pb = true;
e.printStackTrace();
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
dialog.setLocationRelativeTo(dialog.getOwner());
dialog.setVisible(true);
}
});
}
}
}
}
}, "databaseConnectivity");
dbConn.setDaemon(true);
return dbConn;
}
/**
* Get the node of the asked class, creating just the necessary instances (ie getNode(Server)
* won't do a getBase().getServer()).
*
* @param <T> the type wanted.
* @param clazz the class wanted, eg SQLBase.class, DBSystemRoot.class.
* @return the corresponding instance, eg getBase() for SQLBase, getServer() or getBase() for
* DBSystemRoot depending on the SQL system.
*/
public final <T extends DBStructureItem<?>> T getNode(final Class<T> clazz) {
final SQLSystem sys = this.getServer().getSQLSystem();
final HierarchyLevel l = sys.getLevel(clazz);
if (l == HierarchyLevel.SQLSERVER)
return this.getServer().getAnc(clazz);
else if (l == sys.getLevel(DBSystemRoot.class))
return this.getSystemRoot().getAnc(clazz);
else if (l == sys.getLevel(DBRoot.class))
return this.getRoot().getAnc(clazz);
else
throw new IllegalArgumentException("doesn't know an item of " + clazz);
}
public final SQLServer getServer() {
return this.getServer(true);
}
private final SQLServer getServer(final boolean initSysRoot) {
synchronized (this.treeLock) {
checkDestroyed();
if (this.server == null) {
this.setServer(this.createServer());
// necessary otherwise the returned server has no datasource
// (eg getChildren() will fail)
if (initSysRoot && this.server.getSQLSystem().getLevel(DBSystemRoot.class) == HierarchyLevel.SQLSERVER)
this.getSystemRoot();
}
return this.server;
}
}
@Override
public final SQLFilter getFilter() {
synchronized (this.restLock) {
if (this.filter == null)
this.setFilter(this.createFilter());
return this.filter;
}
}
@Override
public final SQLElementDirectory getDirectory() {
return this.directory.get();
}
public final ProductInfo getProductInfo() {
synchronized (this.restLock) {
return this.productInfo;
}
}
@Override
public final String getAppName() {
final ProductInfo productInfo = this.getProductInfo();
if (productInfo != null)
return productInfo.getName();
else
return this.getProperty("app.name");
}
@Override
public final File getWD() {
synchronized (this.restLock) {
if (this.wd == null)
this.setWD(this.createWD());
return this.wd;
}
}
@Override
public BaseDirs getBaseDirs() {
synchronized (this.restLock) {
if (this.baseDirs == null)
this.baseDirs = this.createBaseDirs();
return this.baseDirs;
}
}
@Override
public Locale getLocale() {
synchronized (this.restLock) {
return this.locale;
}
}
// *** setters
// MAYBE add synchronized (not necessary since they're private, and only called with the lock)
private final void setFilter(final SQLFilter filter) {
this.filter = filter;
}
private void setServer(final SQLServer server) {
this.server = server;
}
private final void setRoot(final DBRoot root) {
this.root = root;
checkDestroyed();
// be sure to try to set a manager to avoid giving all permissions to everyone
this.urMngr = createUserRightsManager(root);
this.uMngr = UserManager.getSingletonManager().setInstanceIfNone(root);
}
private final void setWD(final File dir) {
this.wd = dir;
}
public void setLocale(Locale locale) {
Objects.requireNonNull(locale);
// don't create the directory
final boolean localeChangedAndDirExists;
synchronized (this.restLock) {
if (locale.equals(this.locale)) {
localeChangedAndDirExists = false;
} else {
this.locale = locale;
localeChangedAndDirExists = this.directory.isComputeStarted();
}
}
if (localeChangedAndDirExists) {
final SQLElementDirectory dir = this.getDirectory();
dir.setTranslator(createTranslator(dir));
}
}
public FieldMapper getFieldMapper() {
return fieldMapper;
}
public void setFieldMapper(FieldMapper fieldMapper) {
this.fieldMapper = fieldMapper;
}
}