Package freenet.support

Source Code of freenet.support.SimpleFieldSet

package freenet.support;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;

import freenet.node.FSParseException;
import freenet.support.io.Closer;
import freenet.support.io.LineReader;
import freenet.support.io.Readers;

/**
* @author amphibian
*
* Simple FieldSet type thing, which uses the standard Java facilities. Should always be written as
* UTF-8. Simpler encoding than Properties.
*
* All of the methods treat the data as a tree, where levels are indicated by ".". Hence e.g.:
* DirectKey=Value
* Subset.Key=Value
* Subset.Subset.Key=Value
*
* DETAILS:
* <key>=<value>
* Key is split into a tree via "."'s.
* Value is a string.
* Conversion methods are provided for most key types, one notable issue is arrays of string,
* which are separated by ";".
*
* ALTERNATE FORMAT:
* <key>==<standard base64 encoded value>
* The value is encoded. We will use this later on to prevent problems when transferring noderefs
* (line breaks, whitespace get changes when people paste stuff etc), and to allow e.g. newlines
* in strings. For now we only *read* such formats.
*/
public class SimpleFieldSet {

    private final Map<String, String> values;
    private Map<String, SimpleFieldSet> subsets;
    private String endMarker;
    private final boolean shortLived;
    private final boolean alwaysUseBase64;
    protected String[] header;

    public static final char MULTI_LEVEL_CHAR = '.';
    public static final char MULTI_VALUE_CHAR = ';';
    public static final char KEYVALUE_SEPARATOR_CHAR = '=';
    private static final String[] EMPTY_STRING_ARRAY = new String[0];

    public SimpleFieldSet(boolean shortLived) {
        this(shortLived, false);
    }
   
    /**
     * Create a SimpleFieldSet.
     * @param shortLived If false, strings will be interned to ensure that they use as
     * little memory as possible. Only set to true if the SFS will be short-lived or
     * small.
     * @param alwaysUseBase64 If true, the SFS can contain newlines etc in values, and we will
     * always use base64 for the values if they contain such invalid characters.
     */
    public SimpleFieldSet(boolean shortLived, boolean alwaysUseBase64) {
        values = new HashMap<String, String>();
         subsets = null;
         this.shortLived = shortLived;
         this.alwaysUseBase64 = alwaysUseBase64;
    }

    public SimpleFieldSet(BufferedReader br, boolean allowMultiple, boolean shortLived) throws IOException {
        this(br, allowMultiple, shortLived, false, false);
    }
   
    /**
     * Construct a SimpleFieldSet from reading a BufferedReader.
     * @param br
     * @param allowMultiple If true, multiple lines with the same field name will be
     * combined; if false, the constructor will throw.
     * @param shortLived If false, strings will be interned to ensure that they use as
     * little memory as possible. Only set to true if the SFS will be short-lived or
     * small.
     * @throws IOException If the buffer could not be read, or if there was a formatting
     * problem.
     */
    public SimpleFieldSet(BufferedReader br, boolean allowMultiple, boolean shortLived, boolean allowBase64, boolean alwaysBase64) throws IOException {
        this(shortLived, alwaysBase64);
        read(Readers.fromBufferedReader(br), allowMultiple, allowBase64);
    }

    /** Copy constructor */
    public SimpleFieldSet(SimpleFieldSet sfs){
      values = new HashMap<String, String>(sfs.values);
      if(sfs.subsets != null)
        subsets = new HashMap<String, SimpleFieldSet>(sfs.subsets);
      this.shortLived = false; // it's been copied!
      this.header = sfs.header;
      this.endMarker = sfs.endMarker;
      this.alwaysUseBase64 = sfs.alwaysUseBase64;
    }

    public SimpleFieldSet(LineReader lis, int maxLineLength, int lineBufferSize, boolean utf8OrIso88591, boolean allowMultiple, boolean shortLived) throws IOException {
      this(lis, maxLineLength, lineBufferSize, utf8OrIso88591, allowMultiple, shortLived, false);
    }

    public SimpleFieldSet(LineReader lis, int maxLineLength, int lineBufferSize, boolean utf8OrIso88591, boolean allowMultiple, boolean shortLived, boolean allowBase64) throws IOException {
      this(shortLived);
      read(lis, maxLineLength, lineBufferSize, utf8OrIso88591, allowMultiple, allowBase64);
    }

    /**
     * Construct from a string.
     * String format:
     * blah=blah
     * blah=blah
     * End
     * @param shortLived If false, strings will be interned to ensure that they use as
     * little memory as possible. Only set to true if the SFS will be short-lived or
     * small.
     * @throws IOException if the string is too short or invalid.
     */
    public SimpleFieldSet(String content, boolean allowMultiple, boolean shortLived, boolean allowBase64) throws IOException {
      this(shortLived);
        StringReader sr = new StringReader(content);
        BufferedReader br = new BufferedReader(sr);
      read(Readers.fromBufferedReader(br), allowMultiple, allowBase64);
    }
   
    /**
     * Construct from a {@link String} array.
     * <p>
     * Similar to {@link #SimpleFieldSet(String, boolean, boolean)},
     * but each item of array represents a single line
     * </p>
     * @param content to be parsed
     * @param allowMultiple If {@code true}, multiple lines with the same field name will be
     * combined; if {@code false}, the constructor will throw.
     * @param shortLived If {@code false}, strings will be interned to ensure that they use as
     * little memory as possible. Only set to {@code true} if the SFS will be short-lived or
     * small.
     * @throws IOException
     */
    public SimpleFieldSet(String[] content, boolean allowMultiple, boolean shortLived, boolean allowBase64) throws IOException {
      this(shortLived);
      read(Readers.fromStringArray(content), allowMultiple, allowBase64);
    }
   
    /**
     * @see #read(LineReader, int, int, boolean, boolean)
     */
  private void read(LineReader lr, boolean allowMultiple, boolean allowBase64) throws IOException {
    read(lr, Integer.MAX_VALUE, 0x100, true, allowMultiple, allowBase64);
  }
 
