Java typically stores its configuration in files that end with a .properties extension. These files are essentially key-value pairs of data. Java SE has a built-in class, java.util.Properties, for reading and writing these files.
Sometimes, though, you may want to read these files from other languages. For example, database information is often stored in a .properties file. If another application wishes to connect to the same database, it is better to read that file rather than create a copy. That way, if the password changes (for example), you only have one place to update.
The basic syntax of the file is easy to understand. It is line-based. Each line is either a blank line, a comment line, or a key-value pair. Keys are separated from values by an =, :, or space. Easy, right?
Well, there are a few complications. You can read the full spec here, but this is a brief summary of some of the issues with creating a fully-compliant reader/writer:
- An odd number of backslashes indicates that the next line is a continuation of the previous line, unless the line is a comment line, which can’t be continued
- Comments are started with # or !, and extend to the end of the line; however, if a # or ! appears after the first non-whitespace character, they are not considered to be comments. When writing, though, these values are still escaped using a backslash.
- =, :, or space separate a key from a value. However, these values can be escaped so that they can be used as part of a key or value string.
So I’ve got a working PHP port of the java.util.Properties class. Most of the common methods are implemented with the same name as their Java counterpart. The header documentation of the class goes into more details on what is and is not implemented, and where to find the corresponding functionality.
View the full source below, or you can download a .zip file from here containing the .php file and the tests showing its correctness.
NOTE: If you copy and replace from the source below, you must replace all instances of f_write with fwrite. This is due to security restrictions with posting PHP code here.
/**
* PHP Version of the class java.util.Properties. Loads and saves Java
* .properties files.
*
* The methods inherited from Hashtable are not implemented, as they are
* merely another interface to the same functionality. The save method is not
* implemented, as it is considered deprecated; use store instead.
*
* In general, methods which are overloaded to accept an optional parameter
* are combined into a single method with a default value of null for the
* optional parameters. Methods which take versions of streams, writers,
* etc. are consolidated into a single method taking a PHP stream.
*
* The list() methods are replaced with debugList(), as list is a reserved
* word in PHP.
*
* For more information on the .properties format, see
* http://java.sun.com/javase/6/docs/api/java/util/Properties.html
*/
class Properties
{
/**
* Constructor. Takes an optional Properties object defining the default
* values of the properties.
*/
public function __construct($defaults = null) {
if($defaults != null) {
$this->defaults = $defaults;
}
$this->properties = array();
}
/**
* Retrieves a property value. If a value is not found for this key, the
* defaults object is searched for a default. If not default value is
* found there, the default value passed into this function is used. If
* a value still cannot be determined, null is returned.
*/
public function getProperty($key, $defaultValue = null) {
$value = null;
if(array_key_exists($key, $this->properties)) {
$value = $this->properties[$key];
}
if($value == null && $this->defaults != null) {
$value = $this->defaults->getProperty($key);
}
if($value == null) {
$value = $defaultValue;
}
return $value;
}
/**
* Sets a property value. Returns the old value, if available. No defaults
* are used when searching for the old value; a value is returned only if it
* was explicitly set.
*/
public function setProperty($key, $value) {
$oldValue = null;
if(array_key_exists($key, $this->properties)) {
$oldValue = $this->properties[$key];
}
$this->properties[$key] = $value;
return $oldValue;
}
/**
* Returns the names of all keys in this property set, including keys in the
* default property set if no value is defined in this property set.
*/
public function propertyNames() {
$keys = array();
if($this->defaults != null) {
$keys = $this->defaults->propertyNames();
}
$myKeys = array_keys($this->properties);
sort($myKeys);
for($i = 0; $i < count($myKeys); $i++) {
if(!array_key_exists($myKeys[$i], $keys)) {
array_push($keys, $myKeys[$i]);
}
}
return $keys;
}
/**
* Writes the properties to the given stream, in the format defined by
* java.util.Properties.
*/
public function store($handle, $comment = null) {
if($comment != null) {
f_write($handle, "#" . $this->escapeComment($comment) . PHP_EOL);
}
f_write($handle, "#" . date("D M d H:i:s T Y") . PHP_EOL);
$propertyNames = array_keys($this->properties);
for($i = 0; $i < count($propertyNames); $i++) {
f_write($handle, $this->escapeKey($propertyNames[$i]) . "=" . $this->escapeValue($this->properties[$propertyNames[$i]]) . PHP_EOL);
}
}
/**
* Loads a file from the given stream, in the format defined by
* java.util.Properties
*/
public function load($handle) {
$this->properties = array();
$logicalLine = "";
$appendNext = false;
while(!feof($handle)) {
$line = fgets($handle);
$line = rtrim($line, "\r\n"); // Chomp!
$line = ltrim($line);
// If the previous line was a continuation line, append this one
if($appendNext) {
$logicalLine .= $line;
$appendNext = false;
} else {
$logicalLine = $line;
}
// Is it a comment line?
// Check this before checking for continuations, as comments cannot be continued
if(preg_match("/^[ ]*[#!]/", $logicalLine)) {
continue;
}
// Is this line continued?
// A continuation line is one ending in an odd number of backslashes
$numTrailingBackslashes = 0;
while((strlen($logicalLine) - $numTrailingBackslashes - 1) > -1 &&
$logicalLine[strlen($logicalLine) - $numTrailingBackslashes - 1] == "\\") {
$numTrailingBackslashes++;
}
if($numTrailingBackslashes % 2 == 1) {
// If it is a continuation line, append a newline.. this will
// removed when the line is unescaped, but appending it in
// advance prevents backslashes other than the final one from
// escaping the next line, e.g. in the case
// a=b\\\
// n
// If you are not careful, this will be transformed into
// a=b\\n
// And bad things could happen.
$logicalLine .= "\n";
$appendNext = true;
continue;
}
// Now that we've combined the physical lines to form one large
// logical line, see if it's empty.
if(strcmp($logicalLine, "") == 0) {
continue;
}
// Split into key-value pairs
$matches = array();
$key = "";
$value = "";
// This regex splits the string into 3 sections:
// - Leading whitespace plus all non-whitespace characters up to
// the first unescaped :, =, space, or tab character
// - The :, =, space, or tab character itself, which separates
// the key from the value
// - The rest of the string
// If this match succeeds, the first and third pieces form the key
// and value. Otherwise, the entire string is considered to be a
// key with no value.
if(preg_match("/^([ ]*.*?[^\\\\])([:= ]+?)([^:= ].*)$/s", $logicalLine, $matches)) {
$key = $matches[1];
$value = $matches[3];
} else {
$key = $logicalLine;
}
$key = $this->unescapeGeneric($key);
$value = $this->unescapeGeneric($value);
$this->properties[$key] = $value;
}
}
/**
* List the contents to an output stream. This is useful for debugging.
*/
public function debugList($handle) {
f_write($handle, "-- listing properties --" . PHP_EOL);
$propertyNames = $this->propertyNames();
for($i = 0; $i < count($propertyNames); $i++) {
f_write($handle, $propertyNames[$i] . "=" . $this->getProperty($propertyNames[$i]) . PHP_EOL);
}
}
private function escapeKey($key) {
return str_replace(" ", "\\ ", $this->escapeGeneric($key));
}
private function escapeValue($value) {
$value = $this->escapeGeneric($value);
// Values have leading space replaced, but none other
if(strlen($value) > 0 && strcmp($value[0], ' ') == 0) {
$value = "\\" . $value;
}
return $value;
}
private function escapeGeneric($value) {
$value = str_replace("#", "\\#", $value);
$value = str_replace("!", "\\!", $value);
$value = str_replace("=", "\\=", $value);
$value = str_replace(":", "\\:", $value);
$value = str_replace("\t", "\\t", $value);
$value = str_replace("\n", "\\n", $value);
$value = str_replace("\r", "\\r", $value);
return $value;
}
private function escapeComment($comment) {
return preg_replace("/\n([^#])/", "\n#\\1", $comment);
}
private function unescapeGeneric($value) {
$value = preg_replace("/\\\\([^\\\\trn\\n])/s", "\\1", $value);
$value = str_replace("\\\n", "", $value);
$value = str_replace("\\t", "\t", $value);
$value = str_replace("\\r", "\r", $value);
$value = str_replace("\\n", "\n", $value);
return $value;
}
private $properties /* Array */;
private $defaults /* Properties */;
}