开发者

Reading Java Properties file without escaping values

My application needs to use a .properties file for configuration. In the properties files, use开发者_如何学运维rs are allow to specify paths.

Problem

Properties files need values to be escaped, eg

dir = c:\\mydir

Needed

I need some way to accept a properties file where the values are not escaped, so that the users can specify:

dir = c:\mydir


Why not simply extend the properties class to incorporate stripping of double forward slashes. A good feature of this will be that through the rest of your program you can still use the original Properties class.

public class PropertiesEx extends Properties {
    public void load(FileInputStream fis) throws IOException {
        Scanner in = new Scanner(fis);
        ByteArrayOutputStream out = new ByteArrayOutputStream();

        while(in.hasNext()) {
            out.write(in.nextLine().replace("\\","\\\\").getBytes());
            out.write("\n".getBytes());
        }

        InputStream is = new ByteArrayInputStream(out.toByteArray());
        super.load(is);
    }
}

Using the new class is a simple as:

PropertiesEx p = new PropertiesEx();
p.load(new FileInputStream("C:\\temp\\demo.properties"));
p.list(System.out);

The stripping code could also be improved upon but the general principle is there.


Two options:

  • use the XML properties format instead
  • Writer your own parser for a modified .properties format without escapes


You can "preprocess" the file before loading the properties, for example:

public InputStream preprocessPropertiesFile(String myFile) throws IOException{
    Scanner in = new Scanner(new FileReader(myFile));
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    while(in.hasNext())
        out.write(in.nextLine().replace("\\","\\\\").getBytes());
    return new ByteArrayInputStream(out.toByteArray());
}

And your code could look this way

Properties properties = new Properties();
properties.load(preprocessPropertiesFile("path/myfile.properties"));

Doing this, your .properties file would look like you need, but you will have the properties values ready to use.

*I know there should be better ways to manipulate files, but I hope this helps.


The right way would be to provide your users with a property file editor (or a plugin for their favorite text editor) which allows them entering the text as pure text, and would save the file in the property file format.

If you don't want this, you are effectively defining a new format for the same (or a subset of the) content model as the property files have.

Go the whole way and actually specify your format, and then think about a way to either

  • transform the format to the canonical one, and then use this for loading the files, or
  • parse this format and populate a Properties object from it.

Both of these approaches will only work directly if you actually can control your property object's creation, otherwise you will have to store the transformed format with your application.


So, let's see how we can define this. The content model of normal property files is simple:

  • A map of string keys to string values, both allowing arbitrary Java strings.

The escaping which you want to avoid serves just to allow arbitrary Java strings, and not just a subset of these.

An often sufficient subset would be:

  • A map of string keys (not containing any whitespace, : or =) to string values (not containing any leading or trailing white space or line breaks).

In your example dir = c:\mydir, the key would be dir and the value c:\mydir.

If we want our keys and values to contain any Unicode character (other than the forbidden ones mentioned), we should use UTF-8 (or UTF-16) as the storage encoding - since we have no way to escape characters outside of the storage encoding. Otherwise, US-ASCII or ISO-8859-1 (as normal property files) or any other encoding supported by Java would be enough, but make sure to include this in your specification of the content model (and make sure to read it this way).

Since we restricted our content model so that all "dangerous" characters are out of the way, we can now define the file format simply as this:

<simplepropertyfile> ::= (<line> <line break> )*
<line>               ::= <comment> | <empty> | <key-value>
<comment>            ::= <space>* "#" < any text excluding line breaks >
<key-value>          ::= <space>* <key> <space>* "=" <space>* <value> <space>*
<empty>              ::= <space>*
<key>                ::= < any text excluding ':', '=' and whitespace >
<value>              ::= < any text starting and ending not with whitespace,
                           not including line breaks >
<space>              ::= < any whitespace, but not a line break >
<line break>         ::= < one of "\n", "\r", and "\r\n" >

Every \ occurring in either key or value now is a real backslash, not anything which escapes something else. Thus, for transforming it into the original format, we simply need to double it, like Grekz proposed, for example in a filtering reader:

