/*\
* Copyright 2006 Klaus Rogall, Hamburg, Germany (klaus.rogall@web.de). All rights reserved.
* _____________________________________________________________________________________________________________________
*
* This class is "Open Source" as defined by the Open Source Initiative (OSI). You can redistribute it and/or modify it
* under the terms of the BSD License. The license text is appended to the end of this file.
\*/
package de.klaro.base.util;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.util.Properties;
/**
* Diese Klasse erweitert hierarchisch organisierte Properties um Datei-basierte Persistenzeigenschaften.
*
* Die Klasse ist insbesondere fr Properties gedacht, die Konfigurationsdaten oder Vergleichbares enthalten, die sich
* whrend der Laufzeit einer Applikation ndern und gendert wieder abgespeichert werden sollen, damit die nderunge
* auch beim nchsten Lauf der Applikation zur verfgung stehen.
*
* Beim Speichern von Properties in einer Datei bleibt die ursprnglich in der Datei vorgefundenen Kommentare und die
* Reihenfolge der Property-Eintrge erhalten. Sogar zuvor gelschte Properties werden wieder an derselben Stelle
* einsortiert, da die gelschten Properties als spezielle Kommentareintrge (Zeilen beginnend mit "!") erhalten bleiben
* knnen. Aus diesem Grund ist es verboten, das fr Properties normalerweise erlaubte Kommentarzeichen '!' zu benutzen,
* da es fr die gelschten Property-Eintrge verwendet wird; stattdessen ist ausschliesslich das Kommentarzeichen
* '#' zu benutzen.
*
* Die Properties dieser Klasse mssen immer aus einer Datei gelesen werden (andere Ressourcen wie zum Beispiel Streams)
* sind nicht zulssig, weil diese nicht zum Schreiben geeignet sind.
*
* Whrend die Properties in die Datei zurck geschrieben werden, aus der sie ursprnglich gelesen wurden, werden
* folgende Aktionen durchgefhrt:
*
* - Kommentare werden unverndert bernommen.
* - nderungen eines Property-Wertes werden an die Stelle geschrieben, an der der Property-Eintrag zuvor stand.
* - Gelschte Property-Eintrge werden lediglich durch das Kommentarzeichen '!' deaktiviert, jedoch nicht aus der
* Datei entfernt (dieses Verhalten kann wahlweise abgeschaltet werden, so dass die Properties komplett gelscht
* werden).
* - Neue Properties werden am Ende der Datei hinzugefgt.
* - Neue Eintrge, die zuvor vorhanden waren und nur durch das Kommentarzeichen '!' deaktiviert sind, werden an die
* Stelle in der Datei eingefft, an der sie zuvor standen.
*
* Whrend die Properties in die Datei zurckgeschrieben werden, wird der ehemalige Inhalt temporr in einer zweiten
* Datei gehalten. Diese Datei hat den Namen der Orginaldatei, jedoch um die Endung ".tmp" erweitert. Diese temporre
* Datei wird nach erfolgreicher Sicherung gelscht.
*/
public class StartupProperties extends HierarchicalProperties
{
/* ______________________________________________________________________________________________________________ *\
\* Konstanten */
/* ______________________________________________________________________________________________________________ *\
\* Klassenvariablen */
/* ______________________________________________________________________________________________________________ *\
\* Instanzvariablen */
private File file$;
/* ______________________________________________________________________________________________________________ *\
\* Konstruktoren */
/**
* Erzeugt Properties ohne Bezug zu einer Datei.
*
* Das Sichern der Properties-Daten in eine Datei ist nicht mglich.
*/
public StartupProperties()
{
super();
file$ = null;
}
/**
* Erzeugt Properties, die aus der angegebenen Datei gelesen werden.
*
* Sofern der Dateiname relativ ist, wird er relativ zum Home-Verzeichnis des aktuellen Benutzers (entsprechend der
* System-Property "user.home") interpretiert.
*
* @param fileName Der Dateiname
* @throws FileNotFoundException Die Datei existiert nicht
* @throws IOException Das ffnen oder Lesen der Datei ist fehlgeschlagen
*/
public StartupProperties(String fileName) throws FileNotFoundException,IOException
{
this(new File(System.getProperty("user.home"),fileName));
}
/**
* Erzeugt Properties, die aus der angegebenen Datei gelesen werden.
*
* @param file Die Datei
* @throws FileNotFoundException Die Datei existiert nicht
* @throws IOException Das ffnen oder Lesen der Datei ist fehlgeschlagen
*/
public StartupProperties(File file) throws FileNotFoundException,IOException
{
super(file);
file$ = file;
}
/* ______________________________________________________________________________________________________________ *\
\* Instanzmethoden */
/**
* Liefert die Datei, in der die Properties gespeichert sind.
*
* @return Die Datei
*/
public File getFile()
{
return file$;
}
/**
* Sichert die Properties in der Datei, aus der sie gelesen wurden, wobei gelschte Properties durch
* Kommentarzeilen erhalten bleiben.
*
* @throws IOException Das Sichern der Datei ist fehlgeschlagen
*/
public void save() throws IOException
{
save(true);
}
/**
* Sichert die Properties in der Datei, aus der sie gelesen wurden.
*
* @param saveDeleted Gelschte Properties sollen (durch Kommentarzeilen) erhalten bleiben
* @throws FileNotFoundException Die Properties-Datei existiert nicht
* @throws IOException Die Properties-Datei konnte gelesen oder nicht erneut geschrieben werden
*/
public synchronized void save(boolean saveDeleted) throws IOException
{
// Wenn kein Bezug zu einer Datei vorhanden ist, kann auch nicht gespeichert werden
if (file$ == null)
{
throw new IllegalStateException("cannot save data: instance was created without file");
}
// Es muss eine Arbeitskopie der zu speichernden Daten erstellt werden, die verndert werden kann
Properties properties = (Properties)clone();
// Das Ergebnis wird zunchst in eine Temporrdatei geschrieben
File file = Files.createTempFile(getClass(), file$);
// Eingabe- und Ausgabekanle bereitgestellen
BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(file$), "ISO_8859_1"));
PrintWriter out = new PrintWriter(new FileWriter(file));
try
{
// Die Temporrdatei wird in die Orginaldatei kopiert, wobei die Zeilen entsprechend interpretiert werden
String line;
while ((line = in.readLine()) != null)
{
String key;
String value;
String trimmedLine = line.trim();
// Leere Zeilen werden einfach kopiert
if (trimmedLine.length() == 0)
{
out.println(line);
continue;
}
// Kommentarzeilen (mit '#' als Kommentarzeichen) werden einfach kopiert
if (trimmedLine.charAt(0) == '#')
{
out.println(line);
continue;
}
int index = Strings.indexOf(trimmedLine, "=:");
// Da kein Zuweisungszeichen ('=' oder ':') vorhanden ist, handelt es sich um eine normalen Kommentar, der einfach kopiert wird
if (index < 0)
{
out.println(line);
continue;
}
// Kommentarzeilen (mit '!' als Kommentarzeichen) weisen auf zuvor gelschte Eintrge hin
if (trimmedLine.charAt(0) == '!')
{
// Aus der Zeile werden der Property-Schlssel und der Wert extrahiert
key = trimmedLine.substring(1,index).trim();
value = (String)properties.remove(key);
// Falls die Property nicht existiert, ist sie immer noch gelscht, die Zeile wird dann einfach kopiert
if (value == null)
{
out.println(line);
continue;
}
}
else
{
key = trimmedLine.substring(0,index).trim();
value = (String)properties.remove(key);
// Die Property wurde gelscht, sie bleibt als Kommentar (mit '!' als Kommentarzeichen) erhalten, sofern gewnscht
if (value == null)
{
if (saveDeleted)
out.println("! " + line);
continue;
}
}
// Die Property wird mit ihrem neuen Wert in die Datei geschrieben
switch (trimmedLine.charAt(index))
{
case '=':
{
out.println(key + " = " + mask(value));
break;
}
case ':':
{
out.println(key + ": " + mask(value));
break;
}
}
}
// Alle rbrig gebliebenen Properties sind neu und werden am Dateiende jeweils als Zeile angehngt
if (properties.size() > 0)
{
if (saveDeleted)
out.println();
for (Object key: properties.keySet())
out.println(key + " = " + mask(properties.getProperty((String) key)));
}
}
finally
{
Streams.closeQuietly(in);
Streams.closeQuietly(out);
}
// Die Orginaldatei wird gelscht und die Temporrdatei wird umbenannt
if (!file$.delete())
throw new IOException("unable to delete " + file$.getAbsolutePath());
if (!file.renameTo(file$))
throw new IOException("unable to rename " + file.getAbsolutePath() + " to " + file$.getAbsolutePath());
}
/* ______________________________________________________________________________________________________________ *\
\* Klassenmethoden */
/**
* Diese Methode erzeugt Startup-Properties inklusive der dazu gehrigen Datei, falls diese noch nicht existiert.
*
* Wenn der angegebene Dateiname relativ ist, wird er relativ zum Home-Verzeichnis des aktuellen Benutzers
* (entsprechend der System-Property "user.home") angelegt.
*
* @param fileName Der Dateiname
* @return Die Startup-Properties, oder 'null' falls die Datei nicht erzeugt werden konnte
* @throws FileNotFoundException Die Datei existiert nicht
* @throws IOException Das ffnen oder Lesen der Datei ist fehlgeschlagen
*/
public static StartupProperties create(String fileName) throws FileNotFoundException,IOException
{
File file = new File(fileName);
if (!file.isAbsolute())
file = new File(System.getProperty("user.home"),fileName);
return create(file);
}
/**
* Diese Methode erzeugt Startup-Properties inklusive der dazu gehrigen Datei, falls diese noch nicht existiert.
*
* @param file Die Datei
* @return Die Startup-Properties, oder 'null' falls die Datei nicht erzeugt werden konnte
* @throws FileNotFoundException Die Datei existiert nicht
* @throws IOException Das ffnen oder Lesen der Datei ist fehlgeschlagen
*/
public static StartupProperties create(File file) throws FileNotFoundException,IOException
{
if (!file.exists())
{
// Zuerst werden die Verzeichnisse der Datei erzeugt
File parent = file.getParentFile();
if (parent != null)
parent.mkdirs();
// Jetzt erst wird die Datei erzeugt (mit leerem Inhalt)
file.createNewFile();
}
return new StartupProperties(file);
}
/**
* Maskiert die Sondernzeichen eines Strings
*
* @param string Der String
* @return Der String mit maskierten Sonderzeichen
*/
private static String mask(CharSequence string)
{
StringBuilder maskedValue = new StringBuilder();
for (int i = 0; i < string.length(); i++)
{
char c = string.charAt(i);
if (c >= 32)
{
maskedValue.append(c);
continue;
}
maskedValue.append('\\');
switch (c)
{
case Const.CHAR_BACKSPACE:
{
maskedValue.append('b');
break;
}
case Const.CHAR_TABULATOR:
{
maskedValue.append('t');
break;
}
case Const.CHAR_LINE_FEED:
{
maskedValue.append('n');
break;
}
case Const.CHAR_FORM_FEED:
{
maskedValue.append('f');
break;
}
case Const.CHAR_CARRIAGE_RETURN:
{
maskedValue.append('r');
break;
}
default:
{
maskedValue.append(Strings.format(c, 8, 3));
break;
}
}
}
return maskedValue.toString();
}
/* ______________________________________________________________________________________________________________ *\
\* Klassen */
}
/*\
* _____________________________________________________________________________________________________________________
*
* This software is distributed under the terms of the BSD License:
*
* Copyright 2006 Klaus Rogall, Hamburg, Germany (klaus.rogall@web.de). All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
* following conditions are met:
*
* - Redistributions of source code must retain the above copyright notice, this list of conditions and the following
* disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided with the distribution.
* - Neither the name of the Klaus Rogall nor the names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
* _____________________________________________________________________________________________________________________
\*/