tikhomirov@378: /* tikhomirov@378: * Copyright (c) 2012 TMate Software Ltd tikhomirov@378: * tikhomirov@378: * This program is free software; you can redistribute it and/or modify tikhomirov@378: * it under the terms of the GNU General Public License as published by tikhomirov@378: * the Free Software Foundation; version 2 of the License. tikhomirov@378: * tikhomirov@378: * This program is distributed in the hope that it will be useful, tikhomirov@378: * but WITHOUT ANY WARRANTY; without even the implied warranty of tikhomirov@378: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the tikhomirov@378: * GNU General Public License for more details. tikhomirov@378: * tikhomirov@378: * For information on how to redistribute this software under tikhomirov@378: * the terms of a license other than GNU General Public License tikhomirov@378: * contact TMate Software at support@hg4j.com tikhomirov@378: */ tikhomirov@378: package org.tmatesoft.hg.core; tikhomirov@378: tikhomirov@378: import java.io.File; tikhomirov@498: import java.io.FileInputStream; tikhomirov@498: import java.io.FileOutputStream; tikhomirov@378: import java.io.IOException; tikhomirov@498: import java.util.LinkedList; tikhomirov@378: import java.util.List; tikhomirov@378: import java.util.Map; tikhomirov@378: tikhomirov@378: import org.tmatesoft.hg.internal.ConfigFile; tikhomirov@498: import org.tmatesoft.hg.internal.ConfigFileParser; tikhomirov@378: import org.tmatesoft.hg.internal.Internals; tikhomirov@382: import org.tmatesoft.hg.repo.HgInternals; tikhomirov@498: import org.tmatesoft.hg.repo.HgInvalidStateException; tikhomirov@378: import org.tmatesoft.hg.repo.HgRepository; tikhomirov@378: tikhomirov@378: /** tikhomirov@498: * Command to alter Mercurial configuration settings at various levels (system-wide, user-wide, repository-wide). tikhomirov@378: * tikhomirov@378: * @author Artem Tikhomirov tikhomirov@378: * @author TMate Software Ltd. tikhomirov@378: */ tikhomirov@378: public final class HgUpdateConfigCommand extends HgAbstractCommand { tikhomirov@378: tikhomirov@498: private final SessionContext sessionCtx; tikhomirov@378: private final File configFile; tikhomirov@378: tikhomirov@498: private final List changes = new LinkedList(); tikhomirov@498: private boolean ignoreMissingKeys = false; tikhomirov@483: tikhomirov@483: private HgUpdateConfigCommand(SessionContext sessionContext, File configurationFile) { tikhomirov@483: sessionCtx = sessionContext; tikhomirov@378: configFile = configurationFile; tikhomirov@378: } tikhomirov@378: tikhomirov@378: public static HgUpdateConfigCommand forRepository(HgRepository hgRepo) { tikhomirov@493: return new HgUpdateConfigCommand(hgRepo.getSessionContext(), HgInternals.getImplementationRepo(hgRepo).getFileFromRepoDir("hgrc")); tikhomirov@378: } tikhomirov@378: tikhomirov@382: public static HgUpdateConfigCommand forUser(SessionContext ctx) { tikhomirov@483: return new HgUpdateConfigCommand(ctx, Internals.getUserConfigurationFileToWrite(ctx)); tikhomirov@378: } tikhomirov@378: tikhomirov@382: public static HgUpdateConfigCommand forInstallation(SessionContext ctx) { tikhomirov@483: return new HgUpdateConfigCommand(ctx, Internals.getInstallationConfigurationFileToWrite(ctx)); tikhomirov@378: } tikhomirov@498: tikhomirov@378: /** tikhomirov@498: * Remove an entry altogether. If no entry with the key found, {@link #execute()} fails. tikhomirov@498: * tikhomirov@498: * @param section identifies section to alter, not null or otherwise ill-formed name tikhomirov@498: * @param key identifies entry within section, not null or otherwise ill-formed name tikhomirov@378: * @return this for convenience tikhomirov@498: * @throws IllegalArgumentException if arguments are null or empty tikhomirov@378: */ tikhomirov@498: public HgUpdateConfigCommand remove(String section, String key) throws IllegalArgumentException { tikhomirov@498: checkSection(section); tikhomirov@498: checkKey(key); tikhomirov@498: changes.add(Operation.deleteEntry(section, key)); tikhomirov@378: return this; tikhomirov@378: } tikhomirov@378: tikhomirov@378: /** tikhomirov@498: * Delete single attribute in a multi-valued property. If specified value not found among values of tikhomirov@498: * the identified entry, {@link #execute()} fails. tikhomirov@498: * tikhomirov@498: * @param section identifies section to alter, not null or otherwise ill-formed name tikhomirov@498: * @param key identifies entry within section, not null or otherwise ill-formed name tikhomirov@498: * @param value one of the values to remove, not null or an empty value tikhomirov@378: * @return this for convenience tikhomirov@498: * @throws IllegalArgumentException if arguments are null or empty tikhomirov@378: */ tikhomirov@498: public HgUpdateConfigCommand remove(String section, String key, String value) throws IllegalArgumentException { tikhomirov@498: checkSection(section); tikhomirov@498: checkKey(key); tikhomirov@498: changes.add(Operation.deleteValue(section, key, value)); tikhomirov@378: return this; tikhomirov@378: } tikhomirov@378: tikhomirov@378: /** tikhomirov@498: * Set single-valued property or update multi-valued with a single value tikhomirov@498: * tikhomirov@498: * @param section identifies section to alter, not null or otherwise ill-formed name tikhomirov@498: * @param key identifies entry within section, not null or otherwise ill-formed name tikhomirov@498: * @param value new value, may be null tikhomirov@378: * @return this for convenience tikhomirov@498: * @throws IllegalArgumentException if arguments are null or empty tikhomirov@378: */ tikhomirov@498: public HgUpdateConfigCommand put(String section, String key, String value) throws IllegalArgumentException { tikhomirov@498: checkSection(section); tikhomirov@498: checkKey(key); tikhomirov@498: changes.add(Operation.setValue(section, key, value)); tikhomirov@498: return this; tikhomirov@378: } tikhomirov@378: tikhomirov@427: /** tikhomirov@498: * Add value to a multi-valued entry. If specified entry not found, {@link #execute()} fails. tikhomirov@427: * tikhomirov@498: * @param section identifies section to alter, not null or otherwise ill-formed name tikhomirov@498: * @param key identifies entry within section, not null or otherwise ill-formed name tikhomirov@498: * @param value new value to add, not null or an empty value tikhomirov@498: * @return this for convenience tikhomirov@498: * @throws IllegalArgumentException if arguments are null or empty tikhomirov@498: */ tikhomirov@498: public HgUpdateConfigCommand add(String section, String key, String value) throws IllegalArgumentException { tikhomirov@498: checkSection(section); tikhomirov@498: checkKey(key); tikhomirov@498: changes.add(Operation.addValue(section, key, value)); tikhomirov@498: return this; tikhomirov@498: } tikhomirov@498: tikhomirov@498: /** tikhomirov@498: * Tells whether {@link #execute()} shall fail with exception if keys selected for modification were not found. tikhomirov@498: * If true, missing keys would be silently ignored. tikhomirov@498: * When false(default), exception would be raised. tikhomirov@498: * tikhomirov@498: * @param ignoreMissing pass true to ignore any incorrect keys tikhomirov@498: * @return this for convenience tikhomirov@498: */ tikhomirov@498: public HgUpdateConfigCommand ignoreMissing(boolean ignoreMissing) { tikhomirov@498: ignoreMissingKeys = ignoreMissing; tikhomirov@498: return this; tikhomirov@498: } tikhomirov@498: tikhomirov@498: /** tikhomirov@498: * Perform configuration file update. tikhomirov@498: * tikhomirov@498: * @throws HgMissingConfigElementException if attempt to alter an entry failed to find one, and missing keys are not ignored tikhomirov@498: * @throws HgIOException when configuration file read/write attemt has failed tikhomirov@427: * @throws HgException subclass thereof to indicate specific issue with the command arguments or repository state tikhomirov@427: */ tikhomirov@498: public void execute() throws HgMissingConfigElementException, HgIOException, HgException { tikhomirov@378: try { tikhomirov@498: ConfigFile cfgRead = new ConfigFile(sessionCtx); tikhomirov@498: cfgRead.addLocation(configFile); tikhomirov@498: ConfigFileParser cfgWrite = new ConfigFileParser(); tikhomirov@498: FileInputStream fis = new FileInputStream(configFile); tikhomirov@498: cfgWrite.parse(fis); tikhomirov@498: fis.close(); tikhomirov@498: for (Operation op : changes) { tikhomirov@498: if (!ignoreMissingKeys && !cfgRead.hasSection(op.section)) { tikhomirov@498: throw new HgMissingConfigElementException("Bad section name", op.section, op.key); tikhomirov@498: } tikhomirov@498: Map sect = cfgRead.getSection(op.section); tikhomirov@498: if (!ignoreMissingKeys && !sect.containsKey(op.key)) { tikhomirov@498: throw new HgMissingConfigElementException("Bad key name", op.section, op.key); tikhomirov@498: } tikhomirov@498: String oldValue = sect.get(op.key); tikhomirov@498: if (oldValue == null) { tikhomirov@498: oldValue = ""; tikhomirov@498: } tikhomirov@498: switch (op.kind) { tikhomirov@498: case AddValue: { tikhomirov@498: String separator = ", "; // XXX shall parse and find out separator kind in use tikhomirov@498: String newValue = oldValue + separator + op.value; tikhomirov@498: if (sect.containsKey(op.key)) { tikhomirov@498: cfgWrite.change(op.section, op.key, newValue); tikhomirov@498: } else { tikhomirov@498: cfgWrite.add(op.section, op.key, newValue); tikhomirov@378: } tikhomirov@498: break; tikhomirov@498: } tikhomirov@498: case DelValue: { tikhomirov@498: if (!ignoreMissingKeys && (oldValue.length() == 0 || !oldValue.contains(op.value))) { tikhomirov@498: throw new HgMissingConfigElementException(String.format("Bad value '%s' to delete from '%s'", op.value, oldValue), op.section, op.key); tikhomirov@498: } tikhomirov@498: int start = oldValue.indexOf(op.value); tikhomirov@498: if (start == -1) { tikhomirov@498: // nothing to change tikhomirov@498: break; tikhomirov@498: } tikhomirov@498: int commaPos = -1; tikhomirov@498: for (int i = start-1; i >=0; i--) { tikhomirov@498: if (oldValue.charAt(i) == ',') { tikhomirov@498: commaPos = i; tikhomirov@498: break; tikhomirov@498: } tikhomirov@498: } tikhomirov@498: for (int i = start + op.value.length(); commaPos == -1 && i < oldValue.length(); i++) { tikhomirov@498: if (oldValue.charAt(i) == ',') { tikhomirov@498: commaPos = i; tikhomirov@498: break; tikhomirov@498: } tikhomirov@498: } tikhomirov@498: String newValue; tikhomirov@498: if (commaPos >= 0) { tikhomirov@498: if (commaPos < start) { tikhomirov@498: // from preceding comma up to end of value tikhomirov@498: newValue = oldValue.substring(0, commaPos) + oldValue.substring(start + op.value.length()); tikhomirov@498: } else { tikhomirov@498: // from value start up to and including subsequent comma tikhomirov@498: newValue = oldValue.substring(0, start) + oldValue.substring(commaPos+1); tikhomirov@498: } tikhomirov@498: } else { tikhomirov@498: // found no separator, just remove the value tikhomirov@498: // extra whitespaces (if space, not a comma is a separator) won't hurt tikhomirov@498: newValue = oldValue.substring(0, start) + oldValue.substring(start + op.value.length()); tikhomirov@498: } tikhomirov@498: cfgWrite.change(op.section, op.key, newValue); tikhomirov@498: break; tikhomirov@498: } tikhomirov@498: case SetValue: { tikhomirov@498: if (sect.containsKey(op.key)) { tikhomirov@498: cfgWrite.change(op.section, op.key, op.value); tikhomirov@498: } else { tikhomirov@498: cfgWrite.add(op.section, op.key, op.value); tikhomirov@498: } tikhomirov@498: break; tikhomirov@498: } tikhomirov@498: case DelEntry: { tikhomirov@498: cfgWrite.delete(op.section, op.key); tikhomirov@498: break; tikhomirov@498: } tikhomirov@498: default: throw new HgInvalidStateException(String.format("Unknown change %s", op.kind)); tikhomirov@378: } tikhomirov@378: } tikhomirov@498: FileOutputStream fos = new FileOutputStream(configFile); tikhomirov@498: cfgWrite.update(fos); tikhomirov@498: fos.close(); tikhomirov@378: } catch (IOException ex) { tikhomirov@427: String m = String.format("Failed to update configuration file %s", configFile); tikhomirov@427: throw new HgBadArgumentException(m, ex); // TODO [post-1.0] better exception, it's not bad argument case tikhomirov@378: } tikhomirov@378: } tikhomirov@498: tikhomirov@498: private static void checkSection(String section) throws IllegalArgumentException { tikhomirov@498: if (section == null || section.trim().length() == 0) { tikhomirov@498: throw new IllegalArgumentException(String.format("Section name can't be empty: %s", section)); tikhomirov@498: } tikhomirov@498: } tikhomirov@498: tikhomirov@498: private static void checkKey(String key) throws IllegalArgumentException { tikhomirov@498: if (key == null || key.trim().length() == 0) { tikhomirov@498: throw new IllegalArgumentException(String.format("Entry key can't be empty: %s", key)); tikhomirov@498: } tikhomirov@498: } tikhomirov@378: tikhomirov@378: tikhomirov@498: private static class Operation { tikhomirov@498: private enum OpKind { AddValue, SetValue, DelValue, DelEntry }; tikhomirov@498: tikhomirov@498: public final OpKind kind; tikhomirov@498: public final String section; tikhomirov@498: public final String key; tikhomirov@498: public final String value; tikhomirov@498: tikhomirov@498: private Operation(OpKind t, String s, String k, String v) { tikhomirov@498: kind = t; tikhomirov@498: section = s; tikhomirov@498: key = k; tikhomirov@498: value = v; tikhomirov@498: } tikhomirov@498: tikhomirov@498: public static Operation deleteEntry(String section, String key) throws IllegalArgumentException { tikhomirov@498: return new Operation(OpKind.DelEntry, section, key, null); tikhomirov@498: } tikhomirov@498: public static Operation deleteValue(String section, String key, String value) throws IllegalArgumentException { tikhomirov@498: if (value == null || value.trim().length() == 0) { tikhomirov@498: throw new IllegalArgumentException(String.format("Can't remove empty value '%s'", value)); tikhomirov@498: } tikhomirov@498: return new Operation(OpKind.DelValue, section, key, value); tikhomirov@498: } tikhomirov@498: public static Operation addValue(String section, String key, String value) throws IllegalArgumentException { tikhomirov@498: if (value == null || value.trim().length() == 0) { tikhomirov@498: throw new IllegalArgumentException(String.format("Can't add empty value '%s'", value)); tikhomirov@498: } tikhomirov@498: return new Operation(OpKind.AddValue, section, key, value); tikhomirov@498: } tikhomirov@498: public static Operation setValue(String section, String key, String value) throws IllegalArgumentException { tikhomirov@498: return new Operation(OpKind.SetValue, section, key, value); tikhomirov@498: } tikhomirov@378: } tikhomirov@378: }