  /**
   * Read from stream. Format:
   *
   * # Header1
   * # Header2
   * key0=val0
   * key1=val1
   * # comment
   * key2=val2
   * End
   *
   * (headers and comments are optional)
   *
   * @param utfOrIso88591 If true, read as UTF-8, otherwise read as ISO-8859-1.
   */
  private void read(LineReader br, int maxLength, int bufferSize, boolean utfOrIso88591, boolean allowMultiple, boolean allowBase64) throws IOException {
    boolean firstLine = true;
    boolean headerSection = true;
    List<String> headers = new ArrayList<String>();

    while (true) {
      String line = br.readLine(maxLength, bufferSize, utfOrIso88591);
      if (line == null) {
        if (firstLine) throw new EOFException();
        Logger.error(this, "No end marker");
        break;
      }
      if ((line.length() == 0)) continue; // ignore
      firstLine = false;

      char first = line.charAt(0);
      if (first == '#') {
        if (headerSection) {
          headers.add(line.substring(1).trim());
        }

      } else {
        if (headerSection) {
          if (headers.size() > 0) { this.header = headers.toArray(new String[headers.size()]); }
          headerSection = false;
        }

        int index = line.indexOf(KEYVALUE_SEPARATOR_CHAR);
        if(index >= 0) {
          // Mapping
          String before = line.substring(0, index).trim();
          String after = line.substring(index+1);
          if((!after.isEmpty()) && after.charAt(0) == '=' && allowBase64) {
            try {
              after = after.substring(1);
              after = after.replaceAll("\\s", "");
              after = Base64.decodeUTF8(after);
            } catch (IllegalBase64Exception e) {
              throw new IOException("Unable to decode UTF8, = should not be allowed as first character of a value");
            }
          }
          if(!shortLived) after = after.intern();
          put(before, after, allowMultiple, false, true);
        } else {
          endMarker = line;
          break;
        }
      }
    }
  }

  /** Get a value for a key as a String. This may be a top level value, or we will traverse the
   * tree, so can be used for any key=value or subset.subset.key=value etc.
   * @param key The key to look up.
   * @return The String value corresponding to the given key, or null if there is no such
   * key=value pair.
   */
    public synchronized String get(String key) {
       int idx = key.indexOf(MULTI_LEVEL_CHAR);
       if(idx == -1)
         return values.get(key);
       else if(idx == 0)
      return (subset("") == null) ? null : subset("").get(key.substring(1));
    else {
         if(subsets == null) return null;
         String before = key.substring(0, idx);
         String after = key.substring(idx+1);
         SimpleFieldSet fs = subsets.get(before);
         if(fs == null) return null;
         return fs.get(after);
       }
    }

    public String[] getAll(String key) {
      String k = get(key);
      if(k == null) return null;
      return split(k);
    }

    /** Get a list of String's from a single value, encoded in Base64. This is useful for storing
     * arbitrary String's that may contain illegal characters - the MULTI_VALUE_CHAR, newlines, etc.
     */
    public String[] getAllEncoded(String key) throws IllegalBase64Exception {
        String k = get(key);
        if(k == null) return null;
        String[] ret = split(k);
        for(int i=0;i<ret.length;i++) {
            ret[i] = Base64.decodeUTF8(ret[i]);
        }
        return ret;
    }

    /** Split a set of String's delimeted by MULTI_VALUE_CHAR, accepting empty strings at both ends.
     * E.g. ";blah;blah;blah;;" will give ["", "blah", "blah", "blah", "", ""].
     * Java 7 split() would give ["blah","blah","blah"].
     */
    public static String[] split(String string) {
      if(string == null) return EMPTY_STRING_ARRAY;
      // Java 7 version of String.split() trims the extra delimeters at each end.
      int emptyAtStart = 0;
      for(;emptyAtStart<string.length() && string.charAt(emptyAtStart) == MULTI_VALUE_CHAR;emptyAtStart++);
      if(emptyAtStart == string.length()) {
          String[] ret = new String[string.length()];
          for(int i=0;i<ret.length;i++) ret[i] = "";
          return ret;
      }
      int emptyAtEnd = 0;
      for(int i=string.length()-1; i>=0 && string.charAt(i) == MULTI_VALUE_CHAR;i--) emptyAtEnd++;
      string = string.substring(emptyAtStart, string.length() - emptyAtEnd);
      String[] split = string.split(String.valueOf(MULTI_VALUE_CHAR)); // slower???
      if(emptyAtStart != 0 || emptyAtEnd != 0) {
          String[] ret = new String[emptyAtStart+split.length+emptyAtEnd];
          System.arraycopy(split, 0, ret, emptyAtStart, split.length);
          split = ret;
          for(int i=0;i<split.length;i++)
              if(split[i] == null) split[i] = "";
      }
      return split;
  }

    /** Combine a list of String's into a single String, separating them by the MULTI_VALUE_CHAR. */
    private static String unsplit(String[] strings) {
    if (strings.length == 0) return "";
      StringBuilder sb = new StringBuilder();
      for(String s: strings) {
        sb.append(s);
        assert(s.indexOf(MULTI_VALUE_CHAR) == -1);
      sb.append(MULTI_VALUE_CHAR);
      }
    // assert(sb.length() > 0) -- always true as strings.length != 0
    // remove last MULTI_VALUE_CHAR
    sb.deleteCharAt(sb.length()-1);
      return sb.toString();
    }

    /**
     * Put contents of a fieldset, overwrite old values.
     */
    public void putAllOverwrite(SimpleFieldSet fs) {
      for(Map.Entry<String, String> entry: fs.values.entrySet()) {
        values.put(entry.getKey(), entry.getValue()); // overwrite old
      }
      if(fs.subsets == null) return;
  if(subsets == null) subsets = new HashMap<String, SimpleFieldSet>();
      for(Map.Entry<String, SimpleFieldSet> entry: fs.subsets.entrySet()) {
        String key = entry.getKey();
        SimpleFieldSet hisFS = entry.getValue();
        SimpleFieldSet myFS = subsets.get(key);
        if(myFS != null) {
          myFS.putAllOverwrite(hisFS);
        } else {
          subsets.put(key, hisFS);
        }
      }
    }

