tikhomirov@69: /* tikhomirov@608: * Copyright (c) 2011-2013 TMate Software Ltd tikhomirov@69: * tikhomirov@69: * This program is free software; you can redistribute it and/or modify tikhomirov@69: * it under the terms of the GNU General Public License as published by tikhomirov@69: * the Free Software Foundation; version 2 of the License. tikhomirov@69: * tikhomirov@69: * This program is distributed in the hope that it will be useful, tikhomirov@69: * but WITHOUT ANY WARRANTY; without even the implied warranty of tikhomirov@69: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the tikhomirov@69: * GNU General Public License for more details. tikhomirov@69: * tikhomirov@69: * For information on how to redistribute this software under tikhomirov@69: * the terms of a license other than GNU General Public License tikhomirov@102: * contact TMate Software at support@hg4j.com tikhomirov@69: */ tikhomirov@69: package org.tmatesoft.hg.internal; tikhomirov@69: tikhomirov@378: import java.io.ByteArrayOutputStream; tikhomirov@69: import java.io.File; tikhomirov@378: import java.io.FileOutputStream; tikhomirov@69: import java.io.IOException; tikhomirov@378: import java.io.PrintStream; tikhomirov@378: import java.nio.ByteBuffer; tikhomirov@378: import java.nio.channels.FileChannel; tikhomirov@378: import java.nio.channels.FileLock; tikhomirov@69: import java.util.ArrayList; tikhomirov@69: import java.util.Collections; tikhomirov@378: import java.util.Iterator; tikhomirov@69: import java.util.LinkedHashMap; tikhomirov@69: import java.util.List; tikhomirov@69: import java.util.Map; tikhomirov@69: tikhomirov@483: import org.tmatesoft.hg.core.SessionContext; tikhomirov@483: import org.tmatesoft.hg.repo.HgInvalidFileException; tikhomirov@483: import org.tmatesoft.hg.util.LogFacility; tikhomirov@483: tikhomirov@69: /** tikhomirov@331: * .ini / .rc file reader tikhomirov@69: * @author Artem Tikhomirov tikhomirov@69: * @author TMate Software Ltd. tikhomirov@69: */ tikhomirov@69: public class ConfigFile { tikhomirov@69: tikhomirov@483: private final SessionContext sessionContext; tikhomirov@69: private List sections; tikhomirov@69: private List> content; tikhomirov@69: tikhomirov@483: public ConfigFile(SessionContext ctx) { tikhomirov@483: sessionContext = ctx; tikhomirov@69: } tikhomirov@69: tikhomirov@483: public void addLocation(File path) throws HgInvalidFileException { tikhomirov@69: read(path); tikhomirov@69: } tikhomirov@69: tikhomirov@114: public boolean hasSection(String sectionName) { tikhomirov@331: return sections == null ? false : sections.indexOf(sectionName) != -1; tikhomirov@114: } tikhomirov@114: tikhomirov@69: public List getSectionNames() { tikhomirov@114: return sections == null ? Collections.emptyList() : Collections.unmodifiableList(sections); tikhomirov@69: } tikhomirov@69: tikhomirov@498: /** tikhomirov@498: * Access map of section keys and values. tikhomirov@498: * @param sectionName name of the section to retrieve tikhomirov@498: * @return never null, empty map in case no section with specified name found tikhomirov@498: */ tikhomirov@69: public Map getSection(String sectionName) { tikhomirov@378: if (sections == null) { tikhomirov@114: return Collections.emptyMap(); tikhomirov@114: } tikhomirov@69: int x = sections.indexOf(sectionName); tikhomirov@69: if (x == -1) { tikhomirov@69: return Collections.emptyMap(); tikhomirov@69: } tikhomirov@69: return Collections.unmodifiableMap(content.get(x)); tikhomirov@69: } tikhomirov@69: tikhomirov@114: public boolean getBoolean(String sectionName, String key, boolean defaultValue) { tikhomirov@114: String value = getSection(sectionName).get(key); tikhomirov@114: if (value == null) { tikhomirov@114: return defaultValue; tikhomirov@114: } tikhomirov@114: for (String s : new String[] { "true", "yes", "on", "1" }) { tikhomirov@114: if (s.equalsIgnoreCase(value)) { tikhomirov@114: return true; tikhomirov@114: } tikhomirov@114: } tikhomirov@114: return false; tikhomirov@114: } tikhomirov@128: tikhomirov@128: public String getString(String sectionName, String key, String defaultValue) { tikhomirov@128: String value = getSection(sectionName).get(key); tikhomirov@128: return value == null ? defaultValue : value; tikhomirov@128: } tikhomirov@378: tikhomirov@487: public int getInteger(String sectionName, String key, int defaultValue) { tikhomirov@487: String value = getSection(sectionName).get(key); tikhomirov@487: return value == null ? defaultValue : Integer.parseInt(value); tikhomirov@487: } tikhomirov@487: tikhomirov@378: public void putString(String sectionName, String key, String newValue) { tikhomirov@378: Map section = null; tikhomirov@378: if (sections == null) { tikhomirov@378: // init, in case we didn't read any file with prev config tikhomirov@378: sections = new ArrayList(); tikhomirov@378: content = new ArrayList>(); tikhomirov@378: } tikhomirov@378: int x = sections.indexOf(sectionName); tikhomirov@378: if (x == -1) { tikhomirov@378: if (newValue == null) { tikhomirov@378: return; tikhomirov@378: } tikhomirov@378: sections.add(sectionName); tikhomirov@378: content.add(section = new LinkedHashMap()); tikhomirov@378: } else { tikhomirov@378: section = content.get(x); tikhomirov@378: } tikhomirov@378: if (newValue == null) { tikhomirov@378: section.remove(key); tikhomirov@378: } else { tikhomirov@378: section.put(key, newValue); tikhomirov@378: } tikhomirov@378: } tikhomirov@378: tikhomirov@483: private void read(File f) throws HgInvalidFileException { tikhomirov@114: if (f == null || !f.canRead()) { tikhomirov@114: return; tikhomirov@114: } tikhomirov@69: if (sections == null) { tikhomirov@69: sections = new ArrayList(); tikhomirov@69: content = new ArrayList>(); tikhomirov@69: } tikhomirov@483: new Parser().go(f, this); tikhomirov@483: ((ArrayList) sections).trimToSize(); tikhomirov@483: ((ArrayList) content).trimToSize(); tikhomirov@69: assert sections.size() == content.size(); tikhomirov@69: } tikhomirov@378: tikhomirov@378: public void writeTo(File f) throws IOException { tikhomirov@378: byte[] data = compose(); tikhomirov@378: if (!f.exists()) { tikhomirov@378: f.createNewFile(); tikhomirov@378: } tikhomirov@378: FileChannel fc = new FileOutputStream(f).getChannel(); tikhomirov@378: FileLock fl = fc.lock(); tikhomirov@378: try { tikhomirov@378: fc.write(ByteBuffer.wrap(data)); tikhomirov@378: } finally { tikhomirov@378: fl.release(); tikhomirov@378: fc.close(); tikhomirov@378: } tikhomirov@378: } tikhomirov@378: tikhomirov@378: private byte[] compose() { tikhomirov@378: ByteArrayOutputStream bos = new ByteArrayOutputStream(1024); tikhomirov@378: PrintStream ps = new PrintStream(bos); tikhomirov@378: Iterator sectionNames = sections.iterator(); tikhomirov@378: for (Map s : content) { tikhomirov@378: final String name = sectionNames.next(); // iterate through names despite section may be empty tikhomirov@378: if (s.isEmpty()) { tikhomirov@378: continue; // do not write an empty section tikhomirov@378: } tikhomirov@378: ps.print('['); tikhomirov@378: ps.print(name); tikhomirov@378: ps.println(']'); tikhomirov@378: for (Map.Entry e : s.entrySet()) { tikhomirov@378: ps.print(e.getKey()); tikhomirov@378: ps.print('='); tikhomirov@378: ps.println(e.getValue()); tikhomirov@378: } tikhomirov@378: ps.println(); tikhomirov@378: } tikhomirov@378: ps.flush(); tikhomirov@378: return bos.toByteArray(); tikhomirov@378: } tikhomirov@483: tikhomirov@483: private static class Parser implements LineReader.LineConsumer { tikhomirov@483: tikhomirov@483: private String sectionName = ""; tikhomirov@483: private Map section = new LinkedHashMap(); tikhomirov@483: private File contextFile; tikhomirov@483: tikhomirov@608: // TODO [post-1.1] "" and lists tikhomirov@483: // XXX perhaps, single string to keep whole section with substrings for keys/values to minimize number of arrays (String.value) tikhomirov@483: public boolean consume(String line, ConfigFile cfg) throws IOException { tikhomirov@483: int x; tikhomirov@483: if ((x = line.indexOf('#')) != -1) { tikhomirov@483: // do not keep comments in memory, get new, shorter string tikhomirov@483: line = new String(line.substring(0, x).trim()); tikhomirov@483: } tikhomirov@483: if (line.length() <= 2) { // a=b or [a] are at least of length 3 tikhomirov@483: return true; tikhomirov@483: } tikhomirov@483: if (line.charAt(0) == '[' && line.charAt(line.length() - 1) == ']') { tikhomirov@483: sectionName = line.substring(1, line.length() - 1); tikhomirov@483: if (cfg.sections.indexOf(sectionName) == -1) { tikhomirov@483: cfg.sections.add(sectionName); tikhomirov@483: cfg.content.add(section = new LinkedHashMap()); tikhomirov@483: } else { tikhomirov@483: section = null; // drop cached value tikhomirov@483: } tikhomirov@483: } else if (line.startsWith("%unset")) { tikhomirov@483: if (section != null) { tikhomirov@483: section.remove(line.substring(7).trim()); tikhomirov@483: } tikhomirov@483: } else if (line.startsWith("%include")) { tikhomirov@483: processInclude(line.substring(9).trim(), cfg); tikhomirov@483: } else if ((x = line.indexOf('=')) != -1) { tikhomirov@483: // share char[] of the original string tikhomirov@483: String key = line.substring(0, x).trim(); tikhomirov@483: String value = line.substring(x+1).trim(); tikhomirov@483: if (section == null) { tikhomirov@483: int i = cfg.sections.indexOf(sectionName); tikhomirov@483: assert i >= 0; tikhomirov@483: section = cfg.content.get(i); tikhomirov@483: } tikhomirov@483: if (sectionName.length() == 0) { tikhomirov@483: // add fake section only if there are any values tikhomirov@483: cfg.sections.add(sectionName); tikhomirov@483: cfg.content.add(section); tikhomirov@483: } tikhomirov@483: section.put(key, value); tikhomirov@483: } tikhomirov@483: return true; tikhomirov@483: } tikhomirov@483: tikhomirov@483: public void go(File f, ConfigFile cfg) throws HgInvalidFileException { tikhomirov@483: contextFile = f; tikhomirov@483: LineReader lr = new LineReader(f, cfg.sessionContext.getLog()); tikhomirov@483: lr.ignoreLineComments("#"); tikhomirov@483: lr.read(this, cfg); tikhomirov@483: } tikhomirov@483: tikhomirov@483: // include failure doesn't propagate tikhomirov@483: private void processInclude(String includeValue, ConfigFile cfg) { tikhomirov@483: File f; tikhomirov@608: // TODO [post-1.1] handle environment variable expansion tikhomirov@483: if (includeValue.startsWith("~/")) { tikhomirov@483: f = new File(System.getProperty("user.home"), includeValue.substring(2)); tikhomirov@483: } else { tikhomirov@483: f = new File(contextFile.getParentFile(), includeValue); tikhomirov@483: } tikhomirov@483: try { tikhomirov@483: if (f.canRead()) { tikhomirov@483: new Parser().go(f, cfg); tikhomirov@483: } else { tikhomirov@483: LogFacility lf = cfg.sessionContext.getLog(); tikhomirov@483: lf.dump(ConfigFile.class, LogFacility.Severity.Debug, "Can't read file to include: %s", f); tikhomirov@483: } tikhomirov@483: } catch (HgInvalidFileException ex) { tikhomirov@483: LogFacility lf = cfg.sessionContext.getLog(); tikhomirov@483: lf.dump(ConfigFile.class, LogFacility.Severity.Warn, "Can't include %s (%s)", f, includeValue); tikhomirov@483: } tikhomirov@483: } tikhomirov@483: } tikhomirov@69: }