| /******************************************************************************* |
| * Copyright (c) 2002, 2012 IBM Corporation and others. |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Initial Contributors: |
| * The following IBM employees contributed to the Remote System Explorer |
| * component that contains this file: David McKnight, Kushal Munir, |
| * Michael Berger, David Dykstal, Phil Coulthard, Don Yantzi, Eric Simpson, |
| * Emily Bruner, Mazen Faraj, Adrian Storisteanu, Li Ding, and Kent Hawley. |
| * |
| * Contributors: |
| * David McKnight (IBM) - [193426] don't display exceptions |
| * David McKnight (IBM) - [226561] [apidoc] Add API markup to RSE Javadocs where extend / implement is allowed |
| * David McKnight (IBM) - [245714] [dstore] Multiple user ID/password prompts and connect fails |
| * David McKnight (IBM) - [283613] [dstore] Create a Constants File for all System Properties we support |
| * David McKnight (IBM) - [378878] [dstore] Need ability to log handshake messages from the authentication/server process to ServerLauncher |
| * David McKnight (IBM) - [388472] [dstore] need alternative option for getting at server hostname |
| *******************************************************************************/ |
| |
| package org.eclipse.dstore.core.server; |
| |
| import java.io.BufferedReader; |
| import java.io.BufferedWriter; |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.InputStreamReader; |
| import java.io.OutputStreamWriter; |
| import java.io.PrintWriter; |
| import java.io.RandomAccessFile; |
| import java.net.BindException; |
| import java.net.InetAddress; |
| import java.net.ServerSocket; |
| import java.net.Socket; |
| import java.net.UnknownHostException; |
| import java.util.ArrayList; |
| import java.util.Date; |
| import java.util.regex.Pattern; |
| |
| import javax.net.ssl.HandshakeCompletedEvent; |
| import javax.net.ssl.HandshakeCompletedListener; |
| import javax.net.ssl.SSLContext; |
| import javax.net.ssl.SSLSession; |
| import javax.net.ssl.SSLSocket; |
| |
| import org.eclipse.dstore.core.model.DE; |
| import org.eclipse.dstore.core.model.IDataStoreConstants; |
| import org.eclipse.dstore.core.model.ISSLProperties; |
| import org.eclipse.dstore.internal.core.model.IDataStoreSystemProperties; |
| import org.eclipse.dstore.internal.core.server.ServerAttributes; |
| import org.eclipse.dstore.internal.core.server.ServerReturnCodes; |
| import org.eclipse.dstore.internal.core.server.ServerSSLProperties; |
| import org.eclipse.dstore.internal.core.util.ssl.DStoreSSLContext; |
| |
| /** |
| * This class is the DataStore daemon. It is used for authenticating users, |
| * launching DataStore servers under particular user IDs, and providing a client |
| * with enough information to connect to a launched server. |
| * |
| */ |
| public class ServerLauncher extends Thread { |
| |
| /** |
| * An instances of this class get loaded whenever a client requests access |
| * to a DataStore server. The ConnectionListener attempts to launch a server |
| * under the client user's ID, communicating back information to the client |
| * so that if may connect to the launched server. If the authentification |
| * and connection to the server are successful, ConnectionListener continues |
| * to monitor the server connection until it is terminated. |
| */ |
| public class ConnectionListener extends Thread implements |
| HandshakeCompletedListener { |
| |
| private Socket _socket; |
| |
| private PrintWriter _writer; |
| |
| private BufferedReader _reader; |
| |
| private Process _serverProcess; |
| |
| private String _port; |
| |
| private boolean _done; |
| |
| private BufferedReader _outReader; |
| |
| private BufferedReader _errReader; |
| |
| /** |
| * Constructor |
| * |
| * @param socket |
| * a socket to the daemon |
| */ |
| public ConnectionListener(Socket socket) { |
| |
| _socket = socket; |
| try { |
| _writer = new PrintWriter(new OutputStreamWriter(_socket |
| .getOutputStream(), DE.ENCODING_UTF_8)); |
| _reader = new BufferedReader(new InputStreamReader(_socket |
| .getInputStream(), DE.ENCODING_UTF_8)); |
| } catch (java.io.IOException e) { |
| String msg = "ServerLauncher:" + e; //$NON-NLS-1$ |
| System.out.println(msg); |
| logError(msg, e); |
| } |
| } |
| |
| /** |
| * Called when shutdown |
| */ |
| public void finalize() throws Throwable { |
| if (_serverProcess != null) { |
| _serverProcess.destroy(); |
| } |
| super.finalize(); |
| } |
| |
| /** |
| * Listens to the connection and prints any output while the connection |
| * is active |
| */ |
| public void run() { |
| _done = true; |
| if (listen()) { |
| if (_serverProcess != null) { |
| _done = false; |
| |
| try { |
| String line = null; |
| |
| while ((_outReader != null) |
| && ((line = _outReader.readLine()) != null)) { |
| if (line.equals(ServerReturnCodes.RC_FINISHED)) { |
| break; |
| } else { |
| System.out.println(line); |
| } |
| } |
| |
| if (_outReader != null) { |
| _outReader.close(); |
| } |
| if (_errReader != null) { |
| _errReader.close(); |
| } |
| |
| _serverProcess.waitFor(); |
| } catch (Exception e) { |
| String msg = "ServerLauncher:" + e; //$NON-NLS-1$ |
| System.out.println(msg); |
| logError(msg, e); |
| } |
| } |
| |
| String msg = "finished on port " + _port; //$NON-NLS-1$ |
| System.out.println(msg); |
| |
| _outReader = null; |
| _errReader = null; |
| _serverProcess = null; |
| _done = true; |
| } else { |
| _done = true; |
| } |
| } |
| |
| /** |
| * Indicates whether the connection has terminated or not |
| * |
| * @return true if the connection has terminated |
| */ |
| public boolean isDone() { |
| return _done; |
| } |
| |
| /** |
| * Returns the DataStore server port used |
| * |
| * @return the server port |
| */ |
| public String getServerPort() { |
| return _port; |
| } |
| |
| private boolean isPortInRange(String portStr, String portRange) { |
| if (portRange != null) { |
| String[] range = portRange.split("-"); //$NON-NLS-1$ |
| if (range.length == 2) { |
| int lPort = 0; |
| int hPort = 0; |
| int port = 0; |
| try { |
| lPort = Integer.parseInt(range[0]); |
| hPort = Integer.parseInt(range[1]); |
| port = Integer.parseInt(portStr); |
| } catch (Exception e) { |
| } |
| |
| return (port >= lPort && port <= hPort); |
| } |
| } |
| return true; |
| } |
| |
| /** |
| * Attempt to start a new DataStore server. The port and the ticket for |
| * a newly started DataStore are captured and sent back to the client so |
| * that it may connect to the server. |
| * |
| * @return whether the server started successfully |
| */ |
| public boolean listen() |
| { |
| boolean connected = false; |
| |
| String user = null; |
| String password = null; |
| |
| _port = null; |
| |
| boolean problemReadingSocket = false; |
| try |
| { |
| String userTmp = _reader.readLine(); |
| if (Pattern.matches("\\w+", userTmp)) { |
| user = userTmp; |
| } |
| password = _reader.readLine(); |
| String portTmp = _reader.readLine(); |
| if (Pattern.matches("\\d{5}", portTmp)) { |
| _port = portTmp; |
| } |
| } |
| catch (IOException e) |
| { |
| // don't display exceptions 193426 |
| //e.printStackTrace(); |
| _port = "0"; //$NON-NLS-1$ |
| |
| // this is probably an SSL exception |
| _writer.println(IDataStoreConstants.SERVER_FAILURE + e); |
| problemReadingSocket = true; |
| } |
| |
| |
| if (_serverPortRange != null && (_port == null || _port.equals("0"))) //$NON-NLS-1$ |
| { |
| _port = _serverPortRange; |
| } |
| |
| if (!problemReadingSocket) |
| { |
| boolean isError = false; |
| if (_serverPortRange != null && _port != _serverPortRange) |
| { |
| if (!isPortInRange(_port, _serverPortRange)) |
| { |
| String message = IDataStoreConstants.PORT_OUT_RANGE; |
| message += _serverPortRange; |
| _writer.println(message); |
| isError = true; |
| } |
| } |
| if (!isError) |
| { |
| // start new server |
| try |
| { |
| String launchStatus = null; |
| String ticket = new String("" + System.currentTimeMillis()); //$NON-NLS-1$ |
| |
| String theOS = System.getProperty("os.name"); //$NON-NLS-1$ |
| String timeout = "120000"; //$NON-NLS-1$ |
| |
| |
| if (!theOS.toLowerCase().startsWith("win")) //$NON-NLS-1$ |
| { |
| // assuming unix compatable |
| // |
| // Get the property which |
| // contains the authorization |
| // script path |
| // |
| String authPath = System.getProperty(IDataStoreSystemProperties.RSE_AUTH); |
| File authFile = null; |
| if (authPath != null && authPath.length() > 0) |
| { |
| authFile = new File(authPath); |
| } |
| if (authFile == null || !authFile.exists()) |
| { |
| authPath = "perl " + _path + File.separator + "auth.pl"; //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| |
| String authString = |
| authPath |
| + " " //$NON-NLS-1$ |
| + user |
| + " " //$NON-NLS-1$ |
| + _path |
| + " " //$NON-NLS-1$ |
| + _port |
| + " " //$NON-NLS-1$ |
| + timeout |
| + " " //$NON-NLS-1$ |
| + ticket |
| + " " //$NON-NLS-1$ |
| + System.getProperty("java.home") //$NON-NLS-1$ |
| ; |
| |
| String[] authArray = { "sh", "-c", authString }; //$NON-NLS-1$ //$NON-NLS-2$ |
| |
| // test password |
| _serverProcess = Runtime.getRuntime().exec(authArray); |
| |
| _outReader = new BufferedReader(new InputStreamReader(_serverProcess.getInputStream())); |
| _errReader = new BufferedReader(new InputStreamReader(_serverProcess.getErrorStream())); |
| BufferedWriter inWriter = new BufferedWriter(new OutputStreamWriter(_serverProcess.getOutputStream())); |
| // write password |
| if (password != null) |
| { |
| inWriter.write(password); |
| inWriter.newLine(); |
| inWriter.flush(); |
| |
| launchStatus = _outReader.readLine(); |
| } |
| } |
| else |
| { |
| |
| // launch new server |
| String[] cmdArray = |
| { |
| "java", //$NON-NLS-1$ |
| "-DA_PLUGIN_PATH=" + _path, //$NON-NLS-1$ |
| "-DDSTORE_SPIRIT_ON=true", //$NON-NLS-1$ |
| "org.eclipse.dstore.core.server.Server", //$NON-NLS-1$ |
| _port, |
| timeout, |
| ticket}; |
| |
| _serverProcess = Runtime.getRuntime().exec(cmdArray); |
| _outReader = new BufferedReader(new InputStreamReader(_serverProcess.getInputStream())); |
| _errReader = new BufferedReader(new InputStreamReader(_serverProcess.getErrorStream())); |
| |
| launchStatus = "success"; //$NON-NLS-1$ |
| } |
| |
| logMessage("launch status = "+launchStatus); //$NON-NLS-1$ |
| if ((launchStatus == null) || !launchStatus.equals("success")) //$NON-NLS-1$ |
| { |
| _writer.println(IDataStoreConstants.AUTHENTICATION_FAILED); |
| } |
| else |
| { |
| // look for the server startup string, it needs to occur |
| // somewhere in the line. |
| String status = _errReader.readLine(); |
| logMessage("status = "+status); //$NON-NLS-1$ |
| while (status!=null && (status.indexOf(ServerReturnCodes.RC_DSTORE_SERVER_MAGIC) < 0)) |
| { |
| status = _errReader.readLine(); |
| } |
| // now read the real server status |
| if (status != null) |
| { |
| status = _errReader.readLine(); |
| } |
| if ((status != null) && status.equals(ServerReturnCodes.RC_SUCCESS)) |
| { |
| _port = _errReader.readLine(); |
| _errReader.readLine(); |
| _writer.println(IDataStoreConstants.CONNECTED); |
| _writer.println(_port); |
| _writer.println(ticket); |
| |
| String msg = "launched new server on " + _port; //$NON-NLS-1$ |
| System.out.println(msg); |
| logMessage(msg); |
| connected = true; |
| } |
| else |
| { |
| if (status == null) |
| { |
| status = new String(IDataStoreConstants.UNKNOWN_PROBLEM); |
| } |
| // TODO Make sure that the client doesnt try |
| // connecting forever |
| _writer.println(status); |
| |
| _serverProcess.destroy(); |
| _serverProcess = null; |
| _outReader.close(); |
| _outReader = null; |
| |
| _errReader.close(); |
| _errReader = null; |
| } |
| } |
| |
| } |
| catch (IOException e) |
| { |
| _writer.println(IDataStoreConstants.SERVER_FAILURE + e); |
| } |
| } |
| } |
| |
| _writer.flush(); |
| |
| // close socket |
| try |
| { |
| _socket.close(); |
| } |
| catch (IOException e) |
| { |
| String msg = "ServerLauncher:" + e; //$NON-NLS-1$ |
| System.out.println(msg); |
| logError(msg, e); |
| } |
| |
| return connected; |
| } |
| |
| public void handshakeCompleted(HandshakeCompletedEvent event) { |
| String msg = "handshake completed"; //$NON-NLS-1$ |
| System.out.println(msg); |
| System.out.println(event); |
| |
| logMessage(msg); |
| } |
| } |
| |
| private ServerSocket _serverSocket; |
| |
| private String _path; |
| |
| private ArrayList _connections; |
| |
| private String _serverPortRange; |
| |
| private ISSLProperties _sslProperties; |
| |
| private boolean _logDaemon = false; |
| private RandomAccessFile _logFile = null; |
| |
| public static int DEFAULT_DAEMON_PORT = 4075; |
| |
| /** |
| * Constructor |
| */ |
| public ServerLauncher() { |
| String pluginPath = System.getProperty(IDataStoreSystemProperties.A_PLUGIN_PATH); |
| if (pluginPath == null) { |
| System.out.println("A_PLUGIN_PATH is not defined"); //$NON-NLS-1$ |
| System.exit(-1); |
| } |
| else |
| { |
| _path = pluginPath.trim(); |
| |
| _connections = new ArrayList(); |
| |
| init(DEFAULT_DAEMON_PORT + ""); //$NON-NLS-1$ |
| } |
| } |
| |
| /** |
| * Constructor |
| * |
| * @param portStr |
| * the port for the daemon socket to run on |
| */ |
| public ServerLauncher(String portStr) { |
| String pluginPath = System.getProperty(IDataStoreSystemProperties.A_PLUGIN_PATH); |
| if (pluginPath == null) { |
| System.out.println("A_PLUGIN_PATH is not defined"); //$NON-NLS-1$ |
| System.exit(-1); |
| } |
| else |
| { |
| _path = pluginPath.trim(); |
| |
| _connections = new ArrayList(); |
| init(portStr); |
| } |
| } |
| |
| /** |
| * Constructor |
| * |
| * @param portStr |
| * the port for the daemon socket to run on |
| * @param serverPortRange |
| * the port range for launched servers |
| */ |
| public ServerLauncher(String portStr, String serverPortRange) { |
| String pluginPath = System.getProperty(IDataStoreSystemProperties.A_PLUGIN_PATH); |
| if (pluginPath == null) { |
| System.out.println("A_PLUGIN_PATH is not defined"); //$NON-NLS-1$ |
| System.exit(-1); |
| } |
| else |
| { |
| _path = pluginPath.trim(); |
| |
| _serverPortRange = serverPortRange; |
| _connections = new ArrayList(); |
| init(portStr); |
| } |
| } |
| |
| private String getKeyStoreLocation() { |
| return _sslProperties.getDaemonKeyStorePath(); |
| } |
| |
| private String getKeyStorePassword() { |
| return _sslProperties.getDaemonKeyStorePassword(); |
| } |
| |
| /** |
| * initializes the DataStore daemon |
| * |
| * @param portStr the daemon port |
| */ |
| public void init(String portStr) { |
| |
| String logdaemonLocation = System.getProperty("logdaemonpath"); //$NON-NLS-1$ |
| if (logdaemonLocation != null && logdaemonLocation.length() > 0){ |
| // create temp file for logging |
| File traceFileHandle = new File(logdaemonLocation); |
| if (!traceFileHandle.exists()){ |
| try { // try to create it |
| traceFileHandle.createNewFile(); |
| } catch (IOException e) { |
| } |
| } |
| if (traceFileHandle.canWrite()){ |
| try { |
| _logFile = new RandomAccessFile(traceFileHandle, "rw"); //$NON-NLS-1$ |
| _logFile.seek(traceFileHandle.length()); |
| |
| logMessage("-----------------------------------------"); //$NON-NLS-1$ |
| logMessage("Start Tracing at " + System.currentTimeMillis()); //$NON-NLS-1$ |
| _logDaemon = true; |
| } |
| catch (Exception e){ |
| } |
| } |
| } |
| |
| // create server socket from port |
| _sslProperties = new ServerSSLProperties(); |
| |
| // determine if portStr is a port range or just a port |
| String[] range = portStr.split("-"); //$NON-NLS-1$ |
| if (range.length == 2) { |
| int lPort = 0; |
| int hPort = 0; |
| try { |
| lPort = Integer.parseInt(range[0]); |
| hPort = Integer.parseInt(range[1]); |
| } catch (Exception e) { |
| } |
| |
| boolean socketBound = false; |
| for (int i = lPort; i < hPort && !socketBound; i++) { |
| // create server socket from port |
| try { |
| if (_sslProperties.usingSSL()) { |
| String keyStoreFileName = getKeyStoreLocation(); |
| String keyStorePassword = getKeyStorePassword(); |
| |
| try { |
| SSLContext sslContext = DStoreSSLContext |
| .getServerSSLContext(keyStoreFileName, |
| keyStorePassword); |
| |
| _serverSocket = sslContext.getServerSocketFactory() |
| .createServerSocket(i); |
| } catch (Exception e) { |
| // don't display exceptions 193426 |
| //e.printStackTrace(); |
| } |
| } else { |
| _serverSocket = new ServerSocket(i); |
| } |
| if (_serverSocket != null |
| && _serverSocket.getLocalPort() > 0) { |
| socketBound = true; |
| String msg = "Daemon running on: " + ServerAttributes.getHostName() + ", port: " + i; //$NON-NLS-1$ //$NON-NLS-2$ |
| System.out.println(msg); |
| logMessage(msg); |
| } |
| } catch (UnknownHostException e) { |
| String msg = "Networking problem, can't resolve local host"; //$NON-NLS-1$ |
| System.err.println(msg); |
| // don't display exceptions 193426 |
| logError(msg, e); |
| System.exit(-1); |
| } catch (BindException e) { |
| String msg = "socket taken on " + i; //$NON-NLS-1$ |
| System.err.println(msg); |
| logError(msg, e); |
| // keep going |
| } catch (IOException e) { |
| String msg = "Failure to create ServerSocket"; //$NON-NLS-1$ |
| System.err.println(msg); |
| // don't display exceptions 193426 |
| //e.printStackTrace(); |
| logError(msg, e); |
| System.exit(-1); |
| } |
| |
| } |
| } else { |
| int port = Integer.parseInt(portStr); |
| try { |
| if (_sslProperties.usingSSL()) { |
| String keyStoreFileName = getKeyStoreLocation(); |
| String keyStorePassword = getKeyStorePassword(); |
| |
| try { |
| SSLContext sslContext = DStoreSSLContext |
| .getServerSSLContext(keyStoreFileName, |
| keyStorePassword); |
| |
| _serverSocket = sslContext.getServerSocketFactory() |
| .createServerSocket(port); |
| } catch (Exception e) { |
| // don't display exceptions 193426 |
| //e.printStackTrace(); |
| System.err.println(e.getMessage()); |
| logError(e.getMessage(), e); |
| System.exit(-1); |
| } |
| } else { |
| _serverSocket = new ServerSocket(port); |
| } |
| |
| String msg = "Daemon running on: " //$NON-NLS-1$ |
| + ServerAttributes.getHostName() + ", port: " //$NON-NLS-1$ |
| + port; |
| |
| System.out.println(msg); |
| logMessage(msg); |
| } catch (UnknownHostException e) { |
| String msg = "Networking problem, can't resolve local host"; //$NON-NLS-1$ |
| System.err.println(msg); |
| // don't display exceptions 193426 |
| //e.printStackTrace(); |
| logError(msg, e); |
| System.exit(-1); |
| } catch (IOException e) { |
| String msg = "Failure to create ServerSocket"; //$NON-NLS-1$ |
| System.err.println(msg); |
| // don't display exceptions 193426 |
| //e.printStackTrace(); |
| logError(msg, e); |
| System.exit(-1); |
| } |
| } |
| } |
| |
| private void logMessage(String msg){ |
| if (_logDaemon && _logFile != null){ |
| try { |
| _logFile.writeBytes((new Date()).toString() + ": "); //$NON-NLS-1$ |
| _logFile.writeBytes(msg); |
| _logFile.writeBytes(System.getProperty("line.separator")); //$NON-NLS-1$ |
| } |
| catch (IOException e) |
| { |
| } |
| } |
| } |
| |
| private void logError(String msg, Throwable e){ |
| if (_logDaemon && _logFile != null){ |
| try { |
| _logFile.writeBytes((new Date()).toString() + ": "); //$NON-NLS-1$ |
| _logFile.writeBytes(msg); |
| _logFile.writeBytes(System.getProperty("line.separator")); //$NON-NLS-1$ |
| |
| StackTraceElement[] stack = e.getStackTrace(); |
| for (int i = 0;i<stack.length;i++){ |
| _logFile.writeBytes(stack[i].toString()); |
| _logFile.writeBytes(System.getProperty("line.separator")); //$NON-NLS-1$ |
| } |
| _logFile.writeBytes(System.getProperty("line.separator")); //$NON-NLS-1$ |
| } |
| catch (IOException ex) |
| { |
| } |
| } |
| } |
| |
| |
| /** |
| * Return the connection listener for the specified port if there is one |
| * |
| * @param port |
| * the port |
| * @return the listener associated with the port |
| */ |
| protected ConnectionListener getListenerForPort(String port) { |
| for (int i = 0; i < _connections.size(); i++) { |
| ConnectionListener listener = (ConnectionListener) _connections |
| .get(i); |
| if (listener.getServerPort().equals(port)) { |
| return listener; |
| } |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Run the daemon |
| */ |
| public void run() { |
| while (true) { |
| try { |
| boolean connectionOkay = true; |
| Socket newSocket = _serverSocket.accept(); |
| if (_sslProperties.usingSSL()) { |
| |
| SSLSocket sslSocket = (SSLSocket) newSocket; |
| sslSocket |
| .addHandshakeCompletedListener(new HandshakeCompletedListener() { |
| |
| public void handshakeCompleted( |
| HandshakeCompletedEvent event) { |
| String msg = "handshake completed"; //$NON-NLS-1$ |
| System.out.println(msg); |
| logMessage(msg); |
| } |
| |
| }); |
| SSLSession session = sslSocket.getSession(); |
| if (session == null) { |
| String msg = "handshake failed"; //$NON-NLS-1$ |
| System.out.println(msg); |
| logMessage(msg); |
| sslSocket.close(); |
| connectionOkay = false; |
| } |
| } |
| if (connectionOkay) { |
| ConnectionListener listener = new ConnectionListener( |
| newSocket); |
| listener.start(); |
| _connections.add(listener); |
| } |
| } catch (IOException ioe) { |
| String msg = "Server: error initializing socket: " + ioe; //$NON-NLS-1$ |
| System.err.println(msg); |
| logMessage(msg); |
| System.exit(-1); |
| } |
| } |
| } |
| |
| /** |
| * Entry point into the DataStore daemon |
| * |
| * @param args |
| * the port for the daemon to run on (default is 4075). |
| * Optionally, the second arg specifies whether to use SSL or |
| * not. an optional second arg can be used to specify the port |
| * range of servers that get launched |
| */ |
| public static void main(String args[]) { |
| if (args.length == 2) { |
| ServerLauncher theServer = new ServerLauncher(args[0], args[1]); |
| theServer.start(); |
| } else if (args.length == 1) { |
| ServerLauncher theServer = new ServerLauncher(args[0]); |
| theServer.start(); |
| } else { |
| ServerLauncher theServer = new ServerLauncher(); |
| theServer.start(); |
| } |
| } |
| } |