    /**
     * Set a key to a value. If the value already exists, throw IllegalStateException.
     * @param key The key.
     * @param value The value.
     */
    public void putSingle(String key, String value) {
      if(value == null) return;
      if(!shortLived) value = value.intern();
      if(!put(key, value, false, false, false))
        throw new IllegalStateException("Value already exists: "+value+" but want to set "+key+" to "+value);
    }

    /**
     * Aggregating put. Set a key to a value, if the value already exists, append to it.
     * If you do not need this functionality please use putOverwrite for a minimal
     * performance gain.
     *
     * @param key The key.
     * @param value The value.
     */
    public void putAppend(String key, String value) {
      if(value == null) return;
      if(!shortLived) value = value.intern();
      put(key, value, true, false, false);
    }

    /**
     * Set a key to a value, overwriting any existing value if present.
     * This function is a little bit faster than putAppend() because it does not
     * check whether the key already exists.
     *
     * @param key The key.
     * @param value The value.
     */
    public void putOverwrite(String key, String value) {
      if(value == null) return;
      if(!shortLived) value = value.intern();
      put(key, value, false, true, false);
    }

    /**
     * Set a key to a value.
     * @param key The key.
     * @param value The value.
     * @param allowMultiple If true, if the key already exists then the value will be
     * appended to the existing value. If false, we return false to indicate that the
     * old value is unchanged.
     * @return True unless allowMultiple was false and there was a pre-existing value,
     * or value was null.
     */
  private synchronized boolean put(String key, String value, boolean allowMultiple, boolean overwrite, boolean fromRead) {
    int idx;
    if(value == null) return true; // valid no-op
    if((!alwaysUseBase64) && value.indexOf('\n') != -1) throw new IllegalArgumentException("A simplefieldSet can't accept newlines !");
    if(allowMultiple && (!fromRead) && value.indexOf(MULTI_VALUE_CHAR) != -1) {
      throw new IllegalArgumentException("Appending a string to a SimpleFieldSet value should not contain the multi-value char \""+String.valueOf(MULTI_VALUE_CHAR)+"\" but it does: \"" +value+"\" for \""+key+"\"", new Exception("error"));
    }
    if((idx = key.indexOf(MULTI_LEVEL_CHAR)) == -1) {
      if(!shortLived) key = key.intern();

      if(overwrite) {
        values.put(key, value);
      } else {
        if(values.get(key) == null) {
          values.put(key, value);
        } else {
          if(!allowMultiple) return false;
          values.put(key, (values.get(key))+ MULTI_VALUE_CHAR +value);
        }
      }
    } else {
      String before = key.substring(0, idx);
      String after = key.substring(idx+1);
      SimpleFieldSet fs = null;
      if(subsets == null)
        subsets = new HashMap<String, SimpleFieldSet>();
      fs = subsets.get(before);
      if(fs == null) {
        fs = new SimpleFieldSet(shortLived, alwaysUseBase64);
        if(!shortLived) before = before.intern();
        subsets.put(before, fs);
      }
      fs.put(after, value, allowMultiple, overwrite, fromRead);
    }
    return true;
    }

  public void put(String key, int value) {
    // Use putSingle so it does the intern check
    putSingle(key, Integer.toString(value));
  }

  public void put(String key, long value) {
    putSingle(key, Long.toString(value));
  }

  public void put(String key, short value) {
    putSingle(key, Short.toString(value));
  }

  public void put(String key, char c) {
    putSingle(key, Character.toString(c));
  }

  public void put(String key, boolean b) {
    // Don't use putSingle, avoid intern check (Boolean.toString returns interned strings anyway)
    put(key, Boolean.toString(b), false, false, false);
  }

  public void put(String key, double windowSize) {
    putSingle(key, Double.toString(windowSize));
  }
 
  public void put(String key, byte[] bytes) {
      putSingle(key, Base64.encode(bytes));
  }

    /**
     * Write the contents of the SimpleFieldSet to a Writer.
     * Note: The caller *must* buffer the writer to avoid lousy performance!
     * (StringWriter is by definition buffered, otherwise wrap it in a BufferedWriter)
     *
     * @warning keep in mind that a Writer is not necessarily UTF-8!!
     */
  public void writeTo(Writer w) throws IOException {
    writeTo(w, "", false, false);
  }

    /**
     * Write the contents of the SimpleFieldSet to a Writer.
     * Note: The caller *must* buffer the writer to avoid lousy performance!
     * (StringWriter is by definition buffered, otherwise wrap it in a BufferedWriter)
     *
     * @param w The Writer to write to. @warning keep in mind that a Writer is not necessarily UTF-8!!
     * @param prefix String to prefix the keys with. (E.g. when writing a tree of SFS's).
     * @param noEndMarker If true, don't write the end marker (the last line, the only one with no
     * "=" in it).
     * @param useBase64 If true, use Base64 for any value that has control characters, whitespace,
     * or characters used by SimpleFieldSet in it. In this case the separator will be "==" not "=".
     * This is mainly useful for node references, which tend to lose whitespace, gain newlines etc
     * in transit. Can be overridden (to true) by alwaysUseBase64 setting.
     */
    synchronized void writeTo(Writer w, String prefix, boolean noEndMarker, boolean useBase64) throws IOException {
    writeHeader(w);
      for (Map.Entry<String, String> entry: values.entrySet()) {
      String key = entry.getKey();
      String value = entry.getValue();
      writeValue(w, key, value, prefix, useBase64);
      }
      if(subsets != null) {
        for (Map.Entry<String, SimpleFieldSet> entry: subsets.entrySet()) {
        String key = entry.getKey();
        SimpleFieldSet subset = entry.getValue();
          if(subset == null) throw new NullPointerException();
          subset.writeTo(w, prefix+key+MULTI_LEVEL_CHAR, true, useBase64);
        }
      }
      if(!noEndMarker) {
        if(endMarker == null)
          w.write("End\n");
        else {
          w.write(endMarker);
          w.write('\n');
        }
      }
    }

