tikhomirov@497: /* tikhomirov@497: * Copyright (c) 2012 TMate Software Ltd tikhomirov@497: * tikhomirov@497: * This program is free software; you can redistribute it and/or modify tikhomirov@497: * it under the terms of the GNU General Public License as published by tikhomirov@497: * the Free Software Foundation; version 2 of the License. tikhomirov@497: * tikhomirov@497: * This program is distributed in the hope that it will be useful, tikhomirov@497: * but WITHOUT ANY WARRANTY; without even the implied warranty of tikhomirov@497: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the tikhomirov@497: * GNU General Public License for more details. tikhomirov@497: * tikhomirov@497: * For information on how to redistribute this software under tikhomirov@497: * the terms of a license other than GNU General Public License tikhomirov@497: * contact TMate Software at support@hg4j.com tikhomirov@497: */ tikhomirov@497: package org.tmatesoft.hg.internal; tikhomirov@497: tikhomirov@497: import java.io.ByteArrayInputStream; tikhomirov@497: import java.io.ByteArrayOutputStream; tikhomirov@497: import java.io.IOException; tikhomirov@497: import java.io.InputStream; tikhomirov@497: import java.io.OutputStream; tikhomirov@497: import java.util.ArrayList; tikhomirov@497: import java.util.Collections; tikhomirov@497: import java.util.HashSet; tikhomirov@497: import java.util.Iterator; tikhomirov@497: import java.util.LinkedHashMap; tikhomirov@497: import java.util.LinkedHashSet; tikhomirov@497: import java.util.List; tikhomirov@497: tikhomirov@497: /** tikhomirov@497: * Simplistic parser to allow altering configuration files without touching user modifications/formatting/comments tikhomirov@497: * tikhomirov@497: * @author Artem Tikhomirov tikhomirov@497: * @author TMate Software Ltd. tikhomirov@497: */ tikhomirov@497: public class ConfigFileParser { tikhomirov@497: private enum ParseState {Initial, Section, Entry}; tikhomirov@497: private ParseState state = ParseState.Initial; tikhomirov@497: private int lastNonEmptyLineEndOffset = -1; tikhomirov@497: private String sectionName; tikhomirov@497: private int sectionStart = -1; tikhomirov@497: private String entryKey; tikhomirov@497: private int entryStart = -1; tikhomirov@497: private int valueStart = -1, valueEnd = -1; tikhomirov@497: private ArrayList entries; tikhomirov@497: private ArrayList
sections = new ArrayList
(); tikhomirov@497: private byte[] contents; tikhomirov@497: tikhomirov@497: private List deletions = new ArrayList(5); tikhomirov@497: private List additions = new ArrayList(5), changes = new ArrayList(5); tikhomirov@497: tikhomirov@497: tikhomirov@497: public boolean exists(String section, String key) { tikhomirov@497: assert contents != null; tikhomirov@497: for (Section s : sections) { tikhomirov@497: if (s.name.equals(section)) { tikhomirov@497: for (Entry e : s.entries) { tikhomirov@497: if (e.name.equals(key)) { tikhomirov@497: return true; tikhomirov@497: } tikhomirov@497: } tikhomirov@497: return false; tikhomirov@497: } tikhomirov@497: } tikhomirov@497: return false; tikhomirov@497: } tikhomirov@497: tikhomirov@497: public void add(String section, String key, String newValue) { tikhomirov@497: additions.add(section); tikhomirov@497: additions.add(key); tikhomirov@497: additions.add(newValue); tikhomirov@497: } tikhomirov@497: tikhomirov@497: public void change(String section, String key, String newValue) { tikhomirov@497: changes.add(section); tikhomirov@497: changes.add(key); tikhomirov@497: changes.add(newValue); tikhomirov@497: } tikhomirov@497: tikhomirov@497: public void delete(String section, String key) { tikhomirov@497: deletions.add(section); tikhomirov@497: deletions.add(key); tikhomirov@497: } tikhomirov@497: tikhomirov@497: public void parse(InputStream is) throws IOException { tikhomirov@497: state = ParseState.Initial; tikhomirov@497: sections.clear(); tikhomirov@497: contents = null; tikhomirov@497: ByteArrayOutputStream bos = new ByteArrayOutputStream(1024); tikhomirov@497: ByteArrayOutputStream line = new ByteArrayOutputStream(80); tikhomirov@497: int offset = 0; tikhomirov@497: int lineOffset = -1; tikhomirov@497: int lineNumber = 1; tikhomirov@497: boolean crDetected = false; // true when previous char was \r tikhomirov@497: int b; tikhomirov@497: while ( (b = is.read()) != -1) { tikhomirov@497: bos.write(b); tikhomirov@497: if (b == '\n' || b == '\r') { tikhomirov@497: if (line.size() > 0) { tikhomirov@497: processLine(lineNumber, lineOffset, line.toByteArray()); tikhomirov@497: line.reset(); tikhomirov@497: lineOffset = -1; tikhomirov@497: lastNonEmptyLineEndOffset = bos.size() - 1; // offset points to EOL char tikhomirov@497: } tikhomirov@497: // else: XXX does empty line closes entry??? tikhomirov@497: // when \n follows \r, increment line count only once tikhomirov@497: if (!(b == '\n' && crDetected)) { tikhomirov@497: lineNumber++; tikhomirov@497: } tikhomirov@497: crDetected = b == '\r'; tikhomirov@497: } else { tikhomirov@497: crDetected = false; tikhomirov@497: if (line.size() == 0) { tikhomirov@497: lineOffset = offset; tikhomirov@497: } tikhomirov@497: line.write(b); tikhomirov@497: } tikhomirov@497: offset++; tikhomirov@497: } tikhomirov@497: // handle last line in case it's not EOL-terminated tikhomirov@497: if (line.size() > 0) { tikhomirov@497: processLine(lineNumber, lineOffset, line.toByteArray()); tikhomirov@497: // might need it for #closeSection() below tikhomirov@497: lastNonEmptyLineEndOffset = bos.size(); tikhomirov@497: } tikhomirov@497: if (state == ParseState.Entry) { tikhomirov@497: closeEntry(); tikhomirov@497: } tikhomirov@497: if (state == ParseState.Section) { tikhomirov@497: closeSection(); tikhomirov@497: } tikhomirov@497: contents = bos.toByteArray(); tikhomirov@497: } tikhomirov@497: tikhomirov@497: public void update(OutputStream out) throws IOException { tikhomirov@497: if (contents == null) { tikhomirov@497: throw new IOException("Shall parse first"); tikhomirov@497: } tikhomirov@497: HashSet processedSections = new HashSet(); tikhomirov@497: int contentsOffset = 0; tikhomirov@497: for (Section section : sections) { tikhomirov@497: LinkedHashMap additionsInSection = new LinkedHashMap(); tikhomirov@497: LinkedHashMap changesInSection = new LinkedHashMap(); tikhomirov@497: LinkedHashSet deletionsInSection = new LinkedHashSet(); tikhomirov@497: if (!processedSections.contains(section.name)) { tikhomirov@497: for (Iterator it = additions.iterator(); it.hasNext();) { tikhomirov@497: String s = it.next(), k = it.next(), v = it.next(); tikhomirov@497: if (section.name.equals(s)) { tikhomirov@497: additionsInSection.put(k, v); tikhomirov@497: } tikhomirov@497: } tikhomirov@497: for (Iterator it = changes.iterator(); it.hasNext();) { tikhomirov@497: String s = it.next(), k = it.next(), v = it.next(); tikhomirov@497: if (section.name.equals(s)) { tikhomirov@497: changesInSection.put(k, v); tikhomirov@497: } tikhomirov@497: } tikhomirov@497: for (Iterator it = deletions.iterator(); it.hasNext();) { tikhomirov@497: String s = it.next(), k = it.next(); tikhomirov@497: if (section.name.equals(s)) { tikhomirov@497: deletionsInSection.add(k); tikhomirov@497: } tikhomirov@497: } tikhomirov@497: } tikhomirov@497: for (Entry e : section.entries) { tikhomirov@497: if (deletionsInSection.contains(e.name)) { tikhomirov@497: // write up to key start only tikhomirov@497: out.write(contents, contentsOffset, e.start - contentsOffset); tikhomirov@497: contentsOffset = e.valueEnd + 1; tikhomirov@497: } else if (changesInSection.containsKey(e.name)) { tikhomirov@497: if (e.valueStart == -1) { tikhomirov@497: // e.valueEnd determines insertion point tikhomirov@497: out.write(contents, contentsOffset, e.valueEnd + 1 - contentsOffset); tikhomirov@497: } else { tikhomirov@497: // e.valueEnd points to last character of the value tikhomirov@497: out.write(contents, contentsOffset, e.valueStart - contentsOffset); tikhomirov@497: } tikhomirov@497: String value = changesInSection.get(e.name); tikhomirov@497: out.write(value == null ? new byte[0] : value.getBytes()); tikhomirov@497: contentsOffset = e.valueEnd + 1; tikhomirov@497: } tikhomirov@497: // else: keep contentsOffset to point to first uncopied character tikhomirov@497: } tikhomirov@497: if (section.entries.length == 0) { tikhomirov@497: // no entries, empty or only comments, perhaps. tikhomirov@497: // use end of last meaningful line (whether [section] or comment string), tikhomirov@497: // which points to newline character tikhomirov@497: out.write(contents, contentsOffset, section.end - contentsOffset); tikhomirov@497: contentsOffset = section.end; tikhomirov@497: // since it's tricky to track \n or \r\n with lastNonEmptyLineEndOffset, tikhomirov@497: // we copy up to the line delimiter and insert new lines, if any, with \n prepended, tikhomirov@497: // so that original EOL will be moved to the very end of the section. tikhomirov@497: // Indeed, would be better to insert *after* lastNonEmptyLineEndOffset, tikhomirov@497: // but I don't want to complicate #parse (if line.size() > 0 part) method. tikhomirov@497: // Hope, this won't make too much trouble (if any, at all - tikhomirov@497: // if String.format translates \n to system EOL, then nobody would notice) tikhomirov@497: } tikhomirov@497: if (!additionsInSection.isEmpty()) { tikhomirov@497: // make sure additions are written once everything else is there tikhomirov@497: out.write(contents, contentsOffset, section.end - contentsOffset); tikhomirov@497: contentsOffset = section.end; tikhomirov@497: for (String k : additionsInSection.keySet()) { tikhomirov@497: String v = additionsInSection.get(k); tikhomirov@497: out.write(String.format("\n%s = %s", k, v == null ? "" : v).getBytes()); tikhomirov@497: } tikhomirov@497: } tikhomirov@497: // if section comes more than once, update only first one. tikhomirov@497: processedSections.add(section.name); tikhomirov@497: } tikhomirov@497: out.write(contents, contentsOffset, contents.length - contentsOffset); tikhomirov@497: } tikhomirov@497: tikhomirov@497: private void processLine(int lineNumber, int offset, byte[] line) throws IOException { tikhomirov@497: int localOffset = 0, i = 0; tikhomirov@497: while (i < line.length && Character.isWhitespace(line[i])) { tikhomirov@497: i++; tikhomirov@497: } tikhomirov@497: if (i == line.length) { tikhomirov@497: return; tikhomirov@497: } tikhomirov@497: localOffset = i; tikhomirov@497: if (line[i] == '[') { tikhomirov@497: if (state == ParseState.Entry) { tikhomirov@497: closeEntry(); tikhomirov@497: } tikhomirov@497: if (state == ParseState.Section) { tikhomirov@497: closeSection(); tikhomirov@497: } tikhomirov@497: tikhomirov@497: while (i < line.length && line[i] != ']') { tikhomirov@497: i++; tikhomirov@497: } tikhomirov@497: if (i == line.length) { tikhomirov@497: throw new IOException(String.format("Can't find closing ']' for section name in line %d", lineNumber)); tikhomirov@497: } tikhomirov@497: sectionName = new String(line, localOffset+1, i-localOffset-1); tikhomirov@497: sectionStart = offset + localOffset; tikhomirov@497: state = ParseState.Section; tikhomirov@497: } else if (line[i] == '#' || line[i] == ';') { tikhomirov@497: // comment line, nothing to process tikhomirov@497: return; tikhomirov@497: } else { tikhomirov@497: // entry tikhomirov@497: if (state == ParseState.Initial) { tikhomirov@497: throw new IOException(String.format("Line %d doesn't belong to any section", lineNumber)); tikhomirov@497: } tikhomirov@497: if (localOffset > 0) { tikhomirov@497: if (state == ParseState.Section) { tikhomirov@497: throw new IOException(String.format("Non-indented key is expected in line %d", lineNumber)); tikhomirov@497: } tikhomirov@497: assert state == ParseState.Entry; tikhomirov@497: // whitespace-indented continuation of the previous entry tikhomirov@497: if (valueStart == -1) { tikhomirov@497: // value didn't start at the same line the key was found at tikhomirov@497: valueStart = offset + localOffset; tikhomirov@497: } tikhomirov@497: // value ends with eol (assumption is trailing comments are not allowed) tikhomirov@497: valueEnd = offset + line.length - 1; tikhomirov@497: } else { tikhomirov@497: if (state == ParseState.Entry) { tikhomirov@497: closeEntry(); tikhomirov@497: } tikhomirov@497: assert state == ParseState.Section; tikhomirov@497: // it's a new entry tikhomirov@497: state = ParseState.Entry; tikhomirov@497: // get name of the entry tikhomirov@497: while (i < line.length && !Character.isWhitespace(line[i]) && line[i] != '=') { tikhomirov@497: i++; tikhomirov@497: } tikhomirov@497: if (i == line.length) { tikhomirov@497: throw new IOException(String.format("Can't process entry in line %d", lineNumber)); tikhomirov@497: } tikhomirov@497: entryKey = new String(line, localOffset, i - localOffset); tikhomirov@497: entryStart = offset + localOffset; tikhomirov@497: // look for '=' after key name tikhomirov@497: while (i < line.length && line[i] != '=') { tikhomirov@497: i++; tikhomirov@497: } tikhomirov@497: if (i == line.length) { tikhomirov@497: throw new IOException(String.format("Can't find '=' after key %s in line %d", entryKey, lineNumber)); tikhomirov@497: } tikhomirov@497: // skip whitespaces after '=' tikhomirov@497: i++; // line[i] == '=' tikhomirov@497: while (i < line.length && Character.isWhitespace(line[i])) { tikhomirov@497: i++; tikhomirov@497: } tikhomirov@497: // valueStart might be -1 in case no value is specified in the same line as key tikhomirov@497: // but valueEnd is always initialized just in case there's no next, value continuation line tikhomirov@497: if (i == line.length) { tikhomirov@497: valueStart = -1; tikhomirov@497: } else { tikhomirov@497: valueStart = offset + i; tikhomirov@497: } tikhomirov@497: tikhomirov@497: // if trailing comments are allowed, shall tikhomirov@497: // look up comment char and set valueEnd to its position-1 tikhomirov@497: valueEnd = offset + line.length - 1; tikhomirov@497: } tikhomirov@497: } tikhomirov@497: } tikhomirov@497: tikhomirov@497: private void closeSection() { tikhomirov@497: assert state == ParseState.Section; tikhomirov@497: assert sectionName != null; tikhomirov@497: assert lastNonEmptyLineEndOffset != -1; tikhomirov@497: Section s = new Section(sectionName, sectionStart, lastNonEmptyLineEndOffset, entries == null ? Collections.emptyList() : entries); tikhomirov@497: sections.add(s); tikhomirov@497: sectionName = null; tikhomirov@497: sectionStart = -1; tikhomirov@497: state = ParseState.Initial; tikhomirov@497: entries = null; tikhomirov@497: } tikhomirov@497: tikhomirov@497: private void closeEntry() { tikhomirov@497: assert state == ParseState.Entry; tikhomirov@497: assert entryKey != null; tikhomirov@497: state = ParseState.Section; tikhomirov@497: Entry e = new Entry(entryKey, entryStart, valueStart, valueEnd); tikhomirov@497: if (entries == null) { tikhomirov@497: entries = new ArrayList(); tikhomirov@497: } tikhomirov@497: entries.add(e); tikhomirov@497: entryKey = null; tikhomirov@497: entryStart = valueStart = valueEnd -1; tikhomirov@497: } tikhomirov@497: tikhomirov@497: tikhomirov@497: private static class Block { tikhomirov@497: public final int start; tikhomirov@497: Block(int s) { tikhomirov@497: start = s; tikhomirov@497: } tikhomirov@497: } tikhomirov@497: tikhomirov@497: private static class Entry extends Block { tikhomirov@497: public final int valueStart, valueEnd; tikhomirov@497: public final String name; tikhomirov@497: tikhomirov@497: Entry(String n, int s, int vs, int ve) { tikhomirov@497: super(s); tikhomirov@497: name = n; tikhomirov@497: valueStart = vs; tikhomirov@497: valueEnd = ve; tikhomirov@497: } tikhomirov@497: } tikhomirov@497: tikhomirov@497: private static class Section extends Block { tikhomirov@497: public final String name; tikhomirov@497: public final Entry[] entries; tikhomirov@497: public final int end; tikhomirov@497: tikhomirov@497: Section(String n, int s, int endOffset, List e) { tikhomirov@497: super(s); tikhomirov@497: name = n; tikhomirov@497: end = endOffset; tikhomirov@497: entries = new Entry[e.size()]; tikhomirov@497: e.toArray(entries); tikhomirov@497: } tikhomirov@497: } tikhomirov@497: tikhomirov@497: public static void main(String[] args) throws Exception { tikhomirov@497: ConfigFileParser p = new ConfigFileParser(); tikhomirov@497: p.parse(new ByteArrayInputStream(xx.getBytes())); tikhomirov@497: System.out.println(">>>"); tikhomirov@497: System.out.println(xx); tikhomirov@497: System.out.println("==="); tikhomirov@497: p.add("sect1", "key5", "x"); tikhomirov@497: ByteArrayOutputStream out = new ByteArrayOutputStream(xx.length()); tikhomirov@497: p.update(out); tikhomirov@497: System.out.println(new String(out.toByteArray())); tikhomirov@497: /* tikhomirov@497: for (Section s : p.sections) { tikhomirov@497: System.out.printf("[%s@%d]\n", s.name, s.start); tikhomirov@497: for (Entry e : s.entries) { tikhomirov@497: System.out.printf("%s@%d = %d..%d\n", e.name, e.start, e.valueStart, e.valueEnd); tikhomirov@497: } tikhomirov@497: } tikhomirov@497: */ tikhomirov@497: } tikhomirov@497: private static final String xx = "#comment1\n [sect1]\nkey = value #not a comment2\n#comment3\nkey2= \nkey3 = \n value1, #cc\n value2\nkey4 = v1,\n v2 \n ,v3\n\n\n[sect2]\nx = a"; tikhomirov@497: }