From 984ad3c0217344ebdeb3b55fe2ba4018384fcfa3 Mon Sep 17 00:00:00 2001 From: Conor MacNeill Date: Sat, 8 Feb 2003 14:14:27 +0000 Subject: [PATCH] Refactor output handling from Exec into a new class Redirector and use this in Java task to get access to features such as separate error stream, redirected input, etc. git-svn-id: https://svn.apache.org/repos/asf/ant/core/trunk@274020 13f79535-47bb-0310-9956-ffa450edef68 --- .../apache/tools/ant/taskdefs/ExecTask.java | 169 ++------- .../org/apache/tools/ant/taskdefs/Java.java | 124 ++++--- .../tools/ant/taskdefs/PumpStreamHandler.java | 24 +- .../apache/tools/ant/taskdefs/Redirector.java | 344 ++++++++++++++++++ 4 files changed, 439 insertions(+), 222 deletions(-) create mode 100644 src/main/org/apache/tools/ant/taskdefs/Redirector.java diff --git a/src/main/org/apache/tools/ant/taskdefs/ExecTask.java b/src/main/org/apache/tools/ant/taskdefs/ExecTask.java index 1de8a1fac..19e4ad259 100644 --- a/src/main/org/apache/tools/ant/taskdefs/ExecTask.java +++ b/src/main/org/apache/tools/ant/taskdefs/ExecTask.java @@ -54,24 +54,14 @@ package org.apache.tools.ant.taskdefs; -import java.io.BufferedReader; -import java.io.ByteArrayOutputStream; import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.FileInputStream; import java.io.IOException; -import java.io.StringReader; -import java.io.OutputStream; -import java.io.InputStream; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.Project; import org.apache.tools.ant.Task; import org.apache.tools.ant.types.Commandline; import org.apache.tools.ant.types.Environment; -import org.apache.tools.ant.util.StringUtils; import org.apache.tools.ant.util.FileUtils; -import org.apache.tools.ant.util.TeeOutputStream; /** * Executes a given command if the os platform is appropriate. @@ -89,11 +79,6 @@ import org.apache.tools.ant.util.TeeOutputStream; public class ExecTask extends Task { private String os; - private File out; - private File error; - private File input; - - private boolean logError = false; private File dir; protected boolean failOnError = false; @@ -101,17 +86,13 @@ public class ExecTask extends Task { private Long timeout = null; private Environment env = new Environment(); protected Commandline cmdl = new Commandline(); - private FileOutputStream fos = null; - private ByteArrayOutputStream baos = null; - private ByteArrayOutputStream errorBaos = null; - private String outputprop; - private String errorProperty; private String resultProperty; private boolean failIfExecFails = true; - private boolean append = false; private String executable; private boolean resolveExecutable = false; + private Redirector redirector = new Redirector(this); + /** * Controls whether the VM (1.3 and above) is used to execute the * command @@ -170,28 +151,32 @@ public class ExecTask extends Task { this.cmdl = cmdl; } - /** - * Set the input to use for the task - */ - public void setInput(File input) { - this.input = input; - } - /** * File the output of the process is redirected to. If error is not * redirected, it too will appear in the output */ public void setOutput(File out) { - this.out = out; + redirector.setOutput(out); + } + + /** + * Set the input to use for the task + */ + public void setInput(File input) { + redirector.setInput(input); } + public void setInputString(String inputString) { + redirector.setInputString(inputString); + } + /** * Controls whether error output of exec is logged. This is only useful * when output is being redirected and error output is desired in the * Ant log */ public void setLogError(boolean logError) { - this.logError = logError; + redirector.setLogError(logError); } /** @@ -200,15 +185,15 @@ public class ExecTask extends Task { * @since ant 1.6 */ public void setError(File error) { - this.error = error; + redirector.setError(error); } /** * Property name whose value should be set to the output of * the process. */ - public void setOutputproperty(String outputprop) { - this.outputprop = outputprop; + public void setOutputproperty(String outputProp) { + redirector.setOutputProperty(outputProp); } /** @@ -218,7 +203,7 @@ public class ExecTask extends Task { * @since ant 1.6 */ public void setErrorProperty(String errorProperty) { - this.errorProperty = errorProperty; + redirector.setErrorProperty(errorProperty); } /** @@ -293,7 +278,7 @@ public class ExecTask extends Task { * @since 1.30, Ant 1.5 */ public void setAppend(boolean append) { - this.append = append; + redirector.setAppend(append); } @@ -382,7 +367,7 @@ public class ExecTask extends Task { /** * If true, launch new process with VM, otherwise use the OS's shell. */ - public void setVMLauncher(boolean vmLauncher) { + public void setVMLauncher(boolean vmLauncher) { this.vmLauncher = vmLauncher; } @@ -410,22 +395,6 @@ public class ExecTask extends Task { return exe; } - private void setPropertyFromBAOS(ByteArrayOutputStream baos, - String propertyName) throws IOException { - - BufferedReader in = - new BufferedReader(new StringReader(Execute.toString(baos))); - String line = null; - StringBuffer val = new StringBuffer(); - while ((line = in.readLine()) != null) { - if (val.length() != 0) { - val.append(StringUtils.LINE_SEP); - } - val.append(line); - } - getProject().setNewProperty(propertyName, val.toString()); - } - /** * A Utility method for this classes and subclasses to run an * Execute instance (an external command). @@ -447,12 +416,7 @@ public class ExecTask extends Task { log("Result: " + returnCode, Project.MSG_ERR); } } - if (baos != null) { - setPropertyFromBAOS(baos, outputprop); - } - if (errorBaos != null) { - setPropertyFromBAOS(errorBaos, errorProperty); - } + redirector.complete(); } /** @@ -483,86 +447,7 @@ public class ExecTask extends Task { * Create the StreamHandler to use with our Execute instance. */ protected ExecuteStreamHandler createHandler() throws BuildException { - OutputStream outputStream = null; - OutputStream errorStream = null; - InputStream inputStream = null; - - if (out == null && outputprop == null) { - outputStream = new LogOutputStream(this, Project.MSG_INFO); - errorStream = new LogOutputStream(this, Project.MSG_WARN); - } else { - if (out != null) { - try { - outputStream - = new FileOutputStream(out.getAbsolutePath(), append); - log("Output redirected to " + out, Project.MSG_VERBOSE); - } catch (FileNotFoundException fne) { - throw new BuildException("Cannot write to " + out, fne, - getLocation()); - } catch (IOException ioe) { - throw new BuildException("Cannot write to " + out, ioe, - getLocation()); - } - } - - if (outputprop != null) { - baos = new ByteArrayOutputStream(); - log("Output redirected to property: " + outputprop, - Project.MSG_VERBOSE); - if (out == null) { - outputStream = baos; - } else { - outputStream = new TeeOutputStream(outputStream, baos); - } - } else { - baos = null; - } - - errorStream = outputStream; - } - - if (logError) { - errorStream = new LogOutputStream(this, Project.MSG_WARN); - } - - if (error != null) { - try { - errorStream - = new FileOutputStream(error.getAbsolutePath(), append); - log("Error redirected to " + error, Project.MSG_VERBOSE); - } catch (FileNotFoundException fne) { - throw new BuildException("Cannot write to " + error, fne, - getLocation()); - } catch (IOException ioe) { - throw new BuildException("Cannot write to " + error, ioe, - getLocation()); - } - } - - if (errorProperty != null) { - errorBaos = new ByteArrayOutputStream(); - log("Error redirected to property: " + errorProperty, - Project.MSG_VERBOSE); - if (error == null) { - errorStream = errorBaos; - } else { - errorStream = new TeeOutputStream(errorStream, errorBaos); - } - } else { - errorBaos = null; - } - - if (input != null) { - try { - inputStream = new FileInputStream(input); - } catch (FileNotFoundException fne) { - throw new BuildException("Cannot read from " + input, fne, - getLocation()); - } - } - - return new PumpStreamHandler(outputStream, errorStream, inputStream, - true, true, true); + return redirector.createHandler(); } /** @@ -579,14 +464,6 @@ public class ExecTask extends Task { * Flush the output stream - if there is one. */ protected void logFlush() { - try { - if (fos != null) { - fos.close(); - } - if (baos != null) { - baos.close(); - } - } catch (IOException io) {} } } diff --git a/src/main/org/apache/tools/ant/taskdefs/Java.java b/src/main/org/apache/tools/ant/taskdefs/Java.java index f65820f10..4a3df7af4 100644 --- a/src/main/org/apache/tools/ant/taskdefs/Java.java +++ b/src/main/org/apache/tools/ant/taskdefs/Java.java @@ -55,9 +55,7 @@ package org.apache.tools.ant.taskdefs; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.PrintStream; import java.util.Vector; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.ExitException; @@ -89,12 +87,10 @@ public class Java extends Task { private boolean fork = false; private boolean newEnvironment = false; private File dir = null; - private File out; - private PrintStream outStream = null; private boolean failOnError = false; private boolean append = false; private Long timeout = null; - + private Redirector redirector = new Redirector(this); /** * Do the execution. */ @@ -302,7 +298,54 @@ public class Java extends Task { * File the output of the process is redirected to. */ public void setOutput(File out) { - this.out = out; + redirector.setOutput(out); + } + + /** + * Set the input to use for the task + */ + public void setInput(File input) { + redirector.setInput(input); + } + + public void setInputString(String inputString) { + redirector.setInputString(inputString); + } + + /** + * Controls whether error output of exec is logged. This is only useful + * when output is being redirected and error output is desired in the + * Ant log + */ + public void setLogError(boolean logError) { + redirector.setLogError(logError); + } + + /** + * File the error stream of the process is redirected to. + * + * @since ant 1.6 + */ + public void setError(File error) { + redirector.setError(error); + } + + /** + * Property name whose value should be set to the output of + * the process. + */ + public void setOutputproperty(String outputProp) { + redirector.setOutputProperty(outputProp); + } + + /** + * Property name whose value should be set to the error of + * the process. + * + * @since ant 1.6 + */ + public void setErrorProperty(String errorProperty) { + redirector.setErrorProperty(errorProperty); } /** @@ -366,8 +409,8 @@ public class Java extends Task { * @since Ant 1.5 */ protected void handleOutput(String line) { - if (outStream != null) { - outStream.println(line); + if (redirector.getOutputStream() != null) { + redirector.handleOutput(line); } else { super.handleOutput(line); } @@ -379,8 +422,8 @@ public class Java extends Task { * @since Ant 1.5.2 */ protected void handleFlush(String line) { - if (outStream != null) { - outStream.print(line); + if (redirector.getOutputStream() != null) { + redirector.handleFlush(line); } else { super.handleFlush(line); } @@ -392,8 +435,8 @@ public class Java extends Task { * @since Ant 1.5 */ protected void handleErrorOutput(String line) { - if (outStream != null) { - outStream.println(line); + if (redirector.getErrorStream() != null) { + redirector.handleErrorOutput(line); } else { super.handleErrorOutput(line); } @@ -405,8 +448,8 @@ public class Java extends Task { * @since Ant 1.5.2 */ protected void handleErrorFlush(String line) { - if (outStream != null) { - outStream.println(line); + if (redirector.getErrorStream() != null) { + redirector.handleErrorFlush(line); } else { super.handleErrorOutput(line); } @@ -417,28 +460,17 @@ public class Java extends Task { * was a command line application. */ private void run(CommandlineJava command) throws BuildException { - ExecuteJava exe = new ExecuteJava(); - exe.setJavaCommand(command.getJavaCommand()); - exe.setClasspath(command.getClasspath()); - exe.setSystemProperties(command.getSystemProperties()); - exe.setTimeout(timeout); - if (out != null) { - try { - outStream = - new PrintStream(new FileOutputStream(out.getAbsolutePath(), - append)); - exe.execute(getProject()); - System.out.flush(); - System.err.flush(); - } catch (IOException io) { - throw new BuildException(io, getLocation()); - } finally { - if (outStream != null) { - outStream.close(); - } - } - } else { + try { + ExecuteJava exe = new ExecuteJava(); + exe.setJavaCommand(command.getJavaCommand()); + exe.setClasspath(command.getClasspath()); + exe.setSystemProperties(command.getSystemProperties()); + exe.setTimeout(timeout); + redirector.createStreams(); exe.execute(getProject()); + redirector.complete(); + } catch (IOException e) { + throw new BuildException(e); } } @@ -446,19 +478,9 @@ public class Java extends Task { * Executes the given classname with the given arguments in a separate VM. */ private int run(String[] command) throws BuildException { - FileOutputStream fos = null; - try { - Execute exe = null; - if (out == null) { - exe = new Execute(new LogStreamHandler(this, Project.MSG_INFO, - Project.MSG_WARN), - createWatchdog()); - } else { - fos = new FileOutputStream(out.getAbsolutePath(), append); - exe = new Execute(new PumpStreamHandler(fos), - createWatchdog()); - } + Execute exe + = new Execute(redirector.createHandler(), createWatchdog()); exe.setAntRun(getProject()); if (dir == null) { @@ -487,17 +509,11 @@ public class Java extends Task { if (exe.killedProcess()) { log("Timeout: killed the sub-process", Project.MSG_WARN); } + redirector.complete(); return rc; } catch (IOException e) { throw new BuildException(e, getLocation()); } - } catch (IOException io) { - throw new BuildException(io, getLocation()); - } finally { - if (fos != null) { - try {fos.close();} catch (IOException io) {} - } - } } /** diff --git a/src/main/org/apache/tools/ant/taskdefs/PumpStreamHandler.java b/src/main/org/apache/tools/ant/taskdefs/PumpStreamHandler.java index 9a35fdfd1..b0b2d098b 100644 --- a/src/main/org/apache/tools/ant/taskdefs/PumpStreamHandler.java +++ b/src/main/org/apache/tools/ant/taskdefs/PumpStreamHandler.java @@ -77,24 +77,15 @@ public class PumpStreamHandler implements ExecuteStreamHandler { private OutputStream err; private InputStream input; - private boolean closeOutOnStop = false; - private boolean closeErrOnStop = false; - private boolean closeInputOnStop = false; - public PumpStreamHandler(OutputStream out, OutputStream err, - InputStream input, - boolean closeOutOnStop, boolean closeErrOnStop, - boolean closeInputOnStop) { + InputStream input) { this.out = out; this.err = err; this.input = input; - this.closeOutOnStop = closeOutOnStop; - this.closeErrOnStop = closeErrOnStop; - this.closeInputOnStop = closeInputOnStop; } public PumpStreamHandler(OutputStream out, OutputStream err) { - this(out, err, null, false, false, false); + this(out, err, null); } public PumpStreamHandler(OutputStream outAndErr) { @@ -151,29 +142,18 @@ public class PumpStreamHandler implements ExecuteStreamHandler { if (inputThread != null) { try { inputThread.join(); - if (closeInputOnStop) { - input.close(); - } } catch (InterruptedException e) { // ignore - } catch (IOException e) { - // ignore } } try { err.flush(); - if (closeErrOnStop) { - err.close(); - } } catch (IOException e) { // ignore } try { out.flush(); - if (closeOutOnStop) { - out.close(); - } } catch (IOException e) { // ignore } diff --git a/src/main/org/apache/tools/ant/taskdefs/Redirector.java b/src/main/org/apache/tools/ant/taskdefs/Redirector.java new file mode 100644 index 000000000..e90aab107 --- /dev/null +++ b/src/main/org/apache/tools/ant/taskdefs/Redirector.java @@ -0,0 +1,344 @@ +/* + * The Apache Software License, Version 1.1 + * + * Copyright (c) 2003 The Apache Software Foundation. All rights + * reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The end-user documentation included with the redistribution, if + * any, must include the following acknowlegement: + * "This product includes software developed by the + * Apache Software Foundation (http://www.apache.org/)." + * Alternately, this acknowlegement may appear in the software itself, + * if and wherever such third-party acknowlegements normally appear. + * + * 4. The names "The Jakarta Project", "Ant", and "Apache Software + * Foundation" must not be used to endorse or promote products derived + * from this software without prior written permission. For written + * permission, please contact apache@apache.org. + * + * 5. Products derived from this software may not be called "Apache" + * nor may "Apache" appear in their names without prior written + * permission of the Apache Group. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + */ +package org.apache.tools.ant.taskdefs; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.StringReader; +import java.io.OutputStream; +import java.io.InputStream; +import java.io.PrintStream; +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.Project; +import org.apache.tools.ant.Task; +import org.apache.tools.ant.util.StringUtils; +import org.apache.tools.ant.util.TeeOutputStream; + +/** + * The Redirector class manages the setup and connection of + * input and output redirection for an Ant task. + * + * @author Conor MacNeill + * @since Ant 1.6 + */ +public class Redirector { + private File out; + private File error; + private File input; + + private boolean logError = false; + private ByteArrayOutputStream baos = null; + private ByteArrayOutputStream errorBaos = null; + private String outputProperty; + private String errorProperty; + private String inputString; + private boolean append = false; + + private Task managingTask; + + private OutputStream outputStream = null; + private OutputStream errorStream = null; + private InputStream inputStream = null; + private PrintStream outPrintStream = null; + private PrintStream errorPrintStream = null; + + public Redirector(Task managingTask) { + this.managingTask = managingTask; + } + + /** + * Set the input to use for the task + */ + public void setInput(File input) { + this.input = input; + } + + public void setInputString(String inputString) { + this.inputString = inputString; + } + + + /** + * File the output of the process is redirected to. If error is not + * redirected, it too will appear in the output + */ + public void setOutput(File out) { + this.out = out; + } + + /** + * Controls whether error output of exec is logged. This is only useful + * when output is being redirected and error output is desired in the + * Ant log + */ + public void setLogError(boolean logError) { + this.logError = logError; + } + + /** + * File the error stream of the process is redirected to. + * + */ + public void setError(File error) { + this.error = error; + } + + /** + * Property name whose value should be set to the output of + * the process. + */ + public void setOutputProperty(String outputProperty) { + this.outputProperty = outputProperty; + } + + /** + * Whether output should be appended to or overwrite an existing file. + * Defaults to false. + * + */ + public void setAppend(boolean append) { + this.append = append; + } + + /** + * Property name whose value should be set to the error of + * the process. + * + */ + public void setErrorProperty(String errorProperty) { + this.errorProperty = errorProperty; + } + + private void setPropertyFromBAOS(ByteArrayOutputStream baos, + String propertyName) throws IOException { + + BufferedReader in = + new BufferedReader(new StringReader(Execute.toString(baos))); + String line = null; + StringBuffer val = new StringBuffer(); + while ((line = in.readLine()) != null) { + if (val.length() != 0) { + val.append(StringUtils.LINE_SEP); + } + val.append(line); + } + managingTask.getProject().setNewProperty(propertyName, val.toString()); + } + + + public void createStreams() { + if (out == null && outputProperty == null) { + outputStream = new LogOutputStream(managingTask, Project.MSG_INFO); + errorStream = new LogOutputStream(managingTask, Project.MSG_WARN); + } else { + if (out != null) { + try { + outputStream + = new FileOutputStream(out.getAbsolutePath(), append); + managingTask.log("Output redirected to " + out, + Project.MSG_VERBOSE); + } catch (FileNotFoundException fne) { + throw new BuildException("Cannot write to " + out, fne); + } catch (IOException ioe) { + throw new BuildException("Cannot write to " + out, ioe); + } + } + + if (outputProperty != null) { + baos = new ByteArrayOutputStream(); + managingTask.log("Output redirected to property: " + + outputProperty, Project.MSG_VERBOSE); + if (out == null) { + outputStream = baos; + } else { + outputStream = new TeeOutputStream(outputStream, baos); + } + } else { + baos = null; + } + + errorStream = outputStream; + } + + if (logError) { + errorStream = new LogOutputStream(managingTask, Project.MSG_WARN); + } + + if (error != null) { + try { + errorStream + = new FileOutputStream(error.getAbsolutePath(), append); + managingTask.log("Error redirected to " + error, + Project.MSG_VERBOSE); + } catch (FileNotFoundException fne) { + throw new BuildException("Cannot write to " + error, fne); + } catch (IOException ioe) { + throw new BuildException("Cannot write to " + error, ioe); + } + } + + if (errorProperty != null) { + errorBaos = new ByteArrayOutputStream(); + managingTask.log("Error redirected to property: " + errorProperty, + Project.MSG_VERBOSE); + if (error == null) { + errorStream = errorBaos; + } else { + errorStream = new TeeOutputStream(errorStream, errorBaos); + } + } else { + errorBaos = null; + } + + if (input != null && inputString != null) { + throw new BuildException("The \"input\" and \"inputstring\" " + + "attributes cannot both be specified"); + } + if (input != null) { + try { + inputStream = new FileInputStream(input); + } catch (FileNotFoundException fne) { + throw new BuildException("Cannot read from " + input, fne); + } + } else if (inputString != null) { + inputStream = new ByteArrayInputStream(inputString.getBytes()); + } + } + + + /** + * Create the StreamHandler to use with our Execute instance. + */ + public ExecuteStreamHandler createHandler() throws BuildException { + createStreams(); + return new PumpStreamHandler(outputStream, errorStream, inputStream); + } + + /** + * Pass output sent to System.out to specified output file. + * + */ + protected void handleOutput(String line) { + if (outPrintStream == null) { + outPrintStream = new PrintStream(outputStream); + } + outPrintStream.println(line); + } + + /** + * Pass output sent to System.out to specified output file. + * + */ + protected void handleFlush(String line) { + if (outPrintStream == null) { + outPrintStream = new PrintStream(outputStream); + } + outPrintStream.print(line); + } + + /** + * Pass output sent to System.err to specified output file. + * + */ + protected void handleErrorOutput(String line) { + if (errorPrintStream == null) { + errorPrintStream = new PrintStream(errorStream); + } + errorPrintStream.println(line); + } + + /** + * Pass output sent to System.err to specified output file. + * + */ + protected void handleErrorFlush(String line) { + if (errorPrintStream == null) { + errorPrintStream = new PrintStream(errorStream); + } + errorPrintStream.print(line); + } + + public OutputStream getOutputStream() { + return outputStream; + } + + public OutputStream getErrorStream() { + return errorStream; + } + + + public void complete() throws IOException { + System.out.flush(); + System.err.flush(); + + if (inputStream != null) { + inputStream.close(); + } + outputStream.close(); + errorStream.close(); + + if (baos != null) { + setPropertyFromBAOS(baos, outputProperty); + } + if (errorBaos != null) { + setPropertyFromBAOS(errorBaos, errorProperty); + } + } +}