    private void writeValue(Writer w, String key, String value, String prefix, boolean useBase64) throws IOException {
        w.write(prefix);
        w.write(key);
        w.write(KEYVALUE_SEPARATOR_CHAR);
        if((useBase64 || alwaysUseBase64) && shouldBase64(value)) {
          w.write(KEYVALUE_SEPARATOR_CHAR);
          w.write(Base64.encodeUTF8(value));
        } else {
          w.write(value);
        }
        w.write('\n');
  }

  private boolean shouldBase64(String value) {
      for(int i=0;i<value.length();i++) {
        char c = value.charAt(i);
        if(c == SimpleFieldSet.KEYVALUE_SEPARATOR_CHAR) return true;
        if(c == SimpleFieldSet.MULTI_LEVEL_CHAR) return true;
        if(c == SimpleFieldSet.MULTI_VALUE_CHAR) return true;
        if(Character.isISOControl(c)) return true;
        if(Character.isWhitespace(c)) return true;
      }
      return false;
  }

  public void writeToOrdered(Writer w) throws IOException {
    writeToOrdered(w, "", false, false);
  }

  /**
   * Write the SimpleFieldSet to a Writer, in order.
   * @param w The Writer to write the SFS to.
   * @param prefix The prefix ("" at the top level, e.g. "something." if we are inside a sub-SFS
   * called "something").
   * @param noEndMarker If true, don't write the end marker (usually End). Again this is normally
   * set only in sub-SFS's.
   * @param allowOptionalBase64 If true, write fields as Base64 if they contain spaces etc. This
   * improves the robustness of e.g. node references, where the SFS can be written with or
   * without Base64. However, for SFS's where the values can contain <b>anything</b>, the member
   * flag alwaysUseBase64 will be set and we will write lines that need to be Base64 as such
   * regardless of this allowOptionalBase64.
   * @throws IOException If an error occurs writing to the Writer.
   */
    private synchronized void writeToOrdered(Writer w, String prefix, boolean noEndMarker, boolean allowOptionalBase64) throws IOException {
    writeHeader(w);
      String[] keys = values.keySet().toArray(new String[values.size()]);
      int i=0;

      // Sort
      Arrays.sort(keys);

      // Output
      for(i=0; i < keys.length; i++) {
        writeValue(w, keys[i], get(keys[i]), prefix, allowOptionalBase64);
      }

      if(subsets != null) {
        String[] orderedPrefixes = subsets.keySet().toArray(new String[subsets.size()]);
        // Sort
        Arrays.sort(orderedPrefixes);

          for(i=0; i < orderedPrefixes.length; i++) {
          SimpleFieldSet subset = subset(orderedPrefixes[i]);
          if(subset == null) throw new NullPointerException();
          subset.writeToOrdered(w, prefix+orderedPrefixes[i]+MULTI_LEVEL_CHAR, true, allowOptionalBase64);
        }
      }

      if(!noEndMarker) {
        if(endMarker == null)
          w.write("End\n");
        else
          w.write(endMarker+ '\n');
      }
    }

  private void writeHeader(Writer w) throws IOException {
    if (header != null) {
      for (String line: header) {
        w.write("# " + line + "\n");
      }
    }
  }

  @Override
    public String toString() {
        StringWriter sw = new StringWriter();
        try {
            writeTo(sw);
        } catch (IOException e) {
            Logger.error(this, "WTF?!: "+e+" in toString()!", e);
        }
        return sw.toString();
    }

    public String toOrderedString() {
      StringWriter sw = new StringWriter();
        try {
            writeToOrdered(sw);
        } catch (IOException e) {
            Logger.error(this, "WTF?!: "+e+" in toString()!", e);
        }
        return sw.toString();
    }

    public String toOrderedStringWithBase64() {
        StringWriter sw = new StringWriter();
        try {
            writeToOrdered(sw, "", false, true);
        } catch (IOException e) {
            Logger.error(this, "WTF?!: "+e+" in toString()!", e);
        }
        return sw.toString();
    }

    public String getEndMarker() {
      return endMarker;
    }

    public void setEndMarker(String s) {
      endMarker = s;
    }

  public synchronized SimpleFieldSet subset(String key) {
    if(subsets == null) return null;
    int idx = key.indexOf(MULTI_LEVEL_CHAR);
    if(idx == -1)
      return subsets.get(key);
    String before = key.substring(0, idx);
    String after = key.substring(idx+1);
    SimpleFieldSet fs = subsets.get(before);
    if(fs == null) return null;
    return fs.subset(after);
  }

  /**
   * Like subset(), only throws instead of returning null.
   * @throws FSParseException
   */
  public synchronized SimpleFieldSet getSubset(String key) throws FSParseException {
    SimpleFieldSet fs = subset(key);
    if(fs == null) throw new FSParseException("No such subset "+key);
    return fs;
  }

  /** Iterate over all keys in the SimpleFieldSet, even if they are at lower levels. */
  public Iterator<String> keyIterator() {
    return new KeyIterator("");
  }

  /** Iterate over all keys in the SimpleFieldSet, even if they are at lower levels.
   * @param prefix Add the given prefix to lower levels. This is used recursively by KeyIterator.
   */
  public KeyIterator keyIterator(String prefix) {
    return new KeyIterator(prefix);
  }

  /** Iterate over keys that are in the top level of the tree, i.e. that do not contain a ".".
   * E.g. "Name=Value" is a top level key. "Subset.Name=Value" is NOT a top level key. */
  public Iterator<String> toplevelKeyIterator() {
      return values.keySet().iterator();
  }
 
