/*******************************************************************************
* ALMA - Atacama Large Millimeter Array
* Copyright (c) ESO - European Southern Observatory, 2011
* (in the framework of the ALMA collaboration).
* All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*******************************************************************************/
package alma.acs.tmcdb.logic;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import alma.acs.tmcdb.ContStartOptType;
import alma.acs.tmcdb.Container;
import alma.acs.tmcdb.ContainerStartupOption;
/**
* Encapsulates the translation of Container parameters between
* <ol>
* <li> The one-string version used by the CDB and container daemon
* (where different levels of options are expressed via wrapper options like --passthroughProcessStart)
* <li> and the more atomic storage in the TMCDB as a set of {@link ContainerStartupOption} objects.
* </ol>
* In the future this class may also be used to provide lists of available options to the TMCDB Explorer.
* <p>
* @TODO Perhaps move this class to module acsstartup if we refactor this class's API to not use TMCDB pojos.
*
* @author hsommer
*/
public class ContainerStartupOptionHelper {
private final Logger logger;
/**
* Wrapper for options targeted at the container application (passed to the container's main method).
*/
public static final String CONT_ARG_WRAPPER_OPTION = "--passthrough";
/**
* Wrapper for options targeted at the language-specific container start script.
*/
public static final String EXEC_ARG_LANG_WRAPPER_OPTION = "--passthroughProcessStart";
/**
* Default name used when storing a string of concatenated options,
* typically from importing options that were specified before the TMCDB table ContainerStartupOption
* allowed for finer granularity and meaningful option names.
*/
public static final String OPTION_NAME_LEGACY_CONCATENATED = "LegacyOptionsConcat";
/**
* Ctor that takes a logger.
*/
public ContainerStartupOptionHelper(Logger logger) {
this.logger = logger;
}
/**
* Converts a string of options to one or more ContainerStartupOption objects.
* <p>
* Parses out the wrapper options and creates specialized ContainerStartupOption instances for their contents.
* @TODO: Parse out known options such as '-maxHeapSize' into separate ContainerStartupOptions,
* so that we can use nicer OptionNames than OPTION_NAME_LEGACY_CONCATENATED.
*
* @param container The container whose flags we convert (will be set on the created <code>ContainerStartupOption</code>.
* @param flags The flags string, as it comes from the CDB (Container.DeployInfo.Flags) or from the WDAL interface. May be null.
* @return
*/
public Collection<ContainerStartupOption> convertFlagsString(Container container, String flags) {
List<ContainerStartupOption> ret = new ArrayList<ContainerStartupOption>();
if (flags != null) {
flags = flags.trim();
if (flags.length() > 0) {
WrapperOptionParser wop = new WrapperOptionParser();
String verbatimOptions = "";
try {
wop.parseAll(flags);
if (!wop.getWrappedOptionsContainerExecutable().isEmpty()) {
ContainerStartupOption containerStartupOption = new ContainerStartupOption();
ret.add(containerStartupOption);
containerStartupOption.setContainer(container);
containerStartupOption.setOptionType(ContStartOptType.EXEC_ARG_LANG);
containerStartupOption.setOptionName(OPTION_NAME_LEGACY_CONCATENATED);
containerStartupOption.setOptionValue(wop.getWrappedOptionsContainerExecutable());
}
if (!wop.getWrappedOptionsContainerArgs().isEmpty()) {
ContainerStartupOption containerStartupOption = new ContainerStartupOption();
ret.add(containerStartupOption);
containerStartupOption.setContainer(container);
containerStartupOption.setOptionType(ContStartOptType.CONT_ARG);
containerStartupOption.setOptionName(OPTION_NAME_LEGACY_CONCATENATED);
containerStartupOption.setOptionValue(wop.getWrappedOptionsContainerArgs());
}
verbatimOptions = wop.getRemainingOptions();
} catch (IllegalArgumentException ex) {
logger.log(Level.WARNING, "Failed to parse container options '" + flags + "'. Will leave them as verbatim string.");
verbatimOptions = flags;
}
if (!verbatimOptions.isEmpty()) {
ContainerStartupOption containerStartupOption = new ContainerStartupOption();
ret.add(containerStartupOption);
containerStartupOption.setContainer(container);
containerStartupOption.setOptionType(ContStartOptType.EXEC_ARG);
containerStartupOption.setOptionName(OPTION_NAME_LEGACY_CONCATENATED);
containerStartupOption.setOptionValue(verbatimOptions);
}
}
}
return ret;
}
/**
* Parses out the options wrapped by <code>--passthroughProcessStart</code>
* and <code>--passthrough</code> from an option string, as well as the remaining unwrapped options.
*/
static class WrapperOptionParser {
private String wrappedOptionsContainerExecutable;
private String wrappedOptionsContainerArgs;
private String remainingOptions;
WrapperOptionParser() {
wrappedOptionsContainerExecutable = "";
wrappedOptionsContainerArgs = "";
remainingOptions = "";
}
/**
* Parses all wrapper options and makes results available through subsequent calls to
* {@link #getWrappedOptionsContainerExecutable()}, {@link #getWrappedOptionsContainerArgs()},
* and {@link #getRemainingOptions()}.
* @param flags
* @throws IllegalArgumentException if wrapper options do not wrap the underlying options in a pair of single or double quotes.
*/
void parseAll(String flags) {
// first parse out "passthroughProcessStart" and remove that wrapper from the option string,
// because parsing for "passthrough" first would get confused by "passthroughProcessStart".
wrappedOptionsContainerExecutable = parse(flags, EXEC_ARG_LANG_WRAPPER_OPTION);
wrappedOptionsContainerArgs = parse(remainingOptions, CONT_ARG_WRAPPER_OPTION);
}
/**
* Tries to extract options even from multiple occurrences of wrapper options,
* although the container daemon does not (verify this!) support this,
* so that it should not happen in practice.
* <p>
* Returns the wrapped options, and stores the remaining option string in
* {@link #remainingOptions}.
*
* @param flags The option string that may contain wrapper options.
* @param wrapperOptionName Should be {@link #EXEC_ARG_LANG_WRAPPER_OPTION} or {@link #CONT_ARG_WRAPPER_OPTION}.
* @return the wrapped options, or empty string.
* @throws IllegalArgumentException if wrapper options do not wrap the underlying options in a pair of single or double quotes.
*/
private String parse(String flags, String wrapperOptionName) {
String wrappedOptions = "";
remainingOptions = "";
if (flags != null) {
int indexRemainingOptionsBegin = 0;
int indexWrapperOption = flags.indexOf(wrapperOptionName);
while (indexWrapperOption >= 0) {
char wrapperQuoteChar = '"';
// find opening quotes, and assert that only space and '=' lie between the wrapper option and the quotes
int indexOptionBeginQuote = -1;
for (int i = indexWrapperOption + wrapperOptionName.length(); i < flags.length(); i++) {
char c = flags.charAt(i);
if (c == '\"' || c == '\'') {
wrapperQuoteChar = c;
indexOptionBeginQuote = i;
break;
}
if (c != ' ' && c != '=') {
// bad, will lead to IllegalArgumentException (indexOptionBeginQuote == -1)
break;
}
}
if (indexOptionBeginQuote < 0) {
throw new IllegalArgumentException("Wrapper option at pos." + indexWrapperOption + " must be followed by '=' and single or double quotes.");
}
int indexOptionEndQuote = flags.indexOf(wrapperQuoteChar, indexOptionBeginQuote + 1);
if (indexOptionEndQuote < 0) {
throw new IllegalArgumentException("Wrapper option at pos." + indexWrapperOption +
" must be followed by '=' and a pair of '" + wrapperQuoteChar + "' chars around the wrapped options.");
}
String option = flags.substring(indexOptionBeginQuote + 1, indexOptionEndQuote).trim();
wrappedOptions += option + " ";
remainingOptions += flags.substring(indexRemainingOptionsBegin, indexWrapperOption);
indexRemainingOptionsBegin = indexOptionEndQuote + 1;
indexWrapperOption = flags.indexOf(wrapperOptionName, indexOptionEndQuote);
}
if (indexRemainingOptionsBegin < flags.length()) {
remainingOptions += flags.substring(indexRemainingOptionsBegin).trim();
}
remainingOptions = remainingOptions.trim();
}
return wrappedOptions.trim();
}
/**
* Returns the parsed options found inside <code>--passthroughProcessStart</code> wrapper option.
* Call {@link #parseAll(String)} first.
*/
String getWrappedOptionsContainerExecutable() {
return wrappedOptionsContainerExecutable;
}
/**
* Returns the parsed options found inside <code>--passthrough</code> wrapper options.
* Call {@link #parseAll(String)} first.
*/
String getWrappedOptionsContainerArgs() {
return wrappedOptionsContainerArgs;
}
/**
* Returns the options that were not wrapped.
* Call {@link #parseAll(String)} first.
*/
String getRemainingOptions() {
return remainingOptions;
}
}
/**
* Converts a list of ContainerStartupOption to a flat option string
* that can be passed to the container daemon or used to satisfy DAL calls.
* @param options
* @return Options in one string, wrapped as needed. Possibly empty string, never null.
* @throws IllegalArgumentException if an option references a different container than the other options
* (all refs null is OK though)
*/
public String convertContainerStartupOptions(Collection<ContainerStartupOption> options) {
if (options == null || options.isEmpty()) {
logger.finer("convertContainerStartupOptions called without options.");
return "";
}
String execArgs = "";
String execArgsLang = "";
String contArgs = "";
Container commonContainer = null;
for (ContainerStartupOption option : options) {
// validate container ref
if (commonContainer == null) {
commonContainer = option.getContainer();
}
else if (option.getContainer() != commonContainer) {
throw new IllegalArgumentException();
}
// gather by option type
if (option.getOptionType().equals(ContStartOptType.ENV_VAR)) {
logger.warning("Ignoring option of type " + ContStartOptType.ENV_VAR);
}
else if (option.getOptionType().equals(ContStartOptType.EXEC_ARG)) {
execArgs += " " + option.getOptionValue();
}
else if (option.getOptionType().equals(ContStartOptType.EXEC_ARG_LANG)) {
execArgsLang += " " + option.getOptionValue();
}
else if (option.getOptionType().equals(ContStartOptType.CONT_ARG)) {
contArgs += " " + option.getOptionValue();
}
}
// wrap and concatenate the options
String ret = execArgs.trim();
if (execArgsLang.length() > 0) {
ret += " " + EXEC_ARG_LANG_WRAPPER_OPTION + "=\"" + execArgsLang.trim() + "\"";
}
if (contArgs.length() > 0) {
ret += " " + CONT_ARG_WRAPPER_OPTION + "=\"" + contArgs.trim() + "\"";
}
if (commonContainer != null && logger.isLoggable(Level.FINER)) {
logger.finer(options.size() + " options for container " + commonContainer + " flattened: " + ret.trim());
}
return ret.trim();
}
}