/*
This file belongs to the Servoy development and deployment environment, Copyright (C) 1997-2010 Servoy BV
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 or write to the Free
Software Foundation,Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
*/
package com.servoy.j2db.util;
import java.awt.Component;
import java.awt.Frame;
import java.awt.Point;
import java.awt.Rectangle;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import javax.crypto.Cipher;
import javax.swing.JFrame;
import net.jcip.annotations.ThreadSafe;
import com.servoy.j2db.J2DBGlobals;
import com.servoy.j2db.LAFManager;
@ThreadSafe
@SuppressWarnings("nls")
public final class Settings extends SortedProperties
{
public static final long serialVersionUID = 8213681985670137977L;
public static final String FILE_NAME;
private static final int MAXIMIZED_INVIZIBLE_BORDER_PIXELS = 4;
private static final String CLIENT_LOCAL_FILE_NAME = "servoy_client.properties";
public static final int INITIAL_CLIENT_WIDTH = 800;
public static final int INITIAL_CLIENT_HEIGHT = 600;
public static final String TRUSTED_REMOTE_PLUGINS = "servoy.application_server.trustedRemotePlugins";
public static final String START_AS_TEAMPROVIDER_SETTING = "servoy.application_server.startRepositoryAsTeamProvider";
public static final boolean START_AS_TEAMPROVIDER_DEFAULT = false;
public static final String SMARTCLIENT_ENABLE_JAVAFX = "servoy.client.javafx"; //$NON-NLS-1$
@Deprecated
// do not persist global maintenance mode; when running clustered this could result in entering cluster-wide maintenance mode
// unwillingly when some app. servers were already started and working for a while and you start another app. server
public static final String START_GLOBAL_MAINTENANCE_MODE_SETTING = "servoy.application_server.global_maintenance_mode";
public static final String SERVER_MAINTENANCE_MODE_SETTING = "servoy.application_server.maintenance_mode";
public static final String ALLOW_CLIENT_REPOSITORY_ACCESS_SETTING = "servoy.application_server.allowClientRepositoryAccess"; //$NON-NLS-1$
public static final boolean ALLOW_CLIENT_REPOSITORY_ACCESS_DEFAULT = false;
public static final String LOG_CLIENT_STATS = "servoy.log.clientstats";
public static final String WAIT_FOR_NATIVE_STARTUP = "waitForNativeStartup";
public static final String RMI_CONNECTION_TIMEOUT = "rmi.connection.timeout";
private boolean loadedFromServer = false;
private File file;
static
{
String pFile = System.getProperty("property-file");
if (pFile == null) pFile = "servoy.properties";
FILE_NAME = pFile;
}
private final static Settings me = new Settings();
private Settings()
{
}
public void addPropertyChangeListener(PropertyChangeListener listener, String sProperty)
{
J2DBGlobals.addPropertyChangeListener(this, sProperty, listener);
}
public void removePropertyChangeListener(PropertyChangeListener listener, String sProperty)
{
J2DBGlobals.removePropertyChangeListener(this, sProperty, listener);
}
/**
* Load the config file from file or url(in case of webstart)
*/
public synchronized void loadFromServer(URL base) throws Exception
{
loadedFromServer = true;
URL configfile = null;
String profileName = System.getProperty("servoy.profilename"); //$NON-NLS-1$
if (profileName != null)
{
configfile = new URL(base, FILE_NAME + "?profilename=" + profileName); //$NON-NLS-1$
}
else
{
configfile = new URL(base, FILE_NAME);
}
//load from local
file = new File(System.getProperty("user.home"), J2DBGlobals.CLIENT_LOCAL_DIR + CLIENT_LOCAL_FILE_NAME); //$NON-NLS-1$
if (file.exists())
{
FileInputStream fis = new FileInputStream(file);
load(fis);
fis.close();
}
String currentLnf = getProperty("selectedlnf"); //$NON-NLS-1$
String currentNumberFormat = getProperty("locale.numberformat"); //$NON-NLS-1$
String currentIntegerformat = getProperty("locale.integerformat"); //$NON-NLS-1$
String currentDateFormat = getProperty("locale.dateformat"); //$NON-NLS-1$
String currentUseSystemPrintDialog = getProperty("useSystemPrintDialog"); //$NON-NLS-1$
// Setting all system property entries of the settings.
Iterator iterator = keySet().iterator();
while (iterator.hasNext())
{
String property = (String)iterator.next();
if (property.startsWith("system.property.")) //$NON-NLS-1$
{
iterator.remove();
}
}
removePureServerValues();
//load from server (so it can be overridden from server)
try
{
InputStream is = configfile.openStream();
load(is);
is.close();
}
catch (Exception ex)
{
Debug.error(ex);
}
String twoWay = getProperty(base.getHost() + base.getPort() + "SocketFactory.useTwoWaySocket"); //$NON-NLS-1$
boolean serverTwoWay = Utils.getAsBoolean(getProperty("SocketFactory.useTwoWaySocket")); //$NON-NLS-1$
if (twoWay == null || !serverTwoWay)
{
setProperty(base.getHost() + base.getPort() + "SocketFactory.useTwoWaySocket", String.valueOf(serverTwoWay)); //$NON-NLS-1$
}
if (currentIntegerformat != null)
{
setProperty("locale.integerformat", currentIntegerformat); //$NON-NLS-1$
}
if (currentNumberFormat != null)
{
setProperty("locale.numberformat", currentNumberFormat); //$NON-NLS-1$
}
if (currentDateFormat != null)
{
setProperty("locale.dateformat", currentDateFormat); //$NON-NLS-1$
}
if (currentUseSystemPrintDialog != null)
{
setProperty("useSystemPrintDialog", currentUseSystemPrintDialog); //$NON-NLS-1$
}
boolean pushLnfToMac = Utils.getAsBoolean(getProperty("pushLnfToMac", "false")); //$NON-NLS-1$ //$NON-NLS-2$
if (!LAFManager.isUsingAppleLAF() || pushLnfToMac)
{
String lnf = getProperty("selectedlnf"); //$NON-NLS-1$
String theme = getProperty("lnf.theme"); //$NON-NLS-1$
String font = getProperty("font"); //$NON-NLS-1$
// keep the lnf the user has selected
if (lnf != null && getProperty(base.getHost() + base.getPort() + "_selectedlnf") == null) //$NON-NLS-1$
{
setProperty(base.getHost() + base.getPort() + "_selectedlnf", lnf); //$NON-NLS-1$
}
if (theme != null && getProperty(base.getHost() + base.getPort() + "_lnf.theme") == null) //$NON-NLS-1$
{
setProperty(base.getHost() + base.getPort() + "_lnf.theme", theme); //$NON-NLS-1$
}
if (font != null && getProperty(base.getHost() + base.getPort() + "_font") == null) //$NON-NLS-1$
{
setProperty(base.getHost() + base.getPort() + "_font", font); //$NON-NLS-1$
}
}
else if (currentLnf != null)
{
setProperty("selectedlnf", currentLnf); //$NON-NLS-1$
}
else
{
remove("selectedlnf"); //$NON-NLS-1$
}
applySystemProperties();
}
public synchronized void loadFromURL(URL url) throws IOException
{
InputStream is = url.openStream();
if (is != null)
{
load(is);
is.close();
applySystemProperties();
}
}
public synchronized void loadFromFile(File file) throws IOException
{
this.file = file;
Debug.log("Loading " + FILE_NAME + " from " + file.getAbsolutePath()); //$NON-NLS-1$ //$NON-NLS-2$
if (file.exists())
{
FileInputStream fis = new FileInputStream(file);
load(fis);
fis.close();
if (size() == 0)
{
Debug.log("Nothing loaded, size is 0, trying again"); //$NON-NLS-1$
synchronized (this)
{
try
{
this.wait(1500);
}
catch (InterruptedException e)
{
// well we just can't wait
}
}
fis = new FileInputStream(file);
load(fis);
fis.close();
if (size() == 0)
{
Debug.log("Loading - Failed (size == 0)"); //$NON-NLS-1$
}
}
applySystemProperties();
Debug.log("Loading - Done"); //$NON-NLS-1$
}
else
{
Debug.log("Loading - Failed (not found)"); //$NON-NLS-1$
}
}
private void applySystemProperties()
{
// Setting all system property entries of the settings.
Iterator<Object> iterator = keySet().iterator();
while (iterator.hasNext())
{
String property = iterator.next().toString();
if (property.startsWith("system.property."))
{
System.setProperty(property.substring(16), getProperty(property));
}
else if (property.startsWith("sablo."))
{
System.setProperty(property, getProperty(property));
}
}
}
/**
*
*/
private void removePureServerValues()
{
// remove it always reload this from the server.
remove("SocketFactory.rmiClientFactory"); //$NON-NLS-1$
remove("SocketFactory.http.tunnel.encryption"); //$NON-NLS-1$
remove("SocketFactory.tunnelConnectionMode"); //$NON-NLS-1$
remove("servoy.branding"); //$NON-NLS-1$
remove("servoy.branding.windowtitle"); //$NON-NLS-1$
remove("servoy.branding.loadingimage"); //$NON-NLS-1$
remove("servoy.branding.windowicon"); //$NON-NLS-1$
remove("servoy.allowSolutionBrowsing"); //$NON-NLS-1$
remove("servoy.disable.record.insert.reorder"); //$NON-NLS-1$
remove("servoy.client.tracing"); //$NON-NLS-1$
}
/**
* Remove old or moved settings.
*/
private void removeObsoleteSettings()
{
// remove settings that are now saved in the eclipse workspace
remove("showConfirmationDialogWhenErrors"); //$NON-NLS-1$
remove("showConfirmationDialogWhenWarnings"); //$NON-NLS-1$
remove("servoy.maxSampleDataTableRowCount"); //$NON-NLS-1$
remove("designer.preferdMetrics"); //$NON-NLS-1$
remove("designer.stepSize"); //$NON-NLS-1$
remove("copyPasteOffset"); //$NON-NLS-1$
remove("guidesize"); //$NON-NLS-1$
remove("gridcolor"); //$NON-NLS-1$
remove("gridsize"); //$NON-NLS-1$
remove("pointsize"); //$NON-NLS-1$
remove("showGrid"); //$NON-NLS-1$
remove("snapToGrid"); //$NON-NLS-1$
remove("saveEditorState"); //$NON-NLS-1$
}
private static final String enc_prefix = "encrypted:"; //$NON-NLS-1$
@Override
public synchronized void load(InputStream inStream) throws IOException
{
//1 load them in
super.load(inStream);
//2 convert properties to unencrypted form
try
{
Cipher desCipher = Cipher.getInstance("DESede"); //$NON-NLS-1$
desCipher.init(Cipher.DECRYPT_MODE, SecuritySupport.getCryptKey(this));
Iterator it = entrySet().iterator();
while (it.hasNext())
{
Map.Entry entry = (Map.Entry)it.next();
if (entry.getKey().toString().toLowerCase().indexOf("password") != -1) //$NON-NLS-1$
{
String val = entry.getValue().toString();
if (val.startsWith(enc_prefix))
{
String val_val = val.substring(enc_prefix.length());
byte[] array_val = Utils.decodeBASE64(val_val);
entry.setValue(new String(desCipher.doFinal(array_val)));
}
}
}
}
catch (Exception e)
{
Debug.error(e);
throw new IOException(e.getMessage());
}
}
@Override
public synchronized void store(OutputStream out, String header) throws IOException
{
//1 convert properties to encrypted form
Properties to_save = new SortedProperties(); //make background list, we want to keep this unencripted
try
{
SecuritySupport.clearCryptKey();
Cipher desCipher = Cipher.getInstance("DESede"); //$NON-NLS-1$
desCipher.init(Cipher.ENCRYPT_MODE, SecuritySupport.getCryptKey(this));
Iterator it = entrySet().iterator();
while (it.hasNext())
{
Map.Entry entry = (Map.Entry)it.next();
String key = entry.getKey().toString();
String val = entry.getValue().toString();
if (!val.startsWith(enc_prefix) && key.toLowerCase().indexOf("password") != -1) //$NON-NLS-1$
{
byte[] array_val = entry.getValue().toString().getBytes();
String new_val = Utils.encodeBASE64(desCipher.doFinal(array_val));
val = enc_prefix + new_val;
}
to_save.put(key, val);
}
}
catch (Exception e)
{
throw new IOException(e.getMessage());
}
//2 store them
to_save.store(out, header);
}
/**
* Store the settings
*/
public synchronized void save() throws Exception
{
if (size() == 0) return;//nothing to save
if (file == null) return;
if (loadedFromServer)
{
removePureServerValues();
File hiddendir = new File(System.getProperty("user.home"), J2DBGlobals.CLIENT_LOCAL_DIR); //$NON-NLS-1$
if (!hiddendir.exists()) hiddendir.mkdirs();
// file = new File(System.getProperty("user.home"), J2DBGlobals.CLIENT_LOCAL_DIR + CLIENT_LOCAL_FILE_NAME); //$NON-NLS-1$
}
//no need to store those
Object appServerDir = remove(J2DBGlobals.SERVOY_APPLICATION_SERVER_DIRECTORY_KEY);
removeObsoleteSettings();
FileOutputStream fis = new FileOutputStream(file);
store(fis, "servoy"); //$NON-NLS-1$
fis.close();
if (appServerDir != null) put(J2DBGlobals.SERVOY_APPLICATION_SERVER_DIRECTORY_KEY, appServerDir);
}
/**
* Returns the default settings
*/
public static Settings getInstance()
{
return me;
}
/**
* Load the bounds from a certain component
*/
public synchronized boolean loadBounds(Component component, String solutionName)
{
if (component == null || component.getName() == null) return false;
String bounds = getProperty("rect_" + (solutionName != null ? solutionName + "_" : "") + component.getName() + "_bounds"); //$NON-NLS-1$ //$NON-NLS-2$
if (bounds != null)
{
Rectangle r = PersistHelper.createRectangle(bounds);
if (component instanceof JFrame)
{
Object oState = get("window_state_" + component.getName()); //$NON-NLS-1$
if (oState != null)
{
int state = Utils.getAsInteger(oState);
boolean maximized = (state & Frame.MAXIMIZED_BOTH) == Frame.MAXIMIZED_BOTH;
if (maximized)
{
// set the bounds for when the window is no longer maximized (to avoid 0 size & stuff)
component.setLocation(r.x + MAXIMIZED_INVIZIBLE_BORDER_PIXELS, // the location is also needed in order to maximize
r.y + MAXIMIZED_INVIZIBLE_BORDER_PIXELS); // the window on the correct display (if there is more than 1 monitor)
component.setSize(INITIAL_CLIENT_WIDTH, INITIAL_CLIENT_HEIGHT);
}
((JFrame)component).setExtendedState(state);
if (maximized)
{
return true;
}
}
}
try
{
if (UIUtils.isOnScreen(r))
{
component.setBounds(r);
component.validate();
return true;
} // else falls of the screens
}
catch (Exception e)
{
Debug.error(e);//just in case isOnScreen() its called and fails in headless env.
}
}
return false;
}
/**
* Load the bounds from a certain component
*/
public synchronized boolean loadBounds(Component component)
{
return loadBounds(component, null);
}
/**
* Save the bounds from a certain component
*/
public synchronized void saveBounds(Component component, String solutionName)
{
if (component == null || component.getName() == null) return;
if (component instanceof JFrame)
{
try
{
Method method = component.getClass().getMethod("getExtendedState", (Class[])null); //$NON-NLS-1$
Object o = method.invoke(component, (Object[])null);
put("window_state_" + component.getName(), o == null ? "" : o.toString()); //$NON-NLS-1$ //$NON-NLS-2$
}
catch (Exception ex)
{
}
}
//in case of having a previously saved property 'point', delete it
remove("point_" + (solutionName != null ? solutionName + "_" : "") + component.getName() + "_location");
Point l = component.getLocation();
Debug.trace("location of " + component.getName() + " " + l); //$NON-NLS-1$ //$NON-NLS-2$
put("rect_" + (solutionName != null ? solutionName + "_" : "") + component.getName() + "_bounds", PersistHelper.createRectangleString(component.getBounds())); //$NON-NLS-1$ //$NON-NLS-2$
}
public synchronized void saveBounds(Component component)
{
saveBounds(component, null);
}
public synchronized boolean loadLocation(Component component, String solutionName)
{
if (component == null || component.getName() == null) return false;
String location = getProperty("point_" + (solutionName != null ? solutionName + "_" : "") + component.getName() + "_location"); //$NON-NLS-1$ //$NON-NLS-2$
if (location != null)
{
Point l = PersistHelper.createPoint(location);
try
{
Rectangle r = new Rectangle(l.x, l.y, 1, 1);
if (UIUtils.isOnScreen(r))
{
component.setLocation(l);
return true;
} // else falls of the screens
}
catch (Exception e)
{
Debug.error(e);//just in case isOnScreen() its called and fails in headless env.
}
}
return false;
}
/**
* Load the location from a certain component
*/
public synchronized boolean loadLocation(Component component)
{
return loadLocation(component, null);
}
public synchronized void saveLocation(Component component, String solutionName)
{
if (component == null || component.getName() == null) return;
//in case of having a previously saved property 'rect', delete it
remove("rect_" + (solutionName != null ? solutionName + "_" : "") + component.getName() + "_bounds");
Point l = component.getLocation();
Debug.trace("location of " + component.getName() + " " + l); //$NON-NLS-1$ //$NON-NLS-2$
put("point_" + (solutionName != null ? solutionName + "_" : "") + component.getName() + "_location", PersistHelper.createPointString(l)); //$NON-NLS-1$ //$NON-NLS-2$
}
public synchronized void saveLocation(Component component)
{
saveLocation(component, null);
}
public synchronized void deleteBounds(String componentName, String solutionName)
{
remove("rect_" + (solutionName != null ? solutionName + "_" : "") + componentName + "_bounds");
remove("point_" + (solutionName != null ? solutionName + "_" : "") + componentName + "_location");
remove("window_state_" + componentName);
}
public synchronized void deleteAllBounds()
{
ArrayList deleteList = new ArrayList();
Enumeration e = keys();
while (e.hasMoreElements())
{
String element = (String)e.nextElement();
if (element.startsWith("rect_") || element.startsWith("window_state_") || element.startsWith("point_")) //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
{
deleteList.add(element);
}
}
Iterator it = deleteList.iterator();
while (it.hasNext())
{
Object element = it.next();
remove(element);
}
}
@Override
public synchronized Object remove(Object key)
{
Object oldValue = super.remove(key);
if (oldValue != null)
{
J2DBGlobals.firePropertyChange(this, key.toString(), oldValue, null);
}
return oldValue;
}
/*
* @see Properties#setProperty(String, String)
*/
@Override
public synchronized Object setProperty(String key, String value)
{
Object o = super.setProperty(key, value);
if (o == null && value == null) return o;
if (o != null && value != null && o.equals(value)) return o;
J2DBGlobals.firePropertyChange(this, key, o, value);
return o;
}
@Override
//FIXME why is the default implementation not ok?
public int hashCode()
{
return 1;
}
/**
* Get all properties with prefixed key
* @param settings
* @param string
* @return
*/
public Map<String, String> getPrefixedProperties(String prefix)
{
Map<String, String> map = null;
for (Map.Entry<Object, Object> entry : entrySet())
{
if (entry.getKey() instanceof String && entry.getValue() instanceof String && ((String)entry.getKey()).startsWith(prefix))
{
if (map == null)
{
map = new HashMap<String, String>();
}
map.put(((String)entry.getKey()).substring(prefix.length()), (String)entry.getValue());
}
}
return map;
}
/**
* Remove all properties with prefixed key
* @param settings
* @param string
* @return
*/
public void removePrefixedProperties(String prefix)
{
for (Object key : keySet().toArray())
{
if (key instanceof String && ((String)key).startsWith(prefix))
{
remove(key);
}
}
}
public File getFile()
{
return file;
}
public static final String USER = "user."; //$NON-NLS-1$
public static final String DEVELOPER_USER = "developer.user."; //$NON-NLS-1$
public void loadUserProperties(Map<String, String> userProperties)
{
Iterator<Object> it = this.keySet().iterator();
while (it.hasNext())
{
String key = (String)it.next();
if (key.startsWith(USER))
{
userProperties.put(key.substring(USER.length()), this.getProperty(key));
}
}
}
public String getUserProperty(String prefix, String name)
{
return getProperty(prefix + (name.length() > 255 ? name.substring(0, 255) : name));
}
public void setUserProperty(String prefix, String name, String value)
{
if (value == null)
{
this.remove(prefix + (name.length() > 255 ? name.substring(0, 255) : name));
}
else
{
this.setProperty(prefix + (name.length() > 255 ? name.substring(0, 255) : name), (value.length() > 255 ? value.substring(0, 255) : value));
}
}
public Properties getAsProperties()
{
return this;
}
@Override
public String getProperty(String key)
{
// we do no more support repository team provider
if (Settings.START_AS_TEAMPROVIDER_SETTING.equals(key)) return Boolean.FALSE.toString();
return super.getProperty(key);
}
}