    public class KeyIterator implements Iterator<String> {
      final Iterator<String> valuesIterator;
      final Iterator<String> subsetIterator;
      KeyIterator subIterator;
      String prefix;

      /**
       * It provides an iterator for the SimpleSetField
       * which passes through every key.
       * (e.g. for key1=value1 key2.sub2=value2 key1.sub=value3
       * it will provide key1,key2.sub2,key1.sub)
       * @param a prefix to put BEFORE every key
       * (e.g. for key1=value, if the iterator is created with prefix "aPrefix",
       * it will provide aPrefixkey1
       */
      public KeyIterator(String prefix) {
        synchronized(SimpleFieldSet.this) {
          valuesIterator = values.keySet().iterator();
          if(subsets != null)
            subsetIterator = subsets.keySet().iterator();
          else
            subsetIterator = null;
          while(true) {
            if(valuesIterator != null && valuesIterator.hasNext()) break;
            if(subsetIterator == null || !subsetIterator.hasNext()) break;
            String name = subsetIterator.next();
            if(name == null) continue;
            SimpleFieldSet fs = subsets.get(name);
            if(fs == null) continue;
            String newPrefix = prefix + name + MULTI_LEVEL_CHAR;
            subIterator = fs.keyIterator(newPrefix);
            if(subIterator.hasNext()) break;
            subIterator = null;
          }
          this.prefix = prefix;
        }
      }

    @Override
    public boolean hasNext() {
      synchronized(SimpleFieldSet.this) {
        while(true) {
          if(valuesIterator.hasNext()) return true;
          if((subIterator != null) && subIterator.hasNext()) return true;
          if(subIterator != null) subIterator = null;
          if(subsetIterator != null && subsetIterator.hasNext()) {
            String key = subsetIterator.next();
            SimpleFieldSet fs = subsets.get(key);
            String newPrefix = prefix + key + MULTI_LEVEL_CHAR;
            subIterator = fs.keyIterator(newPrefix);
          } else
            return false;
        }
      }
    }

    @Override
    public final String next() {
      return nextKey();
    }

    public String nextKey() {
      synchronized(SimpleFieldSet.this) {
        String ret = null;
        if(valuesIterator != null && valuesIterator.hasNext()) {
          return prefix + valuesIterator.next();
        }
        // Iterate subsets.
        while(true) {
          if(subIterator != null && subIterator.hasNext()) {
            // If we have a retval, and we have a next value, return
            if(ret != null) return ret;
            ret = subIterator.next();
            if(subIterator.hasNext())
              // If we have a retval, and we have a next value, return
              if(ret != null) return ret;
          }
          // Otherwise, we need to get a new subIterator (or hasNext() will return false)
          subIterator = null;
          if(subsetIterator != null && subsetIterator.hasNext()) {
            String key = subsetIterator.next();
            SimpleFieldSet fs = subsets.get(key);
            String newPrefix = prefix + key + MULTI_LEVEL_CHAR;
            subIterator = fs.keyIterator(newPrefix);
          } else {
            // No more subIterator's
            if(ret == null) {
              //There is nothing to return and no more iterators, so we must be out
              //of elements
              throw new NoSuchElementException();
            }
            return ret;
          }
        }
      }
    }

    @Override
    public synchronized void remove() {
      throw new UnsupportedOperationException();
    }
  }
   
    /** Get a read-only map of direct key name:value pairs. Direct key values are things like
     * "Name=Value" (which would return a map containing "Name" -> "Value", NOT
     * "Subset.Name=Value" (which would not be returned). */
    public Map<String, String> directKeyValues() {
        return Collections.unmodifiableMap(values);
    }

    /** Get a read-only set of direct key names. So:
     * Name=Value
     * Subset.OtherName=Value
     * End
     * Would give "Name".
     * @return
     */
    public Set<String> directKeys() {
        return Collections.unmodifiableSet(values.keySet());
    }

    /** Get a read-only set of direct subsets. So:
     * Name=Value
     * Subset.OtherName=Value
     * End
     * Would give "OtherName" -> SFS containing OtherName=Value.
     * @return
     */
    public Map<String, SimpleFieldSet> directSubsets() {
        return Collections.unmodifiableMap(subsets);
    }

    /** Tolerant put(); does nothing if fs is empty */
    public void tput(String key, SimpleFieldSet fs) {
      if(fs == null || fs.isEmpty()) return;
      put(key, fs);
    }

    /** Add a name:value pair, traversing the tree and creating sub-SFS's if necessary. So we can
     * add("a.b.c.d", "value) even if there is no subset "a"; it will create it automatically.
     * @param key Name of the key to add.
     * @param fs Subset under the key.
     */
  public void put(String key, SimpleFieldSet fs) {
    if(fs == null) return; // legal no-op, because used everywhere
    if(fs.isEmpty()) // can't just no-op, because caller might add the FS then populate it...
      throw new IllegalArgumentException("Empty");
    if(subsets == null)
      subsets = new HashMap<String, SimpleFieldSet>();
    if(subsets.containsKey(key))
      throw new IllegalArgumentException("Already contains "+key+" but trying to add a SimpleFieldSet!");
    if(!shortLived) key = key.intern();
    subsets.put(key, fs);
  }

  /** Remove a name:value pair at any point in the tree. Will automatically traverse the tree and
   * remove empty subsets (which are not written anyway). */
  public synchronized void removeValue(String key) {
    int idx;
    if((idx = key.indexOf(MULTI_LEVEL_CHAR)) == -1) {
      values.remove(key);
    } else {
      if(subsets == null) return;
      String before = key.substring(0, idx);
      String after = key.substring(idx+1);
      SimpleFieldSet fs = subsets.get(before);
      if(fs == null) {
        return;
      }
      fs.removeValue(after);
      if(fs.isEmpty()) {
        subsets.remove(before);
        if(subsets.isEmpty())
          subsets = null;
      }
    }
  }

