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