public DoubleBackslashFilter extends FilterReader {
    private boolean bufferedBackslash = false;

    public DoubleBackslashFilter(Reader org) {
        super(org);
    }

    public int read() {
        if(bufferedBackslash) {
            bufferedBackslash = false;
            return '\\';
        }
        int c = super.read();
        if(c == '\\')
           bufferedBackslash = true;
        return c;
    }

    public int read(char[] buf, int off, int len) {
        int read = 0;
        if(bufferedBackslash) {
           buf[off] = '\\';
           read++;
           off++;
           len --;
           bufferedBackslash = false;
        }
        if(len > 1) {
           int step = super.read(buf, off, len/2);
           for(int i = 0; i < step; i++) {
               if(buf[off+i] == '\\') {
                  // shift everything from here one one char to the right.
                  System.arraycopy(buf, i, buf, i+1, step - i);
                  // adjust parameters
                  step++; i++;
               }
           }
           read += step;
        }
        return read;
    }
}

Then we would pass this Reader to our Properties object (or save the contents to a new file).

Instead, we could simply parse this format ourselves.

public Properties parse(Reader in) {
    BufferedReader r = new BufferedReader(in);
    Properties prop = new Properties();
    Pattern keyValPattern = Pattern.compile("\s*=\s*");
    String line;
    while((line = r.readLine()) != null) {
        line = line.trim(); // remove leading and trailing space
        if(line.equals("") || line.startsWith("#")) {
            continue; // ignore empty and comment lines
        }
        String[] kv = line.split(keyValPattern, 2);
        // the pattern also grabs space around the separator.
        if(kv.length < 2) {
            // no key-value separator. TODO: Throw exception or simply ignore this line?
            continue;
        }
        prop.setProperty(kv[0], kv[1]);
    }
    r.close();
    return prop;
}

Again, using Properties.store() after this, we can export it in the original format.


Based on @Ian Harrigan, here is a complete solution to get Netbeans properties file (and other escaping properties file) right from and to ascii text-files :

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Properties;

/**
 * This class allows to handle Netbeans properties file. 
 * It is based on the work of  : http://stackoverflow.com/questions/6233532/reading-java-properties-file-without-escaping-values.
 * It overrides both load methods in order to load a netbeans property file, taking into account the \ that 
 * were escaped by java properties original load methods.
 * @author stephane
 */
public class NetbeansProperties extends Properties {
    @Override
    public synchronized void load(Reader reader) throws IOException {
        BufferedReader bfr = new BufferedReader( reader );
        ByteArrayOutputStream out = new ByteArrayOutputStream();

        String readLine = null;
        while( (readLine = bfr.readLine()) != null ) {
            out.write(readLine.replace("\\","\\\\").getBytes());
            out.write("\n".getBytes());
        }//while

        InputStream is = new ByteArrayInputStream(out.toByteArray());
        super.load(is);
    }//met

    @Override
    public void load(InputStream is) throws IOException {
        load( new InputStreamReader( is ) );
    }//met

    @Override
    public void store(Writer writer, String comments) throws IOException {
        PrintWriter out = new PrintWriter( writer );
        if( comments != null ) {
            out.print( '#' );
            out.println( comments );
        }//if
        List<String> listOrderedKey = new ArrayList<String>();
        listOrderedKey.addAll( this.stringPropertyNames() );
        Collections.sort(listOrderedKey );
        for( String key : listOrderedKey ) {
            String newValue = this.getProperty(key);
            out.println( key+"="+newValue  );
       }//for
    }//met

    @Override
    public void store(OutputStream out, String comments) throws IOException {
        store( new OutputStreamWriter(out), comments );
    }//met
}//class


You could try using guava's Splitter: split on '=' and build a map from resulting Iterable.

The disadvantage of this solution is that it does not support comments.


@pdeva: one more solution

//Reads entire file in a String 
//available in java1.5
Scanner scan = new Scanner(new File("C:/workspace/Test/src/myfile.properties"));   
scan.useDelimiter("\\Z");   
String content = scan.next();

//Use apache StringEscapeUtils.escapeJava() method to escape java characters
ByteArrayInputStream bi=new ByteArrayInputStream(StringEscapeUtils.escapeJava(content).getBytes());

//load properties file
Properties properties = new Properties(); 
properties.load(bi);


It's not an exact answer to your question, but a different solution that may be appropriate to your needs. In Java, you can use / as a path separator and it'll work on both Windows, Linux, and OSX. This is specially useful for relative paths.

In your example, you could use:

dir = c:/mydir
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