  /**
   * It removes the specified subset.
   * For example, in a SimpleFieldSet like this:
   * foo=bar
   * foo.bar=foobar
   * foo.bar.boo=foobarboo
   * calling it with the parameter "foo"
   * means to drop the second and the third line.
   * @param is the subset to remove
   */
  public synchronized void removeSubset(String key) {
    if(subsets == null) return;
    int idx;
    if((idx = key.indexOf(MULTI_LEVEL_CHAR)) == -1) {
      subsets.remove(key);
    } else {
      String before = key.substring(0, idx);
      String after = key.substring(idx+1);
      SimpleFieldSet fs = subsets.get(before);
      if(fs == null) {
        return;
      }
      fs.removeSubset(after);
      if(fs.isEmpty()) {
        subsets.remove(before);
        if(subsets.isEmpty())
          subsets = null;
      }
    }
  }

  /** Is this SimpleFieldSet empty? */
  public synchronized boolean isEmpty() {
    return values.isEmpty() && (subsets == null || subsets.isEmpty());
  }

  /** Iterator over the names of direct subsets, i.e. the tree nodes just below this one, not the
   * values. E.g.:
   * Foo.Bar.Bat=1
   * Baz.Boo=hello
   * Grrr=goodbye
   * End
   * Returns "Foo", "Baz".
   */
  public Iterator<String> directSubsetNameIterator() {
    return (subsets == null) ? null : subsets.keySet().iterator();
  }

  /** Get the names of direct subsets, i.e. the tree nodes just below this one, not the
     * values. E.g.:
     * Foo.Bar.Bat=1
     * Baz.Boo=hello
     * Grrr=goodbye
     * End
     * Returns [ "Foo", "Baz" ].
     */
  public String[] namesOfDirectSubsets() {
    return (subsets == null) ? EMPTY_STRING_ARRAY : subsets.keySet().toArray(new String[subsets.size()]);
  }

  /** Read a SimpleFieldSet from an InputStream in the standard format, using UTF-8, and not
   * allowing the Base64 encoding.
   * @param is The InputStream to read from. We will use the UTF-8 charset.
     * @param allowMultiple Whether to allow multiple entries for each key (and automatically
     * combine them). Not usually useful except maybe in FCP.
     * @param shortLived If true, don't intern the strings.
     * @return A new SimpleFieldSet.
     * @throws IOException If a read error occurs, including a formatting error, illegal
     * characters etc.
   */
  public static SimpleFieldSet readFrom(InputStream is, boolean allowMultiple, boolean shortLived) throws IOException {
      return readFrom(is, allowMultiple, shortLived, false, false);
  }

  /**
   * Read a SimpleFieldSet from an InputStream.
   * @param is The InputStream to read from. We will use the UTF-8 charset.
   * @param allowMultiple Whether to allow multiple entries for each key (and automatically
   * combine them). Not usually useful except maybe in FCP.
   * @param shortLived If true, don't intern the strings.
   * @param allowBase64 If true, allow reading Base64 encoded lines (key==base64(value)).
   * @param alwaysBase64 If true, the resulting SFS should have the alwaysUseBase64 flag enabled,
   * i.e it can store anything in key values including newlines, special chars such as = etc.
   * Otherwise, even if allowBase64 is enabled, invalid chars will not be.
   * @return A new SimpleFieldSet.
   * @throws IOException If a read error occurs, including a formatting error, illegal
   * characters etc.
   */
  public static SimpleFieldSet readFrom(InputStream is, boolean allowMultiple, boolean shortLived, boolean allowBase64, boolean alwaysBase64) throws IOException {
    BufferedInputStream bis = null;
    InputStreamReader isr = null;
    BufferedReader br = null;

    try {
      bis = new BufferedInputStream(is);
      try {
        isr = new InputStreamReader(bis, "UTF-8");
      } catch (UnsupportedEncodingException e) {
        Logger.error(SimpleFieldSet.class, "Impossible: "+e, e);
        is.close();
        throw new Error("Impossible: JVM doesn't support UTF-8: " + e, e);
      }
      br = new BufferedReader(isr);
      SimpleFieldSet fs = new SimpleFieldSet(br, allowMultiple, shortLived, allowBase64, alwaysBase64);
      br.close();

      return fs;
    } finally {
                        Closer.close(br);
                        Closer.close(isr);
                        Closer.close(bis);
                }
  }

  /** Read a SimpleFieldSet from a File. */
  public static SimpleFieldSet readFrom(File f, boolean allowMultiple, boolean shortLived) throws IOException {
      FileInputStream fis = new FileInputStream(f);
      try {
          return readFrom(fis, allowMultiple, shortLived);
      } finally {
          fis.close();
      }
  }

    /** Write to the given OutputStream (as UTF-8) and flush it. */
    public void writeTo(OutputStream os) throws IOException {
        writeTo(os, 4096);
    }  
   
