.classpath | 1 modules/enterprise/server/appserver/src/main/bin-resources/bin/rhq-server.bat | 6 modules/enterprise/server/appserver/src/main/bin-resources/bin/rhq-storage.bat | 2 modules/enterprise/server/server-control/src/main/java/org/rhq/server/control/Commands.java | 1 modules/enterprise/server/server-control/src/main/java/org/rhq/server/control/ControlCommand.java | 19 modules/enterprise/server/server-control/src/main/java/org/rhq/server/control/command/Remove.java | 194 ++++++++++ modules/enterprise/server/server-control/src/main/java/org/rhq/server/control/command/Start.java | 83 +++- modules/enterprise/server/server-control/src/main/java/org/rhq/server/control/command/Status.java | 29 + modules/enterprise/server/server-control/src/main/java/org/rhq/server/control/command/Stop.java | 110 +++-- 9 files changed, 365 insertions(+), 80 deletions(-)
New commits: commit 7bab025bcfae910541b9d559d6cbafc94b2b69a2 Author: Jay Shaughnessy jshaughn@redhat.com Date: Mon Apr 29 18:18:24 2013 -0400
More rhqctl work - update start/stop/status commands for windows compat - fix logdir mkdir issues in rhq-server.bat - add option to ControlCommand.getCommandLine() to not append .sh to the script name - add Remove command (although currently not sure if we want this in rhqctl, so commented out)
diff --git a/.classpath b/.classpath index 8010bae..54ff6de 100644 --- a/.classpath +++ b/.classpath @@ -1,6 +1,7 @@ <?xml version="1.0" encoding="UTF-8"?> <classpath> <classpathentry kind="src" path="modules/common/ant-bundle/src/main/java"/> + <classpathentry kind="src" path="modules/common/cassandra-util/src/main/java"/> <classpathentry kind="src" path="modules/common/cassandra-ccm/cassandra-ccm-maven-plugin/src/main/java"/> <classpathentry kind="src" path="modules/common/cassandra-ccm/cassandra-ccm-core/src/test/java"/> <classpathentry kind="src" path="modules/common/cassandra-ccm/cassandra-ccm-arquillian/src/main/java"/> diff --git a/modules/enterprise/server/appserver/src/main/bin-resources/bin/rhq-server.bat b/modules/enterprise/server/appserver/src/main/bin-resources/bin/rhq-server.bat index 63cc162..0991caa 100644 --- a/modules/enterprise/server/appserver/src/main/bin-resources/bin/rhq-server.bat +++ b/modules/enterprise/server/appserver/src/main/bin-resources/bin/rhq-server.bat @@ -148,11 +148,11 @@ rem ---------------------------------------------------------------------- rem Create and configure the wrapper log directory. rem ----------------------------------------------------------------------
-if not defined %RHQ_SERVER_WRAPPER_LOG_DIR_PATH ( +if not defined RHQ_SERVER_WRAPPER_LOG_DIR_PATH ( set RHQ_SERVER_WRAPPER_LOG_DIR_PATH=%RHQ_SERVER_HOME%\logs ) -if not exist "RHQ_SERVER_WRAPPER_LOG_DIR_PATH" ( - mkdir "%RHQ_SERVER_WRAPPER_LOG_DIR_PATH%" +if not exist "%RHQ_SERVER_WRAPPER_LOG_DIR_PATH%" ( + mkdir "%RHQ_SERVER_WRAPPER_LOG_DIR_PATH%" )
if defined RHQ_SERVER_DEBUG echo RHQ_SERVER_WRAPPER_LOG_DIR_PATH: %RHQ_SERVER_WRAPPER_LOG_DIR_PATH% diff --git a/modules/enterprise/server/appserver/src/main/bin-resources/bin/rhq-storage.bat b/modules/enterprise/server/appserver/src/main/bin-resources/bin/rhq-storage.bat index 5058874..3ab4442 100644 --- a/modules/enterprise/server/appserver/src/main/bin-resources/bin/rhq-storage.bat +++ b/modules/enterprise/server/appserver/src/main/bin-resources/bin/rhq-storage.bat @@ -171,7 +171,7 @@ if not defined RHQ_STORAGE_WRAPPER_LOG_DIR_PATH ( set RHQ_STORAGE_WRAPPER_LOG_DIR_PATH=%RHQ_SERVER_HOME%\logs ) if not exist "%RHQ_STORAGE_WRAPPER_LOG_DIR_PATH%" ( - mkdir "%RHQ_STORAGE_WRAPPER_LOG_DIR_PATH%" + mkdir "%RHQ_STORAGE_WRAPPER_LOG_DIR_PATH%" )
if defined RHQ_STORAGE_DEBUG echo RHQ_STORAGE_WRAPPER_LOG_DIR_PATH: %RHQ_STORAGE_WRAPPER_LOG_DIR_PATH% diff --git a/modules/enterprise/server/server-control/src/main/java/org/rhq/server/control/Commands.java b/modules/enterprise/server/server-control/src/main/java/org/rhq/server/control/Commands.java index faa135d..3a6dfc0 100644 --- a/modules/enterprise/server/server-control/src/main/java/org/rhq/server/control/Commands.java +++ b/modules/enterprise/server/server-control/src/main/java/org/rhq/server/control/Commands.java @@ -55,6 +55,7 @@ public class Commands { registerCommand(new Start()); registerCommand(new Stop()); registerCommand(new Status()); + // registerCommand(new Remove()); TODO: Do we need this? Maybe use would just use individual .bat files... registerCommand(new Console()); }
diff --git a/modules/enterprise/server/server-control/src/main/java/org/rhq/server/control/ControlCommand.java b/modules/enterprise/server/server-control/src/main/java/org/rhq/server/control/ControlCommand.java index 851dd2d..ee7a404 100644 --- a/modules/enterprise/server/server-control/src/main/java/org/rhq/server/control/ControlCommand.java +++ b/modules/enterprise/server/server-control/src/main/java/org/rhq/server/control/ControlCommand.java @@ -133,13 +133,12 @@ public abstract class ControlCommand { }
protected File getStorageBasedir() { - return new File(getProperty(RHQ_STORAGE_BASEDIR_PROP, new File(basedir, - STORAGE_BASEDIR_NAME).getAbsolutePath())); + return new File( + getProperty(RHQ_STORAGE_BASEDIR_PROP, new File(basedir, STORAGE_BASEDIR_NAME).getAbsolutePath())); }
protected File getAgentBasedir() { - return new File(getProperty(RHQ_AGENT_BASEDIR_PROP, new File(basedir, - AGENT_BASEDIR_NAME).getAbsolutePath())); + return new File(getProperty(RHQ_AGENT_BASEDIR_PROP, new File(basedir, AGENT_BASEDIR_NAME).getAbsolutePath())); }
protected boolean isServerInstalled() { @@ -189,7 +188,7 @@ public abstract class ControlCommand { return rhqctlConfig.getString(key); }
- private String getProperty(String key, String defaultValue) { + private String getProperty(String key, String defaultValue) { return rhqctlConfig.getString(key, defaultValue); }
@@ -205,13 +204,17 @@ public abstract class ControlCommand {
File file = new File(sysprop); if (!file.isFile()) { - throw new RHQControlException("rhqctl.properties-file has as its values [" + file + "] which is not " + - "a file."); + throw new RHQControlException("rhqctl.properties-file has as its values [" + file + "] which is not " + + "a file."); } return file; }
protected org.apache.commons.exec.CommandLine getCommandLine(String scriptName, String... args) { + return getCommandLine(true, scriptName, args); + } + + protected org.apache.commons.exec.CommandLine getCommandLine(boolean addShExt, String scriptName, String... args) { org.apache.commons.exec.CommandLine result;
if (isWindows()) { @@ -220,7 +223,7 @@ public abstract class ControlCommand { result.addArgument(scriptName + ".bat");
} else { - result = new org.apache.commons.exec.CommandLine("./" + scriptName + ".sh"); + result = new org.apache.commons.exec.CommandLine("./" + (addShExt ? scriptName + ".sh" : scriptName)); }
for (String arg : args) { diff --git a/modules/enterprise/server/server-control/src/main/java/org/rhq/server/control/command/Remove.java b/modules/enterprise/server/server-control/src/main/java/org/rhq/server/control/command/Remove.java new file mode 100644 index 0000000..dea18a6 --- /dev/null +++ b/modules/enterprise/server/server-control/src/main/java/org/rhq/server/control/command/Remove.java @@ -0,0 +1,194 @@ +/* + * + * * RHQ Management Platform + * * Copyright (C) 2005-2012 Red Hat, Inc. + * * All rights reserved. + * * + * * This program is free software; you can redistribute it and/or modify + * * it under the terms of the GNU General Public License, version 2, as + * * published by the Free Software Foundation, and/or the GNU Lesser + * * General Public License, version 2.1, also as published by the Free + * * Software Foundation. + * * + * * 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 and the GNU Lesser General Public License + * * for more details. + * * + * * You should have received a copy of the GNU General Public License + * * and the GNU Lesser General Public License along with this program; + * * if not, write to the Free Software Foundation, Inc., + * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +package org.rhq.server.control.command; + +import java.io.File; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Options; +import org.apache.commons.exec.DefaultExecutor; +import org.apache.commons.exec.Executor; +import org.apache.commons.exec.PumpStreamHandler; + +import org.rhq.server.control.ControlCommand; +import org.rhq.server.control.RHQControlException; + +/** + * @author John Sanda + */ +public class Remove extends ControlCommand { + + private Options options; + + public Remove() { + options = new Options().addOption(null, "storage", false, "Remove RHQ storage node service") + .addOption(null, "server", false, "Remove RHQ server service") + .addOption(null, "agent", false, "Remove RHQ agent service"); + } + + @Override + public String getName() { + return "remove"; + } + + @Override + public String getDescription() { + return "Removes RHQ services on Windows. Otherwise, same as Stop command"; + } + + @Override + public Options getOptions() { + return options; + } + + @Override + protected void exec(CommandLine commandLine) { + try { + // if no options specified, then stop whatever is installed + if (commandLine.getOptions().length == 0) { + if (isAgentInstalled()) { + removeAgentService(); + } + if (isServerInstalled()) { + removeServerService(); + } + if (isStorageInstalled()) { + removeStorageService(); + } + } else { + if (commandLine.hasOption(AGENT_OPTION)) { + if (isAgentInstalled()) { + removeAgentService(); + } else { + log.warn("It appears that the agent is not installed. The --" + AGENT_OPTION + + " option will be ignored."); + } + } + if (commandLine.hasOption(SERVER_OPTION)) { + if (isServerInstalled()) { + removeServerService(); + } else { + log.warn("It appears that the server is not installed. The --" + SERVER_OPTION + + " option will be ignored."); + } + } + if (commandLine.hasOption(STORAGE_OPTION)) { + if (isStorageInstalled()) { + removeStorageService(); + } else { + log.warn("It appears that the storage node is not installed. The --" + STORAGE_OPTION + + " option will be ignored."); + } + } + } + } catch (Exception e) { + throw new RHQControlException("Failed to stop services", e); + } + } + + private void removeStorageService() throws Exception { + log.debug("Stopping RHQ storage node"); + + Executor executor = new DefaultExecutor(); + executor.setWorkingDirectory(binDir); + executor.setStreamHandler(new PumpStreamHandler()); + org.apache.commons.exec.CommandLine commandLine; + + if (isWindows()) { + commandLine = getCommandLine("rhq-storage", "remove"); + try { + executor.execute(commandLine); + } catch (Exception e) { + log.debug("Failed to remove storage service", e); + } + } else { + String pid = getStoragePid(); + if (pid != null) { + System.out.println("Stopping RHQ storage node..."); + System.out.println("RHQ storage node (pid=" + pid + ") is stopping..."); + + commandLine = new org.apache.commons.exec.CommandLine("kill").addArgument(pid); + executor.execute(commandLine); + + System.out.println("RHQ storage node has stopped"); + } + + } + } + + private void removeServerService() throws Exception { + log.debug("Stopping RHQ server"); + + Executor executor = new DefaultExecutor(); + executor.setWorkingDirectory(binDir); + executor.setStreamHandler(new PumpStreamHandler()); + org.apache.commons.exec.CommandLine commandLine; + + if (isWindows()) { + try { + commandLine = getCommandLine("rhq-server", "remove"); + executor.execute(commandLine); + } catch (Exception e) { + // Ignore, service may not exist or be running, , script returns 1 + log.debug("Failed to remove server service", e); + } + } else { + String pid = getServerPid(); + + if (pid != null) { + commandLine = getCommandLine("rhq-server", "stop"); + executor.execute(commandLine); + } + } + } + + private void removeAgentService() throws Exception { + log.debug("Stopping RHQ agent"); + + File agentBinDir = new File(getAgentBasedir(), "bin"); + Executor executor = new DefaultExecutor(); + executor.setWorkingDirectory(agentBinDir); + executor.setStreamHandler(new PumpStreamHandler()); + org.apache.commons.exec.CommandLine commandLine; + + if (isWindows()) { + try { + commandLine = getCommandLine("rhq-agent-wrapper", "remove"); + executor.execute(commandLine); + } catch (Exception e) { + // Ignore, service may not exist, script returns 1 + log.debug("Failed to remove agent service", e); + } + } else { + String pid = getAgentPid(); + + if (pid != null) { + commandLine = getCommandLine("rhq-agent-wrapper", "stop"); + executor.execute(commandLine); + } + } + } +} diff --git a/modules/enterprise/server/server-control/src/main/java/org/rhq/server/control/command/Start.java b/modules/enterprise/server/server-control/src/main/java/org/rhq/server/control/command/Start.java index 942f6ee..cfc104d 100644 --- a/modules/enterprise/server/server-control/src/main/java/org/rhq/server/control/command/Start.java +++ b/modules/enterprise/server/server-control/src/main/java/org/rhq/server/control/command/Start.java @@ -114,51 +114,86 @@ public class Start extends ControlCommand { private void startStorage() throws Exception { log.debug("Starting RHQ storage node");
- File storageBinDir = new File(getStorageBasedir(), "bin"); - File pidFile = new File(storageBinDir, "cassandra.pid"); - - // For now we are duplicating logic in the status command. This code will be - // replaced when we implement a rhq-storage.sh script. - if (pidFile.exists()) { - String pid = StreamUtil.slurp(new FileReader(pidFile)); - System.out.println("RHQ storage node (pid " + pid + ") is running"); + Executor executor = new DefaultExecutor(); + executor.setStreamHandler(new PumpStreamHandler()); + org.apache.commons.exec.CommandLine commandLine; + + if (isWindows()) { + executor.setWorkingDirectory(binDir); + commandLine = getCommandLine("rhq-storage", "start"); + try { + executor.execute(commandLine); + + } catch (Exception e) { + // Ignore, service may not exist or may already be running, script returns 1 + log.debug("Failed to start storage service", e); + } } else { - String startScript; - if (isWindows()) { - startScript = "./cassandra.bat"; + + File storageBinDir = new File(getStorageBasedir(), "bin"); + File pidFile = new File(storageBinDir, "cassandra.pid"); + + // For now we are duplicating logic in the status command. This code will be + // replaced when we implement a rhq-storage.sh script. + if (pidFile.exists()) { + String pid = StreamUtil.slurp(new FileReader(pidFile)); + System.out.println("RHQ storage node (pid " + pid + ") is running"); } else { - startScript = "./cassandra"; - } - org.apache.commons.exec.CommandLine commandLine = new org.apache.commons.exec.CommandLine(startScript) - .addArgument("-p").addArgument(pidFile.getAbsolutePath()); - Executor executor = new DefaultExecutor(); - executor.setWorkingDirectory(storageBinDir); - executor.setStreamHandler(new PumpStreamHandler()); + commandLine = getCommandLine(false, "cassandra", "-p", pidFile.getAbsolutePath()); + executor.setWorkingDirectory(storageBinDir);
- executor.execute(commandLine); + executor.execute(commandLine); + } } }
private void startRHQServer() throws Exception { log.debug("Starting RHQ server");
- org.apache.commons.exec.CommandLine commandLine = getCommandLine("rhq-server", "start"); Executor executor = new DefaultExecutor(); executor.setWorkingDirectory(binDir); executor.setStreamHandler(new PumpStreamHandler()); - executor.execute(commandLine); + org.apache.commons.exec.CommandLine commandLine = getCommandLine("rhq-server", "start"); + + if (isWindows()) { + try { + executor.execute(commandLine); + } catch (Exception e) { + // Ignore, service may not exist or be running, , script returns 1 + log.debug("Failed to start server service", e); + } + } else { + String pid = getServerPid(); + + if (pid != null) { + executor.execute(commandLine); + } + } }
private void startAgent() throws Exception { log.debug("Starting RHQ agent");
File agentBinDir = new File(getAgentBasedir(), "bin"); - - org.apache.commons.exec.CommandLine commandLine = getCommandLine("rhq-agent-wrapper", "start"); Executor executor = new DefaultExecutor(); executor.setWorkingDirectory(agentBinDir); executor.setStreamHandler(new PumpStreamHandler()); - executor.execute(commandLine); + org.apache.commons.exec.CommandLine commandLine = getCommandLine("rhq-agent-wrapper", "start"); + + if (isWindows()) { + try { + executor.execute(commandLine); + } catch (Exception e) { + // Ignore, service may not exist or be running, , script returns 1 + log.debug("Failed to start agent service", e); + } + } else { + String pid = getAgentPid(); + + if (pid != null) { + executor.execute(commandLine); + } + } }
} diff --git a/modules/enterprise/server/server-control/src/main/java/org/rhq/server/control/command/Status.java b/modules/enterprise/server/server-control/src/main/java/org/rhq/server/control/command/Status.java index 0aa054c..478b142 100644 --- a/modules/enterprise/server/server-control/src/main/java/org/rhq/server/control/command/Status.java +++ b/modules/enterprise/server/server-control/src/main/java/org/rhq/server/control/command/Status.java @@ -114,14 +114,29 @@ public class Status extends ControlCommand { private void checkStorageStatus() throws Exception { log.debug("Checking RHQ storage node status");
- File storageBinDir = new File(getStorageBasedir(), "bin"); - File pidFile = new File(storageBinDir, "cassandra.pid"); - - if (pidFile.exists()) { - String pid = StreamUtil.slurp(new FileReader(pidFile)); - System.out.println("RHQ storage node (pid " + pid + ") is running"); + if (isWindows()) { + Executor executor = new DefaultExecutor(); + executor.setStreamHandler(new PumpStreamHandler()); + org.apache.commons.exec.CommandLine commandLine; + executor.setWorkingDirectory(binDir); + commandLine = getCommandLine("rhq-storage", "status"); + try { + executor.execute(commandLine); + + } catch (Exception e) { + log.debug("Failed to check storage service status", e); + } } else { - System.out.println("RHQ storage node (no pid file) is NOT running"); + + File storageBinDir = new File(getStorageBasedir(), "bin"); + File pidFile = new File(storageBinDir, "cassandra.pid"); + + if (pidFile.exists()) { + String pid = StreamUtil.slurp(new FileReader(pidFile)); + System.out.println("RHQ storage node (pid " + pid + ") is running"); + } else { + System.out.println("RHQ storage node (no pid file) is NOT running"); + } } }
diff --git a/modules/enterprise/server/server-control/src/main/java/org/rhq/server/control/command/Stop.java b/modules/enterprise/server/server-control/src/main/java/org/rhq/server/control/command/Stop.java index 5c5355a..4aad05c 100644 --- a/modules/enterprise/server/server-control/src/main/java/org/rhq/server/control/command/Stop.java +++ b/modules/enterprise/server/server-control/src/main/java/org/rhq/server/control/command/Stop.java @@ -68,21 +68,21 @@ public class Stop extends ControlCommand { try { // if no options specified, then stop whatever is installed if (commandLine.getOptions().length == 0) { - if (isStorageInstalled()) { - stopStorage(); + if (isAgentInstalled()) { + stopAgent(); } if (isServerInstalled()) { stopRHQServer(); } - if (isAgentInstalled()) { - stopAgent(); + if (isStorageInstalled()) { + stopStorage(); } } else { - if (commandLine.hasOption(STORAGE_OPTION)) { - if (isStorageInstalled()) { - stopStorage(); + if (commandLine.hasOption(AGENT_OPTION)) { + if (isAgentInstalled()) { + stopAgent(); } else { - log.warn("It appears that the storage node is not installed. The --" + STORAGE_OPTION + log.warn("It appears that the agent is not installed. The --" + AGENT_OPTION + " option will be ignored."); } } @@ -94,11 +94,11 @@ public class Stop extends ControlCommand { + " option will be ignored."); } } - if (commandLine.hasOption(AGENT_OPTION)) { - if (isAgentInstalled()) { - stopAgent(); + if (commandLine.hasOption(STORAGE_OPTION)) { + if (isStorageInstalled()) { + stopStorage(); } else { - log.warn("It appears that the agent is not installed. The --" + AGENT_OPTION + log.warn("It appears that the storage node is not installed. The --" + STORAGE_OPTION + " option will be ignored."); } } @@ -111,31 +111,57 @@ public class Stop extends ControlCommand { private void stopStorage() throws Exception { log.debug("Stopping RHQ storage node");
- String pid = getStoragePid(); - if (pid != null) { - System.out.println("Stopping RHQ storage node..."); - System.out.println("RHQ storage node (pid=" + pid + ") is stopping..."); + Executor executor = new DefaultExecutor(); + executor.setWorkingDirectory(binDir); + executor.setStreamHandler(new PumpStreamHandler()); + org.apache.commons.exec.CommandLine commandLine; + + if (isWindows()) { + commandLine = getCommandLine("rhq-storage", "stop"); + try { + executor.execute(commandLine); + System.out.println("RHQ storage node has stopped"); + + } catch (Exception e) { + // Ignore, service may not exist or be running, script returns 1 + log.debug("Failed to stop storage service", e); + } + } else { + String pid = getStoragePid(); + if (pid != null) { + System.out.println("Stopping RHQ storage node..."); + System.out.println("RHQ storage node (pid=" + pid + ") is stopping...");
- org.apache.commons.exec.CommandLine commandLine = new org.apache.commons.exec.CommandLine("kill") - .addArgument(pid); - Executor executor = new DefaultExecutor(); - executor.setStreamHandler(new PumpStreamHandler()); - executor.execute(commandLine); + commandLine = new org.apache.commons.exec.CommandLine("kill").addArgument(pid); + executor.execute(commandLine); + + System.out.println("RHQ storage node has stopped"); + }
- System.out.println("RHQ storage node has stopped"); } }
private void stopRHQServer() throws Exception { log.debug("Stopping RHQ server");
- String pid = getServerPid(); - if (pid != null) { - org.apache.commons.exec.CommandLine commandLine = getCommandLine("rhq-server", "stop"); - Executor executor = new DefaultExecutor(); - executor.setWorkingDirectory(binDir); - executor.setStreamHandler(new PumpStreamHandler()); - executor.execute(commandLine); + Executor executor = new DefaultExecutor(); + executor.setWorkingDirectory(binDir); + executor.setStreamHandler(new PumpStreamHandler()); + org.apache.commons.exec.CommandLine commandLine = getCommandLine("rhq-server", "stop"); + + if (isWindows()) { + try { + executor.execute(commandLine); + } catch (Exception e) { + // Ignore, service may not exist or be running, , script returns 1 + log.debug("Failed to stop server service", e); + } + } else { + String pid = getServerPid(); + + if (pid != null) { + executor.execute(commandLine); + } } }
@@ -143,14 +169,24 @@ public class Stop extends ControlCommand { log.debug("Stopping RHQ agent");
File agentBinDir = new File(getAgentBasedir(), "bin"); - String pid = getAgentPid(); - - if (pid != null) { - org.apache.commons.exec.CommandLine commandLine = getCommandLine("rhq-agent-wrapper", "stop"); - Executor executor = new DefaultExecutor(); - executor.setWorkingDirectory(agentBinDir); - executor.setStreamHandler(new PumpStreamHandler()); - executor.execute(commandLine); + Executor executor = new DefaultExecutor(); + executor.setWorkingDirectory(agentBinDir); + executor.setStreamHandler(new PumpStreamHandler()); + org.apache.commons.exec.CommandLine commandLine = getCommandLine("rhq-agent-wrapper", "stop"); + + if (isWindows()) { + try { + executor.execute(commandLine); + } catch (Exception e) { + // Ignore, service may not exist or be running, , script returns 1 + log.debug("Failed to stop agent service", e); + } + } else { + String pid = getAgentPid(); + + if (pid != null) { + executor.execute(commandLine); + } } } }