/* * JBoss, Home of Professional Open Source. * Copyright 2016, Red Hat, Inc., and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This 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 software 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 software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.as.cli.handlers.module; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.xml.stream.XMLOutputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamWriter; import org.jboss.as.cli.CommandContext; import org.jboss.as.cli.CommandFormatException; import org.jboss.as.cli.CommandLineCompleter; import org.jboss.as.cli.CommandLineException; import org.jboss.as.cli.Util; import org.jboss.as.cli.handlers.ModuleNameTabCompleter; import org.jboss.as.cli.handlers.CommandHandlerWithHelp; import org.jboss.as.cli.handlers.DefaultFilenameTabCompleter; import org.jboss.as.cli.handlers.FilenameTabCompleter; import org.jboss.as.cli.handlers.WindowsFilenameTabCompleter; import org.jboss.as.cli.impl.ArgumentWithListValue; import org.jboss.as.cli.impl.ArgumentWithValue; import org.jboss.as.cli.impl.DefaultCompleter; import org.jboss.as.cli.impl.DefaultCompleter.CandidatesProvider; import org.jboss.as.cli.impl.FileSystemPathArgument; import org.jboss.as.cli.operation.ParsedCommandLine; import org.jboss.as.cli.parsing.ExpressionBaseState; import org.jboss.as.cli.parsing.ParsingState; import org.jboss.as.cli.parsing.WordCharacterHandler; import org.jboss.staxmapper.FormattingXMLStreamWriter; import org.jboss.staxmapper.XMLExtendedStreamWriter; import org.wildfly.security.manager.WildFlySecurityManager; /** * * @author Alexey Loubyansky */ public class ASModuleHandler extends CommandHandlerWithHelp { private class AddModuleArgument extends ArgumentWithValue { private AddModuleArgument(String fullName) { super(ASModuleHandler.this, fullName); } private AddModuleArgument(String fullName, CommandLineCompleter completer) { super(ASModuleHandler.this, completer, fullName); } @Override public boolean canAppearNext(CommandContext ctx) throws CommandFormatException { final String actionValue = action.getValue(ctx.getParsedCommandLine()); return ACTION_ADD.equals(actionValue) && name.isPresent(ctx.getParsedCommandLine()) && super.canAppearNext(ctx); } } private class AddModuleListArgument extends ArgumentWithListValue { private AddModuleListArgument(String fullname) { super(ASModuleHandler.this, fullname); } private AddModuleListArgument(String fullname, CommandLineCompleter completer) { super(ASModuleHandler.this, completer, fullname); } @Override public boolean canAppearNext(CommandContext ctx) throws CommandFormatException { final String actionValue = action.getValue(ctx.getParsedCommandLine()); return ACTION_ADD.equals(actionValue) && name.isPresent(ctx.getParsedCommandLine()) && super.canAppearNext(ctx); } } private static final String JBOSS_HOME = "JBOSS_HOME"; private static final String JBOSS_HOME_PROPERTY = "jboss.home.dir"; private static final String PATH_SEPARATOR = File.pathSeparator; private static final String MODULE_SEPARATOR = ","; private static final String ACTION_ADD = "add"; private static final String ACTION_REMOVE = "remove"; private final ArgumentWithValue action = new ArgumentWithValue(this, new DefaultCompleter(new CandidatesProvider(){ @Override public Collection<String> getAllCandidates(CommandContext ctx) { return Arrays.asList(new String[]{ACTION_ADD, ACTION_REMOVE}); }}), 0, "--action"); private final ArgumentWithValue name; private final ArgumentWithValue mainClass; private final ArgumentWithValue resources; private final ArgumentWithValue absoluteResources; private final ArgumentWithListValue dependencies; private final ArgumentWithListValue exportDependencies; private final ArgumentWithListValue props; private final ArgumentWithValue moduleArg; private final ArgumentWithValue slot; private final ArgumentWithValue resourceDelimiter; private final ArgumentWithValue moduleRootDir; private File modulesDir; public ASModuleHandler(CommandContext ctx) { super("module", false); final FilenameTabCompleter pathCompleter = FilenameTabCompleter.newCompleter(ctx); moduleRootDir = new FileSystemPathArgument(this, pathCompleter, "--module-root-dir"); name = new ArgumentWithValue(this, new CommandLineCompleter() { @Override public int complete(CommandContext ctx, String buffer, int cursor, List<String> candidates) { try { String currentAction = action.getValue(ctx.getParsedCommandLine()); // suggest only modules from user's repository, not system modules final ModuleNameTabCompleter moduleNameCompleter = ModuleNameTabCompleter.completer(getModulesDir(ctx)) .excludeNonModuleFolders(ACTION_REMOVE.equals(currentAction)) .includeSystemModules(ACTION_ADD.equals(currentAction)) .build(); candidates.addAll(moduleNameCompleter.complete(buffer)); return 0; } catch (CommandLineException e) { return -1; } } }, "--name") { @Override protected ParsingState initParsingState() { final ExpressionBaseState state = new ExpressionBaseState("EXPR", true, false); if(Util.isWindows()) { // to not require escaping FS name separator state.setDefaultHandler(WordCharacterHandler.IGNORE_LB_ESCAPE_OFF); } else { state.setDefaultHandler(WordCharacterHandler.IGNORE_LB_ESCAPE_ON); } return state; } }; name.addRequiredPreceding(action); mainClass = new AddModuleArgument("--main-class"); resources = new AddModuleArgument("--resources", new CommandLineCompleter(){ @Override public int complete(CommandContext ctx, String buffer, int cursor, List<String> candidates) { final int lastSeparator = buffer.lastIndexOf(PATH_SEPARATOR); if(lastSeparator >= 0) { return lastSeparator + 1 + pathCompleter.complete(ctx, buffer.substring(lastSeparator + 1), cursor, candidates); } return pathCompleter.complete(ctx, buffer, cursor, candidates); }}) { @Override public String getValue(ParsedCommandLine args) { String value = super.getValue(args); if(value != null) { if(value.length() >= 0 && value.charAt(0) == '"' && value.charAt(value.length() - 1) == '"') { value = value.substring(1, value.length() - 1); } value = pathCompleter.translatePath(value); } return value; } @Override protected ParsingState initParsingState() { final ExpressionBaseState state = new ExpressionBaseState("EXPR", true, false); if(Util.isWindows()) { // to not require escaping FS name separator state.setDefaultHandler(WordCharacterHandler.IGNORE_LB_ESCAPE_OFF); } else { state.setDefaultHandler(WordCharacterHandler.IGNORE_LB_ESCAPE_ON); } return state; } }; absoluteResources = new AddModuleArgument("--absolute-resources", new CommandLineCompleter(){ @Override public int complete(CommandContext ctx, String buffer, int cursor, List<String> candidates) { final int lastSeparator = buffer.lastIndexOf(PATH_SEPARATOR); if(lastSeparator >= 0) { return lastSeparator + 1 + pathCompleter.complete(ctx, buffer.substring(lastSeparator + 1), cursor, candidates); } return pathCompleter.complete(ctx, buffer, cursor, candidates); }}) { @Override public String getValue(ParsedCommandLine args) { String value = super.getValue(args); if(value != null) { if(value.length() >= 0 && value.charAt(0) == '"' && value.charAt(value.length() - 1) == '"') { value = value.substring(1, value.length() - 1); } value = pathCompleter.translatePath(value); } return value; } @Override protected ParsingState initParsingState() { final ExpressionBaseState state = new ExpressionBaseState("EXPR", true, false); if(Util.isWindows()) { // to not require escaping FS name separator state.setDefaultHandler(WordCharacterHandler.IGNORE_LB_ESCAPE_OFF); } else { state.setDefaultHandler(WordCharacterHandler.IGNORE_LB_ESCAPE_ON); } return state; } }; resourceDelimiter = new AddModuleArgument("--resource-delimiter"); dependencies = new AddModuleListArgument("--dependencies", new CommandLineCompleter(){ @Override public int complete(CommandContext ctx, String buffer, int cursor, List<String> candidates) { return doCompleteDependencies(ctx, buffer, cursor, candidates); } }); exportDependencies = new AddModuleListArgument("--export-dependencies", new CommandLineCompleter() { @Override public int complete(CommandContext ctx, String buffer, int cursor, List<String> candidates) { return doCompleteDependencies(ctx, buffer, cursor, candidates); } }); props = new AddModuleListArgument("--properties"); moduleArg = new FileSystemPathArgument(this, pathCompleter, "--module-xml") { @Override public boolean canAppearNext(CommandContext ctx) throws CommandFormatException { final String actionValue = action.getValue(ctx.getParsedCommandLine()); return ACTION_ADD.equals(actionValue) && name.isPresent(ctx.getParsedCommandLine()) && super.canAppearNext(ctx); } }; slot = new ArgumentWithValue(this, new DefaultCompleter(new CandidatesProvider() { @Override public Collection<String> getAllCandidates(CommandContext ctx) { final String moduleName = name.getValue(ctx.getParsedCommandLine()); if(moduleName == null) { return java.util.Collections.emptyList(); } final File moduleDir; try { moduleDir = new File(getModulesDir(ctx), moduleName.replace('.', File.separatorChar)); } catch (CommandLineException e) { return java.util.Collections.emptyList(); } if(!moduleDir.exists()) { return java.util.Collections.emptyList(); } return Arrays.asList(moduleDir.list()); }}) , "--slot"); moduleArg.addCantAppearAfter(mainClass); moduleArg.addCantAppearAfter(dependencies); moduleArg.addCantAppearAfter(exportDependencies); moduleArg.addCantAppearAfter(props); mainClass.addCantAppearAfter(moduleArg); dependencies.addCantAppearAfter(moduleArg); exportDependencies.addCantAppearAfter(moduleArg); props.addCantAppearAfter(moduleArg); } private int doCompleteDependencies(CommandContext ctx, String buffer, int cursor, List<String> candidates) { final int lastSeparator = buffer.lastIndexOf(MODULE_SEPARATOR); try { // any module (including system) can be a dependency final ModuleNameTabCompleter moduleNameCompleter = ModuleNameTabCompleter.completer(getModulesDir(ctx)) .excludeNonModuleFolders(true) .includeSystemModules(true) .build(); if (lastSeparator >= 0) { candidates.addAll(moduleNameCompleter.complete(buffer.substring(lastSeparator + 1))); return lastSeparator + 1; } else { candidates.addAll(moduleNameCompleter.complete(buffer)); return 0; } } catch (CommandLineException e) { return -1; } } @Override public boolean isAvailable(CommandContext ctx) { return !ctx.isDomainMode(); } @Override protected void doHandle(CommandContext ctx) throws CommandLineException { final ParsedCommandLine parsedCmd = ctx.getParsedCommandLine(); final String actionValue = action.getValue(parsedCmd); if(actionValue == null) { throw new CommandFormatException("Action argument is missing: " + ACTION_ADD + " or " + ACTION_REMOVE); } if(ACTION_ADD.equals(actionValue)) { addModule(ctx, parsedCmd); } else if(ACTION_REMOVE.equals(actionValue)) { removeModule(parsedCmd, ctx); } else { throw new CommandFormatException("Unexpected action '" + actionValue + "', expected values: " + ACTION_ADD + ", " + ACTION_REMOVE); } } protected void addModule(CommandContext ctx, final ParsedCommandLine parsedCmd) throws CommandLineException { final String moduleName = name.getValue(parsedCmd, true); // resources required only if we are generating module.xml if(!moduleArg.isPresent(parsedCmd) && !(resources.isPresent(parsedCmd) || absoluteResources.isPresent(parsedCmd))) { throw new CommandFormatException("You must specify at least one resource: use --resources or --absolute-resources parameter"); } final String resourcePaths = resources.getValue(parsedCmd); final String absoluteResourcePaths = absoluteResources.getValue(parsedCmd); String pathDelimiter = PATH_SEPARATOR; if (resourceDelimiter.isPresent(parsedCmd)) { pathDelimiter = resourceDelimiter.getValue(parsedCmd); } final FilenameTabCompleter pathCompleter = Util.isWindows() ? new WindowsFilenameTabCompleter(ctx) : new DefaultFilenameTabCompleter(ctx); final String[] resourceArr = (resourcePaths == null) ? new String[0] : resourcePaths.split(pathDelimiter); File[] resourceFiles = new File[resourceArr.length]; for(int i = 0; i < resourceArr.length; ++i) { final File f = new File(pathCompleter.translatePath(resourceArr[i])); if(!f.exists()) { throw new CommandLineException("Failed to locate " + f.getAbsolutePath()); } resourceFiles[i] = f; } final String[] absoluteResourceArr = (absoluteResourcePaths == null) ? new String[0] : absoluteResourcePaths.split(pathDelimiter); File[] absoluteResourceFiles = new File[absoluteResourceArr.length]; for(int i = 0; i < absoluteResourceArr.length; ++i) { final File f = new File(pathCompleter.translatePath(absoluteResourceArr[i])); if(!f.exists()) { throw new CommandLineException("Failed to locate " + f.getAbsolutePath()); } absoluteResourceFiles[i] = f; } final File moduleDir = getModulePath(getModulesDir(ctx), moduleName, slot.getValue(parsedCmd)); if(moduleDir.exists()) { throw new CommandLineException("Module " + moduleName + " already exists at " + moduleDir.getAbsolutePath()); } if(!moduleDir.mkdirs()) { throw new CommandLineException("Failed to create directory " + moduleDir.getAbsolutePath()); } final ModuleConfigImpl config; final String moduleXml = moduleArg.getValue(parsedCmd); if(moduleXml != null) { config = null; final File source = new File(moduleXml); if(!source.exists()) { throw new CommandLineException("Failed to locate the file on the filesystem: " + source.getAbsolutePath()); } copy(source, new File(moduleDir, "module.xml")); } else { config = new ModuleConfigImpl(moduleName); } for(File f : resourceFiles) { copy(f, new File(moduleDir, f.getName())); if(config != null) { config.addResource(new ResourceRoot(f.getName())); } } for(File f : absoluteResourceFiles) { if(config != null) { try { config.addResource(new ResourceRoot(f.getCanonicalPath())); } catch (IOException ioe) { throw new CommandLineException("Failed to read path: " + f.getAbsolutePath(), ioe); } } } if (config != null) { Set<String> modules = new HashSet<>(); final String dependenciesStr = dependencies.getValue(parsedCmd); if(dependenciesStr != null) { final String[] depsArr = dependenciesStr.split(",+"); for(String dep : depsArr) { // TODO validate dependencies String depName = dep.trim(); config.addDependency(new ModuleDependency(depName)); modules.add(depName); } } final String exportDependenciesStr = exportDependencies.getValue(parsedCmd); if (exportDependenciesStr != null) { final String[] depsArr = exportDependenciesStr.split(",+"); for (String dep : depsArr) { // TODO validate dependencies String depName = dep.trim(); if (modules.contains(depName)) { deleteRecursively(moduleDir); throw new CommandLineException("Error, duplicated dependency " + depName); } modules.add(depName); config.addDependency(new ModuleDependency(depName, true)); } } final String propsStr = props.getValue(parsedCmd); if(propsStr != null) { final String[] pairs = propsStr.split(","); for (String pair : pairs) { int equals = pair.indexOf('='); if (equals == -1) { throw new CommandFormatException("Property '" + pair + "' in '" + propsStr + "' is missing the equals sign."); } final String propName = pair.substring(0, equals); if (propName.isEmpty()) { throw new CommandFormatException("Property name is missing for '" + pair + "' in '" + propsStr + "'"); } config.setProperty(propName, pair.substring(equals + 1)); } } final String slotVal = slot.getValue(parsedCmd); if (slotVal != null) { config.setSlot(slotVal); } final String mainCls = mainClass.getValue(parsedCmd); if(mainCls != null) { config.setMainClass(mainCls); } FileOutputStream fos = null; final File moduleFile = new File(moduleDir, "module.xml"); try { fos = new FileOutputStream(moduleFile); XMLExtendedStreamWriter xmlWriter = create(XMLOutputFactory.newInstance().createXMLStreamWriter(fos, StandardCharsets.UTF_8.name())); config.writeContent(xmlWriter, null); xmlWriter.flush(); } catch (IOException e) { throw new CommandLineException("Failed to create file " + moduleFile.getAbsolutePath(), e); } catch (XMLStreamException e) { throw new CommandLineException("Failed to write to " + moduleFile.getAbsolutePath(), e); } finally { if(fos != null) { try { fos.close(); } catch (IOException e) {} } } } } private void removeModule(ParsedCommandLine parsedCmd, CommandContext ctx) throws CommandLineException { final String moduleName = name.getValue(parsedCmd, true); final File modulesDir = getModulesDir(ctx); File modulePath = getModulePath(modulesDir, moduleName, slot.getValue(parsedCmd)); if(!modulePath.exists()) { throw new CommandLineException("Failed to locate module " + moduleName + " at " + modulePath.getAbsolutePath()); } // delete the whole slot directory deleteRecursively(modulePath); modulePath = modulePath.getParentFile(); while(!modulesDir.equals(modulePath)) { if(modulePath.list().length > 0) { break; } if(!modulePath.delete()) { throw new CommandLineException("Failed to delete " + modulePath.getAbsolutePath()); } modulePath = modulePath.getParentFile(); } } protected void deleteRecursively(final File file) throws CommandLineException { if (file.isDirectory()) { final File[] files = file.listFiles(); if (files != null) { for (File f : files) { deleteRecursively(f); } } } if (!file.delete()) { throw new CommandLineException("Failed to delete " + file.getAbsolutePath()); } } protected File getModulePath(File modulesDir, final String moduleName, String slot) throws CommandLineException { return new File(modulesDir, moduleName.replace('.', File.separatorChar) + File.separatorChar + (slot == null ? "main" : slot)); } protected File getModulesDir(CommandContext ctx) throws CommandLineException { // First check if we have an option File modsDir = null; String moduleRootDirStr = moduleRootDir.getValue(ctx.getParsedCommandLine()); if (moduleRootDirStr != null) { modsDir = new File(moduleRootDirStr); } if (modsDir == null) { if(modulesDir != null) { return modulesDir; } // First check the environment variable String rootDir = WildFlySecurityManager.getEnvPropertyPrivileged(JBOSS_HOME, null); if (rootDir == null) { // Not found, check the system property, this may be set from a client using the CLI API to execute commands rootDir = WildFlySecurityManager.getPropertyPrivileged(JBOSS_HOME_PROPERTY, null); } if (rootDir == null) { throw new CommandLineException(JBOSS_HOME + " environment variable is not set."); } modulesDir = new File(rootDir, "modules"); modsDir = modulesDir; } if (!modsDir.exists()) { throw new CommandLineException("Failed to locate the modules dir on the filesystem: " + modsDir.getAbsolutePath()); } return modsDir; } public static XMLExtendedStreamWriter create(XMLStreamWriter writer) throws CommandLineException { try { return new FormattingXMLStreamWriter(writer); } catch (Exception e) { throw new CommandLineException("Failed to create xml stream writer.", e); } } public static void copy(final File source, final File target) throws CommandLineException { final byte[] buff = new byte[8192]; BufferedInputStream in = null; BufferedOutputStream out = null; int read; try { in = new BufferedInputStream(new FileInputStream(source)); out = new BufferedOutputStream(new FileOutputStream(target)); while ((read = in.read(buff)) != -1) { out.write(buff, 0, read); } out.flush(); } catch (FileNotFoundException e) { throw new CommandLineException("Failed to locate the file on the filesystem copying " + source.getAbsolutePath() + " to " + target.getAbsolutePath(), e); } catch (IOException e) { throw new CommandLineException("Failed to copy " + source.getAbsolutePath() + " to " + target.getAbsolutePath(), e); } finally { try { if(out != null) { out.close(); } } catch(IOException e) {} try { if(in != null) { in.close(); } } catch(IOException e) {} } } }