  /** Write to the given OutputStream and flush it. Use a big buffer, for jobs that aren't called
   * too often e.g. persisting a file every 10 minutes. */
    public void writeToBigBuffer(OutputStream os) throws IOException {
      writeTo(os, 65536);
   
 
  /** Write to the given OutputStream and flush it. */
    public void writeTo(OutputStream os, int bufferSize) throws IOException {
        BufferedOutputStream bos = null;
        OutputStreamWriter osw = null;
        BufferedWriter bw = null;
       
        bos = new BufferedOutputStream(os, bufferSize);
        try {
            osw = new OutputStreamWriter(bos, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            Logger.error(SimpleFieldSet.class, "Impossible: " + e, e);
            throw e;
        }
        bw = new BufferedWriter(osw);
        writeTo(bw);
        bw.flush();
    }

    /** Get an integer value for the given key. This may be at the top level or lower in the tree,
     * it's just key=value. (Value in decimal)
     * @param key The key to fetch.
     * @param def The default value to return if the key does not exist or can't be parsed.
     * @return The integer value of the key, or the default value.
     */
  public int getInt(String key, int def) {
    String s = get(key);
    if(s == null) return def;
    try {
      return Integer.parseInt(s);
    } catch (NumberFormatException e) {
      return def;
    }
  }

    /** Get an integer value for the given key. This may be at the top level or lower in the tree,
     * it's just key=value. (Value in decimal)
     * @param key The key to fetch.
     * @return The integer value of the key, if it exists and is valid.
     * @throws FSParseException If the key=value pair does not exist or if the value cannot be
     * parsed as an integer.
     */
  public int getInt(String key) throws FSParseException {
    String s = get(key);
    if(s == null) throw new FSParseException("No key "+key);
    try {
      return Integer.parseInt(s);
    } catch (NumberFormatException e) {
      throw new FSParseException("Cannot parse "+s+" for integer "+key);
    }
  }

    /** Get a double precision value for the given key. This may be at the top level or lower in
     * the tree, it's just key=value. (Value in decimal)
     * @param key The key to fetch.
     * @param def The default value to return if the key does not exist or can't be parsed.
     * @return The integer value of the key, or the default value.
     */
  public double getDouble(String key, double def) {
    String s = get(key);
    if(s == null) return def;
    try {
      return Double.parseDouble(s);
    } catch (NumberFormatException e) {
      return def;
    }
  }

    /** Get a double precision value for the given key. This may be at the top level or lower in
     * the tree, it's just key=value. (Value in decimal)
     * @param key The key to fetch.
     * @return The value of the key as a double, if it exists and is valid.
     * @throws FSParseException If the key=value pair does not exist or if the value cannot be
     * parsed as a double.
     */
  public double getDouble(String key) throws FSParseException {
    String s = get(key);
    if(s == null) throw new FSParseException("No key "+key);
    try {
      return Double.parseDouble(s);
    } catch (NumberFormatException e) {
      throw new FSParseException("Cannot parse "+s+" for integer "+key);
    }
  }

    /** Get a long value for the given key. This may be at the top level or lower in the tree,
     * it's just key=value. (Value in decimal)
     * @param key The key to fetch.
     * @param def The default value to return if the key does not exist or can't be parsed.
     * @return The long value of the key, or the default value.
     */
  public long getLong(String key, long def) {
    String s = get(key);
    if(s == null) return def;
    try {
      return Long.parseLong(s);
    } catch (NumberFormatException e) {
      return def;
    }
  }

    /** Get a long value for the given key. This may be at the top level or lower in the tree,
     * it's just key=value. (Value in decimal)
     * @param key The key to fetch.
     * @return The value of the key as a long, if it exists and is valid.
     * @throws FSParseException If the key=value pair does not exist or if the value cannot be
     * parsed as a long.
     */
  public long getLong(String key) throws FSParseException {
    String s = get(key);
    if(s == null) throw new FSParseException("No key "+key);
    try {
      return Long.parseLong(s);
    } catch (NumberFormatException e) {
      throw new FSParseException("Cannot parse "+s+" for long "+key);
    }
  }

    /** Get a short value for the given key. This may be at the top level or lower in the tree,
     * it's just key=value. (Value in decimal)
     * @param key The key to fetch.
     * @return The value of the key as a short, if it exists and is valid.
     * @throws FSParseException If the key=value pair does not exist or if the value cannot be
     * parsed as a short.
     */
  public short getShort(String key) throws FSParseException {
    String s = get(key);
    if(s == null) throw new FSParseException("No key "+key);
    try {
      return Short.parseShort(s);
    } catch (NumberFormatException e) {
      throw new FSParseException("Cannot parse "+s+" for short "+key);
    }
  }

    /** Get a short value for the given key. This may be at the top level or lower in the tree,
     * it's just key=value. (Value in decimal)
     * @param key The key to fetch.
     * @return The value of the key as a short, if it exists and is valid.
     */
  public short getShort(String key, short def) {
    String s = get(key);
    if(s == null) return def;
    try {
      return Short.parseShort(s);
    } catch (NumberFormatException e) {
      return def;
    }
  }

  /** Get a byte value for the given key (represented as a number in decimal). This may be at
   * the top level or lower in the tree, it's just key=value. (Value in decimal)
     * @param key The key to fetch.
     * @return The value of the key as a byte, if it exists and is valid.
     * @throws FSParseException If the key=value pair does not exist or if the value cannot be
     * parsed as a byte.
     */
  public byte getByte(String key) throws FSParseException {
    String s = get(key);
    if(s == null) throw new FSParseException("No key " + key);
    try {
      return Byte.parseByte(s);
    } catch (NumberFormatException e) {
      throw new FSParseException("Cannot parse \"" + s + "\" as a byte.");
    }
  }

    /** Get a byte value for the given key (represented as a number in decimal). This may be at
     * the top level or lower in the tree, it's just key=value. (Value in decimal)
     * @param key The key to fetch.
     * @return The value of the key as a byte, if it exists and is valid, otherwise the default
     * value.
     */
  public byte getByte(String key, byte def) {
    try {
      return getByte(key);
    } catch (FSParseException e) {
      return def;
    }
  }

  /** Get a byte array for the given key (represented in Base64). The key may be at the top level
   * or further down the tree, so this is key=[base64 of value].
   * @param key The key to fetch.
   * @return The byte array to fetch.
   * @throws FSParseException If the key does not exist or cannot be parsed as a byte array.
   */
  public byte[] getByteArray(String key) throws FSParseException {
        String s = get(key);
        if(s == null) throw new FSParseException("No key " + key);
        try {
            return Base64.decode(s);
        } catch (IllegalBase64Exception e) {
            throw new FSParseException("Cannot parse value \""+s+"\" as a byte[]");
        }
  }

  /** Get a char for the given key (represented as a single character). The key may be at the
   * top level or further down the tree, so this is key=[character].
     * @param key The key to fetch.
   * @return The character to fetch.
   * @throws FSParseException If the key does not exist or there is more than one character.
   */
  public char getChar(String key) throws FSParseException {
    String s = get(key);
    if(s == null) throw new FSParseException("No key "+key);
      if (s.length() == 1)
        return s.charAt(0);
      else
        throw new FSParseException("Cannot parse "+s+" for char "+key);
  }

    /** Get a char for the given key (represented as a single character). The key may be at the
     * top level or further down the tree, so this is key=[character].
     * @param key The key to fetch.
     * @param def The default value to return if the key does not exist or can't be parsed.
     * @return The character to fetch.
     * @throws FSParseException If the key does not exist or there is more than one character.
     */
  public char getChar(String key, char def) {
    String s = get(key);
    if(s == null) return def;
      if (s.length() == 1)
        return s.charAt(0);
      else
        return def;
  }

  public boolean getBoolean(String key, boolean def) {
    return Fields.stringToBool(get(key), def);
  }

  public boolean getBoolean(String key) throws FSParseException {
    try {
        return Fields.stringToBool(get(key));
    } catch(NumberFormatException e) {
        throw new FSParseException(e);
    }
  }

  public void put(String key, int[] value) {
    removeValue(key);
    for(int v : value)
      putAppend(key, String.valueOf(v));
  }

  public void put(String key, double[] value) {
    removeValue(key);
    for(double v : value)
      putAppend(key, String.valueOf(v));
  }

  public void put(String key, float[] value) {
    removeValue(key);
    for (float v : value) putAppend(key, String.valueOf(v));
  }
 
    public void put(String key, short[] value) {
        removeValue(key);
        for (short v : value) putAppend(key, String.valueOf(v));
    }
   
    public void put(String key, long[] value) {
        removeValue(key);
        for (long v : value) putAppend(key, String.valueOf(v));
    }
   
    public void put(String key, boolean[] value) {
        removeValue(key);
        for (boolean v : value) putAppend(key, String.valueOf(v));
    }
   
  public int[] getIntArray(String key) {
    String[] strings = getAll(key);
    if(strings == null) return null;
    int[] ret = new int[strings.length];
    for(int i=0;i<strings.length;i++) {
      try {
        ret[i] = Integer.parseInt(strings[i]);
      } catch (NumberFormatException e) {
        Logger.error(this, "Cannot parse "+strings[i]+" : "+e, e);
        return null;
      }
    }
    return ret;
  }

    public short[] getShortArray(String key) {
        String[] strings = getAll(key);
        if(strings == null) return null;
        short[] ret = new short[strings.length];
        for(int i=0;i<strings.length;i++) {
            try {
                ret[i] = Short.parseShort(strings[i]);
            } catch (NumberFormatException e) {
                Logger.error(this, "Cannot parse "+strings[i]+" : "+e, e);
                return null;
            }
        }
        return ret;
    }

    public long[] getLongArray(String key) {
        String[] strings = getAll(key);
        if(strings == null) return null;
        long[] ret = new long[strings.length];
        for(int i=0;i<strings.length;i++) {
            try {
                ret[i] = Long.parseLong(strings[i]);
            } catch (NumberFormatException e) {
                Logger.error(this, "Cannot parse "+strings[i]+" : "+e, e);
                return null;
            }
        }
        return ret;
    }

  public double[] getDoubleArray(String key) {
    String[] strings = getAll(key);
    if(strings == null) return null;
    double[] ret = new double[strings.length];
    for(int i=0;i<strings.length;i++) {
      try {
        ret[i] = Double.valueOf(strings[i]);
      } catch(NumberFormatException e) {
        Logger.error(this, "Cannot parse "+strings[i]+" : "+e,e);
        return null;
      }
    }

    return ret;
  }

  public float[] getFloatArray(String key) {
    String[] strings = getAll(key);
    if(strings == null) return null;
    float[] ret = new float[strings.length];
    for(int i=0;i<strings.length;i++) {
      try {
        ret[i] = Float.valueOf(strings[i]);
      } catch(NumberFormatException e) {
        Logger.error(this, "Cannot parse "+strings[i]+" : "+e,e);
        return null;
      }
    }

    return ret;
  }

    public boolean[] getBooleanArray(String key) {
        String[] strings = getAll(key);
        if(strings == null) return null;
        boolean[] ret = new boolean[strings.length];
        for(int i=0;i<strings.length;i++) {
            try {
                ret[i] = Boolean.valueOf(strings[i]);
            } catch(NumberFormatException e) {
                Logger.error(this, "Cannot parse "+strings[i]+" : "+e,e);
                return null;
            }
        }

        return ret;
    }

  public void putOverwrite(String key, String[] strings) {
    putOverwrite(key, unsplit(strings));
  }

    public void putEncoded(String key, String[] strings) {
        String[] copy = Arrays.copyOf(strings, strings.length);
        for(int i=0;i<copy.length;i++) {
            copy[i] = Base64.encodeUTF8(strings[i]);
        }
        putSingle(key, unsplit(copy));
    }

  public String getString(String key) throws FSParseException {
    String s = get(key);
    if(s == null) throw new FSParseException("No such element "+key);
    return s;
  }

  /** Set the headers. This is a list of String's that are written before the name=value pairs.
   * Usually this is a comment (with each line starting with "#").
   * @param headers The list of lines to precede the SimpleFieldSet by when we write it.
   */
  public void setHeader(String... headers) {
    // FIXME LOW should really check that each line doesn't have a "\n" in it
    this.header = headers;
  }

  /** Get the headers. This is a list of String's that are written before the name=value pairs.
     * Usually this is a comment (with each line starting with "#"). */
  public String[] getHeader() {
    return this.header;
  }

  public void put(String key, String[] values) {
      putSingle(key, unsplit(values));
  }
}
TOP

Related Classes of freenet.support.SimpleFieldSet

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.