0N/A/*
3261N/A * Copyright (c) 2002, 2010, Oracle and/or its affiliates. All rights reserved.
0N/A * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
0N/A *
0N/A * This code is free software; you can redistribute it and/or modify it
0N/A * under the terms of the GNU General Public License version 2 only, as
0N/A * published by the Free Software Foundation.
0N/A *
0N/A * This code is distributed in the hope that it will be useful, but WITHOUT
0N/A * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
0N/A * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
0N/A * version 2 for more details (a copy is included in the LICENSE file that
0N/A * accompanied this code).
0N/A *
0N/A * You should have received a copy of the GNU General Public License version
0N/A * 2 along with this work; if not, write to the Free Software Foundation,
0N/A * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
0N/A *
2362N/A * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
2362N/A * or visit www.oracle.com if you need additional information or have any
2362N/A * questions.
0N/A */
0N/A
0N/Aimport java.net.*;
0N/Aimport java.io.*;
0N/Aimport java.nio.*;
0N/Aimport java.nio.channels.*;
0N/Aimport sun.net.www.MessageHeader;
0N/Aimport java.util.*;
0N/A
0N/A/**
0N/A * This class implements a simple HTTP server. It uses multiple threads to
0N/A * handle connections in parallel, and also multiple connections/requests
0N/A * can be handled per thread.
0N/A * <p>
0N/A * It must be instantiated with a {@link HttpCallback} object to which
0N/A * requests are given and must be handled.
0N/A * <p>
0N/A * Simple synchronization between the client(s) and server can be done
0N/A * using the {@link #waitForCondition(String)}, {@link #setCondition(String)} and
0N/A * {@link #rendezvous(String,int)} methods.
0N/A *
0N/A * NOTE NOTE NOTE NOTE NOTE NOTE NOTE
0N/A *
0N/A * If changes are made here, please sure they are propagated to
0N/A * the HTTPS equivalent in the JSSE regression test suite.
0N/A *
0N/A * NOTE NOTE NOTE NOTE NOTE NOTE NOTE
0N/A */
0N/A
0N/Apublic class HttpServer {
0N/A
0N/A ServerSocketChannel schan;
0N/A int threads;
0N/A int cperthread;
0N/A HttpCallback cb;
0N/A Server[] servers;
0N/A
0N/A /**
0N/A * Create a <code>HttpServer<code> instance with the specified callback object
0N/A * for handling requests. One thread is created to handle requests,
0N/A * and up to ten TCP connections will be handled simultaneously.
0N/A * @param cb the callback object which is invoked to handle each
0N/A * incoming request
0N/A */
0N/A
0N/A public HttpServer (HttpCallback cb) throws IOException {
0N/A this (cb, 1, 10, 0);
0N/A }
0N/A
0N/A /**
0N/A * Create a <code>HttpServer<code> instance with the specified number of
0N/A * threads and maximum number of connections per thread. This functions
0N/A * the same as the 4 arg constructor, where the port argument is set to zero.
0N/A * @param cb the callback object which is invoked to handle each
0N/A * incoming request
0N/A * @param threads the number of threads to create to handle requests
0N/A * in parallel
0N/A * @param cperthread the number of simultaneous TCP connections to
0N/A * handle per thread
0N/A */
0N/A
0N/A public HttpServer (HttpCallback cb, int threads, int cperthread)
0N/A throws IOException {
0N/A this (cb, threads, cperthread, 0);
0N/A }
0N/A
0N/A /**
0N/A * Create a <code>HttpServer<code> instance with the specified number
0N/A * of threads and maximum number of connections per thread and running on
0N/A * the specified port. The specified number of threads are created to
0N/A * handle incoming requests, and each thread is allowed
0N/A * to handle a number of simultaneous TCP connections.
0N/A * @param cb the callback object which is invoked to handle
0N/A * each incoming request
0N/A * @param threads the number of threads to create to handle
0N/A * requests in parallel
0N/A * @param cperthread the number of simultaneous TCP connections
0N/A * to handle per thread
0N/A * @param port the port number to bind the server to. <code>Zero</code>
0N/A * means choose any free port.
0N/A */
0N/A
0N/A public HttpServer (HttpCallback cb, int threads, int cperthread, int port)
0N/A throws IOException {
0N/A schan = ServerSocketChannel.open ();
0N/A InetSocketAddress addr = new InetSocketAddress (port);
0N/A schan.socket().bind (addr);
0N/A this.threads = threads;
0N/A this.cb = cb;
0N/A this.cperthread = cperthread;
0N/A servers = new Server [threads];
0N/A for (int i=0; i<threads; i++) {
0N/A servers[i] = new Server (cb, schan, cperthread);
0N/A servers[i].start();
0N/A }
0N/A }
0N/A
0N/A /** Tell all threads in the server to exit within 5 seconds.
0N/A * This is an abortive termination. Just prior to the thread exiting
0N/A * all channels in that thread waiting to be closed are forceably closed.
0N/A */
0N/A
0N/A public void terminate () {
0N/A for (int i=0; i<threads; i++) {
0N/A servers[i].terminate ();
0N/A }
0N/A }
0N/A
0N/A /**
0N/A * return the local port number to which the server is bound.
0N/A * @return the local port number
0N/A */
0N/A
0N/A public int getLocalPort () {
0N/A return schan.socket().getLocalPort ();
0N/A }
0N/A
0N/A static class Server extends Thread {
0N/A
0N/A ServerSocketChannel schan;
0N/A Selector selector;
0N/A SelectionKey listenerKey;
0N/A SelectionKey key; /* the current key being processed */
0N/A HttpCallback cb;
0N/A ByteBuffer consumeBuffer;
0N/A int maxconn;
0N/A int nconn;
0N/A ClosedChannelList clist;
0N/A boolean shutdown;
0N/A
0N/A Server (HttpCallback cb, ServerSocketChannel schan, int maxconn) {
0N/A this.schan = schan;
0N/A this.maxconn = maxconn;
0N/A this.cb = cb;
0N/A nconn = 0;
0N/A consumeBuffer = ByteBuffer.allocate (512);
0N/A clist = new ClosedChannelList ();
0N/A try {
0N/A selector = Selector.open ();
0N/A schan.configureBlocking (false);
0N/A listenerKey = schan.register (selector, SelectionKey.OP_ACCEPT);
0N/A } catch (IOException e) {
0N/A System.err.println ("Server could not start: " + e);
0N/A }
0N/A }
0N/A
0N/A /* Stop the thread as soon as possible */
0N/A public synchronized void terminate () {
0N/A shutdown = true;
0N/A }
0N/A
0N/A public void run () {
0N/A try {
0N/A while (true) {
0N/A selector.select (1000);
0N/A Set selected = selector.selectedKeys();
0N/A Iterator iter = selected.iterator();
0N/A while (iter.hasNext()) {
0N/A key = (SelectionKey)iter.next();
0N/A if (key.equals (listenerKey)) {
0N/A SocketChannel sock = schan.accept ();
0N/A if (sock == null) {
0N/A /* false notification */
0N/A iter.remove();
0N/A continue;
0N/A }
0N/A sock.configureBlocking (false);
0N/A sock.register (selector, SelectionKey.OP_READ);
0N/A nconn ++;
2612N/A System.out.println("SERVER: new connection. chan[" + sock + "]");
0N/A if (nconn == maxconn) {
0N/A /* deregister */
0N/A listenerKey.cancel ();
0N/A listenerKey = null;
0N/A }
0N/A } else {
0N/A if (key.isReadable()) {
0N/A boolean closed;
0N/A SocketChannel chan = (SocketChannel) key.channel();
2612N/A System.out.println("SERVER: connection readable. chan[" + chan + "]");
0N/A if (key.attachment() != null) {
2612N/A System.out.println("Server: comsume");
0N/A closed = consume (chan);
0N/A } else {
0N/A closed = read (chan, key);
0N/A }
0N/A if (closed) {
0N/A chan.close ();
0N/A key.cancel ();
0N/A if (nconn == maxconn) {
0N/A listenerKey = schan.register (selector, SelectionKey.OP_ACCEPT);
0N/A }
0N/A nconn --;
0N/A }
0N/A }
0N/A }
0N/A iter.remove();
0N/A }
0N/A clist.check();
0N/A if (shutdown) {
0N/A clist.terminate ();
0N/A return;
0N/A }
0N/A }
0N/A } catch (IOException e) {
0N/A System.out.println ("Server exception: " + e);
0N/A // TODO finish
0N/A }
0N/A }
0N/A
0N/A /* read all the data off the channel without looking at it
0N/A * return true if connection closed
0N/A */
0N/A boolean consume (SocketChannel chan) {
0N/A try {
0N/A consumeBuffer.clear ();
0N/A int c = chan.read (consumeBuffer);
0N/A if (c == -1)
0N/A return true;
0N/A } catch (IOException e) {
0N/A return true;
0N/A }
0N/A return false;
0N/A }
0N/A
0N/A /* return true if the connection is closed, false otherwise */
0N/A
0N/A private boolean read (SocketChannel chan, SelectionKey key) {
0N/A HttpTransaction msg;
0N/A boolean res;
0N/A try {
0N/A InputStream is = new BufferedInputStream (new NioInputStream (chan));
0N/A String requestline = readLine (is);
0N/A MessageHeader mhead = new MessageHeader (is);
0N/A String clen = mhead.findValue ("Content-Length");
0N/A String trferenc = mhead.findValue ("Transfer-Encoding");
0N/A String data = null;
0N/A if (trferenc != null && trferenc.equals ("chunked"))
0N/A data = new String (readChunkedData (is));
0N/A else if (clen != null)
0N/A data = new String (readNormalData (is, Integer.parseInt (clen)));
0N/A String[] req = requestline.split (" ");
0N/A if (req.length < 2) {
0N/A /* invalid request line */
0N/A return false;
0N/A }
0N/A String cmd = req[0];
0N/A URI uri = null;
0N/A try {
0N/A uri = new URI (req[1]);
0N/A msg = new HttpTransaction (this, cmd, uri, mhead, data, null, key);
0N/A cb.request (msg);
0N/A } catch (URISyntaxException e) {
0N/A System.err.println ("Invalid URI: " + e);
0N/A msg = new HttpTransaction (this, cmd, null, null, null, null, key);
0N/A msg.sendResponse (501, "Whatever");
0N/A }
0N/A res = false;
0N/A } catch (IOException e) {
0N/A res = true;
0N/A }
0N/A return res;
0N/A }
0N/A
0N/A byte[] readNormalData (InputStream is, int len) throws IOException {
0N/A byte [] buf = new byte [len];
0N/A int c, off=0, remain=len;
0N/A while (remain > 0 && ((c=is.read (buf, off, remain))>0)) {
0N/A remain -= c;
0N/A off += c;
0N/A }
0N/A return buf;
0N/A }
0N/A
0N/A private void readCRLF(InputStream is) throws IOException {
0N/A int cr = is.read();
0N/A int lf = is.read();
0N/A
0N/A if (((cr & 0xff) != 0x0d) ||
0N/A ((lf & 0xff) != 0x0a)) {
0N/A throw new IOException(
0N/A "Expected <CR><LF>: got '" + cr + "/" + lf + "'");
0N/A }
0N/A }
0N/A
0N/A byte[] readChunkedData (InputStream is) throws IOException {
0N/A LinkedList l = new LinkedList ();
0N/A int total = 0;
0N/A for (int len=readChunkLen(is); len!=0; len=readChunkLen(is)) {
0N/A l.add (readNormalData(is, len));
0N/A total += len;
0N/A readCRLF(is); // CRLF at end of chunk
0N/A }
0N/A readCRLF(is); // CRLF at end of Chunked Stream.
0N/A byte[] buf = new byte [total];
0N/A Iterator i = l.iterator();
0N/A int x = 0;
0N/A while (i.hasNext()) {
0N/A byte[] b = (byte[])i.next();
0N/A System.arraycopy (b, 0, buf, x, b.length);
0N/A x += b.length;
0N/A }
0N/A return buf;
0N/A }
0N/A
0N/A private int readChunkLen (InputStream is) throws IOException {
0N/A int c, len=0;
0N/A boolean done=false, readCR=false;
0N/A while (!done) {
0N/A c = is.read ();
0N/A if (c == '\n' && readCR) {
0N/A done = true;
0N/A } else {
0N/A if (c == '\r' && !readCR) {
0N/A readCR = true;
0N/A } else {
0N/A int x=0;
0N/A if (c >= 'a' && c <= 'f') {
0N/A x = c - 'a' + 10;
0N/A } else if (c >= 'A' && c <= 'F') {
0N/A x = c - 'A' + 10;
0N/A } else if (c >= '0' && c <= '9') {
0N/A x = c - '0';
0N/A }
0N/A len = len * 16 + x;
0N/A }
0N/A }
0N/A }
0N/A return len;
0N/A }
0N/A
0N/A private String readLine (InputStream is) throws IOException {
0N/A boolean done=false, readCR=false;
0N/A byte[] b = new byte [512];
0N/A int c, l = 0;
0N/A
0N/A while (!done) {
0N/A c = is.read ();
0N/A if (c == '\n' && readCR) {
0N/A done = true;
0N/A } else {
0N/A if (c == '\r' && !readCR) {
0N/A readCR = true;
0N/A } else {
0N/A b[l++] = (byte)c;
0N/A }
0N/A }
0N/A }
0N/A return new String (b);
0N/A }
0N/A
0N/A /** close the channel associated with the current key by:
0N/A * 1. shutdownOutput (send a FIN)
0N/A * 2. mark the key so that incoming data is to be consumed and discarded
0N/A * 3. After a period, close the socket
0N/A */
0N/A
0N/A synchronized void orderlyCloseChannel (SelectionKey key) throws IOException {
0N/A SocketChannel ch = (SocketChannel)key.channel ();
2612N/A System.out.println("SERVER: orderlyCloseChannel chan[" + ch + "]");
0N/A ch.socket().shutdownOutput();
0N/A key.attach (this);
0N/A clist.add (key);
0N/A }
0N/A
0N/A synchronized void abortiveCloseChannel (SelectionKey key) throws IOException {
0N/A SocketChannel ch = (SocketChannel)key.channel ();
2612N/A System.out.println("SERVER: abortiveCloseChannel chan[" + ch + "]");
2612N/A
0N/A Socket s = ch.socket ();
0N/A s.setSoLinger (true, 0);
0N/A ch.close();
0N/A }
0N/A }
0N/A
0N/A
0N/A /**
0N/A * Implements blocking reading semantics on top of a non-blocking channel
0N/A */
0N/A
0N/A static class NioInputStream extends InputStream {
0N/A SocketChannel channel;
0N/A Selector selector;
0N/A ByteBuffer chanbuf;
0N/A SelectionKey key;
0N/A int available;
0N/A byte[] one;
0N/A boolean closed;
0N/A ByteBuffer markBuf; /* reads may be satisifed from this buffer */
0N/A boolean marked;
0N/A boolean reset;
0N/A int readlimit;
0N/A
0N/A public NioInputStream (SocketChannel chan) throws IOException {
0N/A this.channel = chan;
0N/A selector = Selector.open();
0N/A chanbuf = ByteBuffer.allocate (1024);
0N/A key = chan.register (selector, SelectionKey.OP_READ);
0N/A available = 0;
0N/A one = new byte[1];
0N/A closed = marked = reset = false;
0N/A }
0N/A
0N/A public synchronized int read (byte[] b) throws IOException {
0N/A return read (b, 0, b.length);
0N/A }
0N/A
0N/A public synchronized int read () throws IOException {
0N/A return read (one, 0, 1);
0N/A }
0N/A
0N/A public synchronized int read (byte[] b, int off, int srclen) throws IOException {
0N/A
0N/A int canreturn, willreturn;
0N/A
0N/A if (closed)
0N/A return -1;
0N/A
0N/A if (reset) { /* satisfy from markBuf */
0N/A canreturn = markBuf.remaining ();
0N/A willreturn = canreturn>srclen ? srclen : canreturn;
0N/A markBuf.get(b, off, willreturn);
0N/A if (canreturn == willreturn) {
0N/A reset = false;
0N/A }
0N/A } else { /* satisfy from channel */
0N/A canreturn = available();
0N/A if (canreturn == 0) {
0N/A block ();
0N/A canreturn = available();
0N/A }
0N/A willreturn = canreturn>srclen ? srclen : canreturn;
0N/A chanbuf.get(b, off, willreturn);
0N/A available -= willreturn;
0N/A
0N/A if (marked) { /* copy into markBuf */
0N/A try {
0N/A markBuf.put (b, off, willreturn);
0N/A } catch (BufferOverflowException e) {
0N/A marked = false;
0N/A }
0N/A }
0N/A }
0N/A return willreturn;
0N/A }
0N/A
0N/A public synchronized int available () throws IOException {
0N/A if (closed)
0N/A throw new IOException ("Stream is closed");
0N/A
0N/A if (reset)
0N/A return markBuf.remaining();
0N/A
0N/A if (available > 0)
0N/A return available;
0N/A
0N/A chanbuf.clear ();
0N/A available = channel.read (chanbuf);
0N/A if (available > 0)
0N/A chanbuf.flip();
0N/A else if (available == -1)
0N/A throw new IOException ("Stream is closed");
0N/A return available;
0N/A }
0N/A
0N/A /**
0N/A * block() only called when available==0 and buf is empty
0N/A */
0N/A private synchronized void block () throws IOException {
0N/A //assert available == 0;
0N/A int n = selector.select ();
0N/A //assert n == 1;
0N/A selector.selectedKeys().clear();
0N/A available ();
0N/A }
0N/A
0N/A public void close () throws IOException {
0N/A if (closed)
0N/A return;
0N/A channel.close ();
0N/A closed = true;
0N/A }
0N/A
0N/A public synchronized void mark (int readlimit) {
0N/A if (closed)
0N/A return;
0N/A this.readlimit = readlimit;
0N/A markBuf = ByteBuffer.allocate (readlimit);
0N/A marked = true;
0N/A reset = false;
0N/A }
0N/A
0N/A public synchronized void reset () throws IOException {
0N/A if (closed )
0N/A return;
0N/A if (!marked)
0N/A throw new IOException ("Stream not marked");
0N/A marked = false;
0N/A reset = true;
0N/A markBuf.flip ();
0N/A }
0N/A }
0N/A
0N/A static class NioOutputStream extends OutputStream {
0N/A SocketChannel channel;
0N/A ByteBuffer buf;
0N/A SelectionKey key;
0N/A Selector selector;
0N/A boolean closed;
0N/A byte[] one;
0N/A
0N/A public NioOutputStream (SocketChannel channel) throws IOException {
0N/A this.channel = channel;
0N/A selector = Selector.open ();
0N/A key = channel.register (selector, SelectionKey.OP_WRITE);
0N/A closed = false;
0N/A one = new byte [1];
0N/A }
0N/A
0N/A public synchronized void write (int b) throws IOException {
0N/A one[0] = (byte)b;
0N/A write (one, 0, 1);
0N/A }
0N/A
0N/A public synchronized void write (byte[] b) throws IOException {
0N/A write (b, 0, b.length);
0N/A }
0N/A
0N/A public synchronized void write (byte[] b, int off, int len) throws IOException {
0N/A if (closed)
0N/A throw new IOException ("stream is closed");
0N/A
0N/A buf = ByteBuffer.allocate (len);
0N/A buf.put (b, off, len);
0N/A buf.flip ();
0N/A int n;
0N/A while ((n = channel.write (buf)) < len) {
0N/A len -= n;
0N/A if (len == 0)
0N/A return;
0N/A selector.select ();
0N/A selector.selectedKeys().clear ();
0N/A }
0N/A }
0N/A
0N/A public void close () throws IOException {
0N/A if (closed)
0N/A return;
0N/A channel.close ();
0N/A closed = true;
0N/A }
0N/A }
0N/A
0N/A /**
0N/A * Utilities for synchronization. A condition is
0N/A * identified by a string name, and is initialized
0N/A * upon first use (ie. setCondition() or waitForCondition()). Threads
0N/A * are blocked until some thread calls (or has called) setCondition() for the same
0N/A * condition.
0N/A * <P>
0N/A * A rendezvous built on a condition is also provided for synchronizing
0N/A * N threads.
0N/A */
0N/A
0N/A private static HashMap conditions = new HashMap();
0N/A
0N/A /*
0N/A * Modifiable boolean object
0N/A */
0N/A private static class BValue {
0N/A boolean v;
0N/A }
0N/A
0N/A /*
0N/A * Modifiable int object
0N/A */
0N/A private static class IValue {
0N/A int v;
0N/A IValue (int i) {
0N/A v =i;
0N/A }
0N/A }
0N/A
0N/A
0N/A private static BValue getCond (String condition) {
0N/A synchronized (conditions) {
0N/A BValue cond = (BValue) conditions.get (condition);
0N/A if (cond == null) {
0N/A cond = new BValue();
0N/A conditions.put (condition, cond);
0N/A }
0N/A return cond;
0N/A }
0N/A }
0N/A
0N/A /**
0N/A * Set the condition to true. Any threads that are currently blocked
0N/A * waiting on the condition, will be unblocked and allowed to continue.
0N/A * Threads that subsequently call waitForCondition() will not block.
0N/A * If the named condition did not exist prior to the call, then it is created
0N/A * first.
0N/A */
0N/A
0N/A public static void setCondition (String condition) {
0N/A BValue cond = getCond (condition);
0N/A synchronized (cond) {
0N/A if (cond.v) {
0N/A return;
0N/A }
0N/A cond.v = true;
0N/A cond.notifyAll();
0N/A }
0N/A }
0N/A
0N/A /**
0N/A * If the named condition does not exist, then it is created and initialized
0N/A * to false. If the condition exists or has just been created and its value
0N/A * is false, then the thread blocks until another thread sets the condition.
0N/A * If the condition exists and is already set to true, then this call returns
0N/A * immediately without blocking.
0N/A */
0N/A
0N/A public static void waitForCondition (String condition) {
0N/A BValue cond = getCond (condition);
0N/A synchronized (cond) {
0N/A if (!cond.v) {
0N/A try {
0N/A cond.wait();
0N/A } catch (InterruptedException e) {}
0N/A }
0N/A }
0N/A }
0N/A
0N/A /* conditions must be locked when accessing this */
0N/A static HashMap rv = new HashMap();
0N/A
0N/A /**
0N/A * Force N threads to rendezvous (ie. wait for each other) before proceeding.
0N/A * The first thread(s) to call are blocked until the last
0N/A * thread makes the call. Then all threads continue.
0N/A * <p>
0N/A * All threads that call with the same condition name, must use the same value
0N/A * for N (or the results may be not be as expected).
0N/A * <P>
0N/A * Obviously, if fewer than N threads make the rendezvous then the result
0N/A * will be a hang.
0N/A */
0N/A
0N/A public static void rendezvous (String condition, int N) {
0N/A BValue cond;
0N/A IValue iv;
0N/A String name = "RV_"+condition;
0N/A
0N/A /* get the condition */
0N/A
0N/A synchronized (conditions) {
0N/A cond = (BValue)conditions.get (name);
0N/A if (cond == null) {
0N/A /* we are first caller */
0N/A if (N < 2) {
0N/A throw new RuntimeException ("rendezvous must be called with N >= 2");
0N/A }
0N/A cond = new BValue ();
0N/A conditions.put (name, cond);
0N/A iv = new IValue (N-1);
0N/A rv.put (name, iv);
0N/A } else {
0N/A /* already initialised, just decrement the counter */
0N/A iv = (IValue) rv.get (name);
0N/A iv.v --;
0N/A }
0N/A }
0N/A
0N/A if (iv.v > 0) {
0N/A waitForCondition (name);
0N/A } else {
0N/A setCondition (name);
0N/A synchronized (conditions) {
0N/A clearCondition (name);
0N/A rv.remove (name);
0N/A }
0N/A }
0N/A }
0N/A
0N/A /**
0N/A * If the named condition exists and is set then remove it, so it can
0N/A * be re-initialized and used again. If the condition does not exist, or
0N/A * exists but is not set, then the call returns without doing anything.
0N/A * Note, some higher level synchronization
0N/A * may be needed between clear and the other operations.
0N/A */
0N/A
0N/A public static void clearCondition(String condition) {
0N/A BValue cond;
0N/A synchronized (conditions) {
0N/A cond = (BValue) conditions.get (condition);
0N/A if (cond == null) {
0N/A return;
0N/A }
0N/A synchronized (cond) {
0N/A if (cond.v) {
0N/A conditions.remove (condition);
0N/A }
0N/A }
0N/A }
0N/A }
0N/A}