/* Copyright (c) 2014 Boundless and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Distribution License v1.0 * which accompanies this distribution, and is available at * https://www.eclipse.org/org/documents/edl-v10.html * * Contributors: * David Winslow (Boundless) - initial implementation */ package org.locationtech.geogig.storage.fs; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.util.ArrayList; 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.regex.Matcher; import java.util.regex.Pattern; import com.google.common.base.Optional; /** * Simple implementation of an INI file parser and serializer */ public abstract class INIFile { /** * Timestamp the ini file at latest read */ private long timestamp = 0; /** * Content of the ini file */ private List<Entry> data; public abstract File iniFile(); public synchronized Optional<String> get(String section, String key) throws IOException { if (section == null || section.length() == 0) { throw new IllegalArgumentException("Section name required"); } if (key == null || key.length() == 0) { throw new IllegalArgumentException("Key required"); } checkReload(); for (Entry e : data) { Optional<String> result = e.get(section, key); if (result.isPresent()) return result; } return Optional.absent(); } public synchronized Map<String, String> getAll() throws IOException { checkReload(); Map<String, String> m = new HashMap<String, String>(); for (Entry e : data) { if (e instanceof Section) { Section s = (Section) e; for (KeyAndValue kv : s.getValues()) { m.put(s.getHeader() + "." + kv.getKey(), kv.getValue()); } } } return m; } public synchronized List<String> listSubsections(String section) throws IOException { if (section == null || section.length() == 0) { throw new IllegalArgumentException("Section name required"); } checkReload(); List<String> results = new ArrayList<String>(); for (Entry e : data) { if (e instanceof Section) { Section s = (Section) e; if (s.getHeader().startsWith(section + ".")) { results.add(s.getHeader().substring(section.length() + 1)); } } } return results; } public synchronized Map<String, String> getSection(String section) throws IOException { if (section == null || section.length() == 0) { throw new IllegalArgumentException("Section name required"); } checkReload(); for (Entry e : data) { if (e instanceof Section) { Section s = (Section) e; if (s.getHeader().equals(section)) { Map<String, String> values = new HashMap<String, String>(); for (KeyAndValue kv : s.getValues()) { values.put(kv.getKey(), kv.getValue()); } return values; } } } return new HashMap<String, String>(); } public synchronized void set(String section, String key, String value) throws IOException { if (section == null || section.length() == 0) { throw new IllegalArgumentException("Section name required"); } if (key == null || key.length() == 0) { throw new IllegalArgumentException("Key required"); } checkReload(); boolean written = false; for (Entry e : data) { written = e.set(section, key, value); if (written) { break; } } if (!written) { // didn't add to an existing section, time to add a new section. List<KeyAndValue> kvs = new ArrayList<KeyAndValue>(); kvs.add(new KeyAndValue(key, value)); data.add(new Section(section, kvs)); } write(); } public synchronized void removeSection(String section) throws IOException { if (section == null || section.length() == 0) { throw new IllegalArgumentException("Section name required"); } checkReload(); boolean written = false; Iterator<Entry> iter = data.iterator(); while (iter.hasNext()) { Entry e = iter.next(); if (e instanceof Section && ((Section) e).getHeader().equals(section)) { iter.remove(); written = true; break; } } if (written) { write(); } else { throw new NoSuchElementException("No such section"); } } public synchronized void remove(String section, String key) throws IOException { if (section == null || section.length() == 0) { throw new IllegalArgumentException("Section name required"); } if (key == null || key.length() == 0) { throw new IllegalArgumentException("Section name required"); } checkReload(); boolean written = false; for (Entry e : data) { written |= e.unset(section, key); } if (written) { write(); } } private final static class KeyAndValue { private String key, value; public KeyAndValue(String key, String value) { this.key = key; this.value = value; } public String getKey() { return this.key; } public String getValue() { return this.value; } public void setValue(String value) { this.value = value; } } private static abstract class Entry { public abstract void write(PrintWriter w); public Optional<String> get(String section, String key) { // No-op return Optional.absent(); } public boolean set(String section, String key, String value) { // No-op return false; } public boolean unset(String section, String key) { return false; } } private static class Section extends Entry { private String header; private List<KeyAndValue> values; public Section(String header, List<KeyAndValue> values) { this.header = header; this.values = values; } public String getHeader() { return this.header; } public List<KeyAndValue> getValues() { return Collections.unmodifiableList(values); } @Override public Optional<String> get(String section, String key) { if (!header.equals(section)) { return Optional.absent(); } else { for (KeyAndValue kv : values) { if (kv.getKey().equals(key)) { return Optional.of(kv.getValue()); } } return Optional.absent(); } } @Override public boolean set(String section, String key, String value) { if (!header.equals(section)) { return false; } else { for (KeyAndValue kv : values) { if (kv.getKey().equals(key)) { kv.setValue(value); return true; } } values.add(new KeyAndValue(key, value)); return true; } } @Override public boolean unset(String section, String key) { if (!header.equals(section)) { return false; } else { boolean modified = false; Iterator<KeyAndValue> viterator = values.iterator(); while (viterator.hasNext()) { if (viterator.next().getKey().equals(key)) { viterator.remove(); modified = true; } } return modified; } } public void write(PrintWriter w) { w.println("[" + header.replaceAll("\\.", "\\\\") + "]"); for (KeyAndValue kv : values) { w.println(kv.getKey() + " = " + kv.getValue()); } } @Override public String toString() { StringBuffer buff = new StringBuffer(); buff.append("[" + header + "]"); for (KeyAndValue kv : values) { buff.append("[" + kv.getKey() + " : " + kv.getValue() + "]"); } return buff.toString(); } } private static class Blanks extends Entry { private int nBlanks; public Blanks(int nBlanks) { this.nBlanks = nBlanks; } public void write(PrintWriter w) { for (int i = nBlanks; i > 0; i--) { w.print(" "); } w.println(); } } private static class Comment extends Entry { private String content; public Comment(String content) { this.content = content; } public void write(PrintWriter w) { w.println("#" + content); } } private void checkReload() throws IOException { File ini = iniFile(); long currentTime = ini.lastModified(); if (data == null || currentTime > timestamp) { reload(ini); timestamp = currentTime; } } // Note. If you're tweaking these be careful, throwing an exception in a // static initializer prevents the class from being loaded entirely. private static Pattern SECTION_HEADER = Pattern .compile("^\\p{Space}*\\[([^\\[\\]]+)]\\p{Space}*$"); private static Pattern KEY_VALUE = Pattern .compile("^\\p{Space}*([^=\\p{Space}]+)\\p{Space}*=\\p{Space}*(.*)\\p{Space}*$"); private static Pattern BLANK = Pattern.compile("^(\\p{Space}*)$"); private static Pattern COMMENT = Pattern.compile("^\\p{Space}*#(.*)$"); private void reload(File ini) throws IOException { BufferedReader reader = null; try { reader = new BufferedReader(new InputStreamReader(new FileInputStream(ini))); String sectionName = null; List<Entry> results = new ArrayList<Entry>(); List<KeyAndValue> kvs = new ArrayList<KeyAndValue>(); String line; while ((line = reader.readLine()) != null) { Matcher m; if ((m = SECTION_HEADER.matcher(line)).matches()) { String header = m.group(1); if (sectionName != null) { results.add(new Section(sectionName, kvs)); kvs = new ArrayList<KeyAndValue>(); } sectionName = header.replaceAll("\\\\", "."); } else if ((m = KEY_VALUE.matcher(line)).matches()) { if (sectionName != null) { // if we haven't encountered a section name yet, // ignore the values String key = m.group(1); String value = m.group(2); kvs.add(new KeyAndValue(key, value)); } } else if ((m = BLANK.matcher(line)).matches()) { String blanks = m.group(1); results.add(new Blanks(blanks.length())); } else if ((m = COMMENT.matcher(line)).matches()) { String comment = m.group(1); results.add(new Comment(comment)); } // If no pattern matches we have an invalid .ini file but we just drop those lines. } if (sectionName != null) { results.add(new Section(sectionName, kvs)); } data = results; } catch (IOException e) { data = new ArrayList<Entry>(); } catch (RuntimeException e) { data = new ArrayList<Entry>(); } finally { if (reader != null) { reader.close(); } } } private void write() throws IOException { PrintWriter writer = new PrintWriter(new BufferedWriter(new OutputStreamWriter( new FileOutputStream(iniFile())))); try { for (Entry e : data) { e.write(writer); } } finally { writer.flush(); writer.close(); } } public static INIFile forFile(final File iniFile) { return new INIFile() { private final File file = iniFile; @Override public File iniFile() { return file; } }; } }