/*
* WebServer.java
*
* A simple web server, called YServer.
*
* Supports multiple users, requests for file resources under a
* public directory root, GET and POST HTTP methods, MIME types,
* and loading application settings from an initialization file.
* It also supports uploading files to a specified directory.
*
* Instead of using CGI scripts or another similar technology,
* it is possible to override the 'ReqWorker' class in order to
* extend the functionality of the server. Check this class for
* the two functions that are intented to be overridden. In this
* case, a line in class 'WServer' must be adjusted to create an
* object of the new overriding subclass (check 'WServer'). The
* class 'WebServer', which is the main class of the application,
* can be overridden as well, or substituted altogether.
*
* Yannis Stavrakas (c) 2001
*/
import java.net.*;
import java.io.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.text.DateFormat;
import java.util.Vector;
import java.util.Locale;
import java.util.Properties;
import java.util.Calendar;
import java.util.Date;
import java.util.StringTokenizer;
import java.util.NoSuchElementException;
/********************************************************************
*********************** UTILITY CLASSES ************************
********************************************************************/
//===================================================================
// Preferences
//
// A general-puprose class
//
// Keeps persistent options of the application.
// Default values for options (used in case preferences file is
// not created yet) can be set in method setDef().
//===================================================================
class Preferences {
private String file; //the full pathname of file
private String header; //header in file
//program options that persist
private Properties defPref = new Properties(); //default values
private Properties pref = new Properties(defPref); //user defined values
//initializers
public Preferences(String file, String header) {
this.file = (new File(file)).getAbsolutePath(); //convert to absolute path
this.header = header;
setDef();
pref.clear();
load();
}
//set default values
private void setDef() {
defPref.clear();
//default values of WebServer class variables
defPref.put("port", "80"); //socket port
defPref.put("max_requests", "50"); //max concurrent requests
defPref.put("log_mode", "on"); //screen and filelog msgs on/off: on, off, screen, file
defPref.put("debug_mode", "off"); //screen verbose msgs on/off (in case log_mode=on/screen)
defPref.put("read_request_timeout", "10000"); //timeout for reading requests in millisecs, 0 for infinite
defPref.put("main_html_dir", "./public"); //main HTML (public) directory
defPref.put("def_html_file", "index.html"); //default HTML page
defPref.put("upload_feature", "off"); //allow uploading: on, off
defPref.put("upload_dir", "./uploads"); //directory for storing uploaded files
defPref.put("upload_max_size", "0"); //max number of bytes per upload (0 means no limit)
defPref.put("log_file_path", "./server.log"); //log file path
defPref.put("max_log_size", "50000"); //max log size in bytes
defPref.put("max_screen_lines", "100"); //max screen msg lines
//default window size and position
int w = 400; //application window width
int h = 450; //application window height
int x = 200; //application window top X
int y = 200; //application window top Y
defPref.put("win_width", String.valueOf(w));
defPref.put("win_height", String.valueOf(h));
defPref.put("win_topLeftX", String.valueOf(x));
defPref.put("win_topLeftY", String.valueOf(y));
}
//restore defaults
public void restoreDef() {
pref.clear(); //defPref becomes active
}
//get and set
public void setPref(String name, String val) {
pref.put(name, val);
}
public String getPref(String name) {
return pref.getProperty(name); //null if not there
}
//load and save
public void load() {
try {
FileInputStream in = new FileInputStream(file);
pref.load(in);
}
catch(IOException e) {
//the first time, preferences file will not exist
}
}
public void store() {
try {
FileOutputStream out = new FileOutputStream(file);
pref.store(out, header);
}
catch(IOException e) {
JOptionPane.showMessageDialog
(WebServer.win, //when 'store' is called, 'win' must not be null
"Unable to store user preferences in " + file + ": " + e,
"Error",
JOptionPane.ERROR_MESSAGE);
}
}
}
//===================================================================
// VectorMap
//
// A general-purpose class.
//
// A Vector implementation that maps keys to values. Each key is
// mapped to one value, and duplicate keys are allowed. In contrast
// to hash table, this class is intented to be used for sequential
// access of its key elements (commonly in a loop).
//===================================================================
class VectorMap {
private Vector keys;
private Vector vals;
//initializers
public VectorMap(int initialCapacity) {
//capacity increment is zero
keys = new Vector(initialCapacity);
vals = new Vector(initialCapacity);
}
public VectorMap(int initialCapacity, int capacityIncrement) {
//capacity increment specifies step that vector grows
keys = new Vector(initialCapacity, capacityIncrement);
vals = new Vector(initialCapacity, capacityIncrement);
}
//class info methods
public int capacity() {
return keys.capacity();
}
public int size() {
return keys.size();
}
public boolean isEmpty() {
return keys.isEmpty();
}
//insert, remove, query methods
public void add(Object key, Object val) {
//may fail if capacity = size and increment is 0
keys.add(key);
vals.add(val);
}
public void remove(int idx) {
//may fail if idx out of size() bounds
try {
keys.remove(idx);
vals.remove(idx);
} catch(ArrayIndexOutOfBoundsException e) {
}
}
public void clear() {
keys.clear();
vals.clear();
}
public int indexOfKey(Object key, int startIdx) {
//returns -1 if key not exist
return keys.indexOf(key, startIdx);
}
public int indexOfVal(Object val, int startIdx) {
//returns -1 if val not exist
return vals.indexOf(val, startIdx);
}
public Object getKey(int idx) {
//returns null if idx out of size() bounds
Object key;
try {
key = keys.get(idx);
} catch(ArrayIndexOutOfBoundsException e) {
key = null;
}
return key;
}
public Object getVal(int idx) {
//returns null if idx out of size() bounds
Object val;
try {
val = vals.get(idx);
} catch(ArrayIndexOutOfBoundsException e) {
val = null;
}
return val;
}
}
//===================================================================
// FileLog
//
// A general-purpose class.
//
// Manages a log file, writing messages about
// the progress of the application.
//===================================================================
class FileLog {
private String logPath; //path of log file
private long maxSize; //log max size in bytes
private boolean logOn = true; //if false, no msgs at all are written
//initializers
public FileLog(String logPath, long maxSize) {
this.logPath = logPath;
this.maxSize = maxSize;
}
//misc methods
public String getLogPath() {
return logPath;
}
public long getMaxSize() {
return maxSize;
}
public void setMaxSize(long maxSize) {
this.maxSize = maxSize;
}
public long getSize() {
File log = new File(logPath);
return log.length();
}
public void setLog(boolean mode) {
logOn = mode;
}
//write to log methods
public void writeln(String line) { //write line preceded by date / time
if ( ! logOn) return;
write(now() + " > " + line + "\r\n");
}
public void write(String msg) { //plain write
if ( ! logOn) return;
boolean append = true;
if (getSize() + msg.length() > maxSize) append = false;
//if msg.length() > maxSize, msg will still be writen
try {
FileOutputStream out = new FileOutputStream(logPath, append); //truncate?
byte[] buf = msg.getBytes();
out.write(buf);
out.close();
} catch (IOException e) {
WebServer.win.log.warn("Problem writing file " + logPath + ": " + e);
}
}
public void emptyLog() {
try {
FileOutputStream out = new FileOutputStream(logPath, false); //truncate!!!
out.close();
} catch (IOException e) {
WebServer.win.log.warn("Problem truncating file " + logPath + ": " + e);
}
}
public void forceWriteln(String line) { //writeln irrespective of 'logOn' value
boolean old = logOn;
logOn = true;
writeln(line);
logOn = old;
}
//utility methods
private String now() { //current date - time
Calendar rightNow = Calendar.getInstance();
return rightNow.get(Calendar.DAY_OF_MONTH) + "/" +
(rightNow.get(Calendar.MONTH)+1) + "/" + //months from 0 to 11
rightNow.get(Calendar.YEAR) + " " +
rightNow.get(Calendar.HOUR_OF_DAY) + ":" +
rightNow.get(Calendar.MINUTE) + ":" +
rightNow.get(Calendar.SECOND);
}
}
//===================================================================
// ActivityLog
//
// A general-purpose class.
//
// A scrollable text area component that displays messages about
// the progress of the application.
//===================================================================
class ActivityLog extends JScrollPane {
private JTextArea log; //text area where messages are displayed
private int maxLines; //maximum lines in text area
private boolean logOn = true; //if false, no msgs at all are displayed
private boolean debugOn = true; //if false, debug msgs are not displayed
//initializers
public ActivityLog(int maxLines) {
super(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED );
log = new JTextArea("");
setViewportView(log); //scroll pane adds scrolling to text area
this.maxLines = maxLines;
log.setEditable(false); //no editing
log.setLineWrap(false); //no wrapping
}
//activity reporting methods - they all change line afterwards
public void report(String msg) {
echo("-- " + msg);
}
public void warn(String msg) {
echo(" ! " + msg);
}
public void error(String msg) {
echo(" # " + msg);
}
public void debug(String msg) {
if (debugOn) echo("*>" + msg + "<*");
}
public void markStart() {
echo("*** STARTED at " + now() + " ***");
}
public void markSuccess() {
echo("*** COMPLETED at " + now() + " ***");
echo(""); //another new line
}
public void markFailure() {
echo("### ABORDED at " + now() + " ###");
echo(""); //another new line
}
public void echo(String msg) { //displays msg exactly as it is, plus newline
if ( ! logOn) return;
//check if room for new line
if (maxLines <= 0) return;
while (maxLines <= lines()) { //remove oldest lines
try {
log.replaceRange("", 0, log.getLineEndOffset(0));
} catch(javax.swing.text.BadLocationException e) {
clear(); //just try to make room
}
}
//append message and new line
plainEcho(msg + "\n");
}
public void plainEcho(String msg) { //displays msg exactly as it is, no newline
if ( ! logOn) return;
//append message
log.append(msg);
//set caret to end, to force viewing the latest msg
log.setCaretPosition(log.getText().length());
}
public void forceReport(String msg) { //report irrespective of 'logOn' value
boolean old = logOn;
logOn = true;
report(msg);
logOn = old;
}
//misc methods
public void clear() {
log.setText("");
}
public void setMaxLines(int maxLines) {
this.maxLines = maxLines;
}
public int getMaxLines() {
return maxLines;
}
public int lines() {
return log.getLineCount();
}
public void setLog(boolean mode) {
logOn = mode;
}
public void setDebug(boolean mode) {
debugOn = mode;
}
//utility methods
private String now() { //current date - time
Calendar rightNow = Calendar.getInstance();
return rightNow.get(Calendar.DAY_OF_MONTH) + "/" +
(rightNow.get(Calendar.MONTH)+1) + "/" + //months from 0 to 11
rightNow.get(Calendar.YEAR) + " " +
rightNow.get(Calendar.HOUR_OF_DAY) + ":" +
rightNow.get(Calendar.MINUTE) + ":" +
rightNow.get(Calendar.SECOND);
}
}
/********************************************************************
************************ UI CLASSES **************************
********************************************************************/
//===================================================================
// Border
//
// A general-purpose class.
//
// Class that creates a custom border around user interface elements.
//===================================================================
class Border extends Panel {
int top, left, bottom, right; //no of pixels for border
public Border(Component borderMe, int top, int left, int bottom, int right) {
this.top = top;
this.left = left;
this.bottom = bottom;
this.right = right;
setLayout(new BorderLayout());
add(borderMe, "Center");
}
public Insets getInsets() { //overriding the insets
return new Insets(top, left, bottom, right);
}
}
//===================================================================
// WindowEvents
//
// Events to which the application responds to. Implements the
// corresponding actions.
//===================================================================
class WindowEvents extends WindowAdapter {
//close main window
public void windowClosing(WindowEvent evt) {
//must set ControlWin to do nothing on close, otherwise
//it will hide no matter what this method will do.
//this line should be in ControlWin class, but here more clear.
WebServer.win.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
//confirm first
int quit = JOptionPane.showConfirmDialog
(WebServer.win,
"Shutdown " + WebServer.obj.appName + "?",
"Confirm",
JOptionPane.YES_NO_OPTION,
JOptionPane.QUESTION_MESSAGE);
if (quit == JOptionPane.NO_OPTION) return;
//else quit, but first register new window position and size
//in 'preferences' to be used the next time the server runs
WebServer.pref.setPref("win_width", (new Integer(WebServer.win.getWidth())).toString());
WebServer.pref.setPref("win_height", (new Integer(WebServer.win.getHeight())).toString());
WebServer.pref.setPref("win_topLeftX", (new Integer(WebServer.win.getX())).toString());
WebServer.pref.setPref("win_topLeftY", (new Integer(WebServer.win.getY())).toString());
//now quit
evt.getWindow().dispose(); //not necessary here
WebServer.terminate(true);
}
}
//===================================================================
// ControlWin
//
// Main window of the application.
//===================================================================
class ControlWin extends JFrame {
public ActivityLog log; //displays application messages
//initializer
public ControlWin(String appTitle, int maxLogLines) {
setTitle(appTitle);
makeUI(maxLogLines);
addWindowListener(new WindowEvents());
displayWin();
}
//UI methods
private void makeUI(int maxLogLines) {
log = new ActivityLog(maxLogLines);
//main panel
Container cp = getContentPane(); //particularity of Swing's JFrame
cp.setLayout(new BorderLayout(5, 5));
cp.add(new Border(log, 10, 10, 10, 10), "Center");
}
private void displayWin() {
//get window dimensions and position
int w = Integer.parseInt(WebServer.pref.getPref("win_width"));
int h = Integer.parseInt(WebServer.pref.getPref("win_height"));
int x = Integer.parseInt(WebServer.pref.getPref("win_topLeftX"));
int y = Integer.parseInt(WebServer.pref.getPref("win_topLeftY"));
//display window
setBounds(x, y, w, h);
show();
}
}
/********************************************************************
*********************** LOGIC CLASSES *************************
********************************************************************/
//===================================================================
// ReqWorker
//
// Handles a single request. Analyses the request and forwards
// the corresponding response. Supports requests for retrieving
// files (eg. HTML documents), and for uploading files as well.
// Uploading process is as follows: client initiates the upload
// by requesting the reserved resource /upload . Then the server
// sends an upload form and the client can perform the UPLOAD.
//
// It can be used as a base class by subclasses that extend the
// basic FILE_MODE 'respMode' functionality (requests for files)
// to other modes of response, simulating calls to CGI scripts.
//
// This class, for the purpose of sending files to clients, does
// not need the 'queryParam' variable, or method 'parseQueryParam',
// or the call to this method. Consequently, the class 'VectorMap'
// is not needed either. However, all those exist for supporting
// possible subclasses that will need to have the query parameters
// of the request parsed.
//===================================================================
class ReqWorker extends Thread {
protected Socket sock; //socket for particular client request
protected InetAddress clientAddr; //client address
protected int port; //socket port (dynamically allocated)
//input and output streams of socket
InputStream in = null; //raw input stream
PrintWriter out = null; //text output stream
//request parameters (general)
protected String httpHeader = null; //the incoming HTTP message header
protected String method = null; //HTTP method
protected String resURI = null; //requested resource URI (no query string)
protected boolean isUpload = false; //is it a request for upload?
//request parameters (specific either to FILE or to UPLOAD)
protected String reqBody = null; //request body (FILE request, POST method)
protected String queryStr = null; //query string (FILE request)
protected VectorMap queryParam = null; //query parameter name-value pairs (FILE request)
protected String upHeader = null; //header in the upload body (UPLOAD request)
protected String upBoundary = null; //string delimiting uploaded contents (UPLOAD request)
protected String upFilename = null; //local filename of uploaded file (UPLOAD request)
protected long upSize = 0; //bytes written to local uploaded file (UPLOAD request)
//control variables
protected int respCode; //code of response (error-handling)
protected int respMode; //mode of response (response process)
//response variables
protected StringBuffer httpMsg; //HTTP message
protected String status; //HTTP status code
protected String mimeType; //MIME type
protected FileInputStream rbFile; //response body comes from a file
protected String rbString; //response body is a string (alternatively)
protected long rbLength; //no of bytes of response body
//codes of response handle errors, <= 0
static protected final int NORMAL = 0; //request OK, normal response
static protected final int NO_RESOURCE = -1; //requested resource not found
static protected final int REQUEST_ERR = -2; //client request not understood
static protected final int NO_SERVICE = -3; //requested service not supported
static protected final int SERVER_ERR = -4; //unexpected server error
//modes of response, specify process of responding, > 0
static protected final int FILE_MODE = 1; //request for physical file
static protected final int UPLOAD_MODE = 2; //request to upload file
//variables specific to response body in FILE_MODE
protected String resPath = null; //requested file physical path
protected boolean resAccessible = false; //true if resource is accessible
protected String resFileExt = null; //file extension of resource
//variables specific to UPLOAD_MODE
protected final String upInitiate = //not to be changed, users read about it in manual :-)
"/upload"; //..keyword for initiating upload process (sends form)
protected String upAction = "/do-upload"; //keyword for trigering (Form ACTION) the upload process
protected String upAnswer = null; //HTML answer to upload requests
protected String upForm = null; //the upload form, sent to clients that requested an upload
//initializers
public ReqWorker(Socket sock) {
super();
this.sock = sock;
clientAddr = sock.getInetAddress();
port = sock.getPort(); //can be used as request ID
//the upload form
String maxSizeWarning = "";
long maxSize = WebServer.obj.uploadMax; //size limit per upload, 0 means no limit
if (maxSize > 0) maxSizeWarning =
"
The maximum size allowed to upload is " + maxSize + " bytes.\n";
upForm =
"\n\n
\nYServer Upload Form\n\n\n" +
"\n\n\n
\n
Upload File
\nSelect file to upload:\n
\n" +
"\nIf the file is uploaded successfully, the number of bytes saved will be displayed.\n" +
maxSizeWarning +
"
\n\n\n";
}
//run method
public void run() {
//log the request
logRequest();
//get socket input and output streams
try {
//set maximum blocking time in millisecs
sock.setSoTimeout(WebServer.obj.reqTimeout);
//open "raw" input stream to read from client
in = sock.getInputStream();
//open character output stream to write text to client
out = new PrintWriter(sock.getOutputStream());
} catch (IOException e) {
WebServer.win.log.error(port + "-Connection problem: " + e);
WebServer.flog.writeln(port + "-Connection problem: " + e);
return; //communication failure
}
//do the job
analyseRequest();
respond();
}
//request methods: analysing request
protected void analyseRequest() {
respCode = NORMAL;
//get and parse HTTP message header
if ( ! getHTTPHeader()) {
respCode = SERVER_ERR;
return; //no point in proceeding
}
if ( ! parseHTTPHeader()) {
respCode = REQUEST_ERR;
return; //no point in proceeding
}
if ( ! methodSupported()) {
respCode = NO_SERVICE;
return; //no point in proceeding
}
//if request is for UPLOAD (POST method),
//get the new header from request body and save the contents
if (isUpload) {
if ( ! getUploadHeader()) {
respCode = SERVER_ERR;
return; //unable to receive HTTP body, abord
}
if ( ! parseUploadHeader()) {
respCode = REQUEST_ERR;
return; //unable to retrieve the filename, abord
}
if ( ! saveUploadBody()) {
respCode = SERVER_ERR;
return; //unable to upload file, abord
}
}
//If request is for a FILE, get query string parameters.
//Actually, query params ('queryParam' Vector) are not used
//by YServer, but are intented for overriding subclasses.
else {
//if GET, 'queryStr' is already extracted from HTTP header
//but if method is POST, the query string is the HTTP body
if (method.equalsIgnoreCase("POST")) {
if ( ! getRequestBody()) {
respCode = SERVER_ERR;
return; //unable to receive HTTP body, abord
}
getQueryStr(); //extract query string from request body
}
//GET or POST, extract parameters and values from query string
parseQueryParam();
}
//decide the responce mode, depending on request
switchRespMode();
}
protected boolean getHTTPHeader() {
//fill 'httpHeader' variable
try {
//header is separated by one empty line
boolean first = false; //true if a first newline found
//read client request
int charRead;
StringBuffer buf = new StringBuffer();
while ((charRead = in.read()) != -1) { //end of input stream
char ch = (char) charRead;
buf.append(ch);
//look for end of header
if (ch == '\n' && first) break; //second successive newline found
if (ch == '\n') first = true; //first can only be false here
else if ( ! Character.isWhitespace(ch)) first = false;
}
httpHeader = buf.substring(0);
} catch (InterruptedIOException e) { //'reqTimeout' expired
WebServer.win.log.error(port + "-Timeout reading request header: " + e);
WebServer.flog.writeln(port + "-Timeout reading request header: " + e);
return false;
} catch (IOException e) {
WebServer.win.log.error(port + "-Problem receiving header: " + e);
WebServer.flog.writeln(port + "-Problem receiving header: " + e);
return false;
}
//log info
WebServer.win.log.debug(port + "-Request header below:\n" + httpHeader);
return true;
}
protected boolean parseHTTPHeader() {
if (httpHeader == null) return false;
try {
//get first line of header
String reqLine = httpHeader.substring(0, httpHeader.indexOf('\n') + 1); //include '\n'
StringTokenizer parser = new StringTokenizer(reqLine, " ", true);
//consume HTTP request method (GET, POST)
method = parser.nextToken();
String delim = parser.nextToken();
//get the requested resource URI (without the query string)
String rawResURI = parser.nextToken(" ?");
resURI = URLDecoder.decode(rawResURI); //decode resource URI
delim = parser.nextToken(" ?"); //delim is now ? or space
//Request is for upload?
if (resURI.equals(upAction)) isUpload = true;
//if request is for file and method is GET,
//get query string (if there exists one)
if ( ! isUpload && method.equalsIgnoreCase("GET")) {
//method GET: query string is after '?'
String rawQueryStr = ""; //first initialize to empty string
if (delim.compareTo("?") == 0) {
rawQueryStr = parser.nextToken(); //returns space, if ? alone
if (rawQueryStr.equals(" ")) rawQueryStr = "";
}
else {} //else no query string exists
//decode query string
queryStr = URLDecoder.decode(rawQueryStr);
}
} catch (NoSuchElementException e) {
WebServer.win.log.error(port + "-Problem parsing HTTP header: " + e);
WebServer.flog.writeln(port + "-Problem parsing HTTP header: " + e);
return false;
}
//log info
WebServer.win.log.debug(port + "-Request method:" + method);
WebServer.win.log.debug(port + "-Requested resource URI:" + resURI);
WebServer.win.log.debug(port + "-Request type is UPLOAD:" + isUpload);
return true;
}
protected boolean methodSupported() {
//returns true if HTTP request method is supported.
if (method == null) return false;
//GET and POST supported
if (method.equalsIgnoreCase("GET") |
method.equalsIgnoreCase("POST"))
return true;
return false;
}
protected boolean getUploadHeader() {
//fill 'upHeader' variable with the header in UPLOAD request body
//(HTTP header has already been read from the socket input stream)
try {
//header is separated using one empty line
boolean first = false; //true if a first newline found
//read request body
int charRead;
StringBuffer buf = new StringBuffer();
while ((charRead = in.read()) != -1) { //until end of input stream
char ch = (char) charRead;
buf.append(ch);
//look for end of header
if (ch == '\n' && first) break; //second successive newline found
if (ch == '\n') first = true; //first can only be false here
else if ( ! Character.isWhitespace(ch)) first = false;
}
upHeader = buf.substring(0);
} catch (InterruptedIOException e) { //'reqTimeout' expired
WebServer.win.log.error(port + "-Timeout reading request body: " + e);
WebServer.flog.writeln(port + "-Timeout reading request body: " + e);
return false;
} catch (IOException e) {
WebServer.win.log.error(port + "-Problem receiving body: " + e);
WebServer.flog.writeln(port + "-Problem receiving body: " + e);
return false;
}
//log info
WebServer.win.log.debug(port + "-Upload header below:\n" + upHeader);
return true;
}
protected boolean parseUploadHeader() {
String upFileOrig = "";
try { //'try' not really needed, but used for custom error handling
//get upload content demimiter (called boundary), and fill 'upBoundary'
//boundary is the first line of the upload header
int first = 0;
while (first < httpHeader.length() && Character.isWhitespace(httpHeader.charAt(first)))
first += 1; //now 'first' points to first char of boundary value
int lineR = upHeader.indexOf('\r', first); //check for carriage return
int lineN = upHeader.indexOf('\n', first); //check for new line
int line = lineR < lineN? lineR: lineN; //'line' points to first between them
if (first >= line) throw new NoSuchElementException();
upBoundary = upHeader.substring(first, line);
//get the original filename (path) of the uploaded file
String flnm = "filename"; //field name of upload form, should exist in upload header
int idx = upHeader.indexOf(flnm);
if (idx == -1) throw new NoSuchElementException();
idx += flnm.length();
while (idx < upHeader.length() && upHeader.charAt(idx) != '=')
idx += 1; //now 'idx' points to '='
idx += 1;
while (idx < upHeader.length() && upHeader.charAt(idx) != '\"') //filename in quotes!
idx += 1; //now 'idx' points to a ", just before the first char of filename value
int end = idx + 1;
while (end < upHeader.length() && upHeader.charAt(end) != '\"') //filename in quotes!
end += 1; //now 'end' points to a ", just after the last char of filename value
if (end >= upHeader.length()) throw new NoSuchElementException();
upFileOrig = upHeader.substring(idx + 1, end); //get original filename value
//keep only the last part, loose the path
int lastWin = upFileOrig.lastIndexOf('\\');
int lastUnx = upFileOrig.lastIndexOf('/');
int last = lastWin > lastUnx? lastWin: lastUnx;
last += 1; //if -1 it becomes 0, else it points just after last '/' or '\'
if (last >= upFileOrig.length()) throw new NoSuchElementException(); //catches empty filenames
String theName = upFileOrig.substring(last);
//add a timestamp and the socket to the filename to save to
Long timestamp = new Long(System.currentTimeMillis());
upFilename = WebServer.obj.uploadDir + "/" + timestamp + "-" + port + "-" + theName;
} catch (NoSuchElementException e) {
WebServer.win.log.error(port + "-Problem parsing upload header");
WebServer.flog.writeln(port + "-Problem parsing upload header");
return false;
}
//log info
WebServer.win.log.debug(port + "-Upload boundary:" + upBoundary);
WebServer.win.log.debug(port + "-Original filename to upload:" + upFileOrig);
WebServer.win.log.debug(port + "-File uploaded in: " + upFilename);
WebServer.flog.writeln(port + "- " + "UPLOADED in " + upFilename);
return true;
}
protected boolean saveUploadBody() {
//save uploaded contents to file
FileOutputStream save = null;
try {
save = new FileOutputStream(upFilename, false);
upSize = 0; //number of bytes written
long lastWritten = 0; //bytes flushed to file
long maxSize = WebServer.obj.uploadMax; //size limit, 0 means no limit
boolean room = true; //becomes false if more bytes written than allowed
//upBoundary marks the end (itself excluded) of contents to upload.
//It starts at a new line, so check for it just after a newline char.
//Be careful though, because a carriage return '\r' precedes '\n'.
//Those two characters must not be saved as part of the uploaded file!
boolean found = false; //becomes true if boundary matched successfully
int charRead;
while ((charRead = in.read()) != -1) { //until end of input stream
if (charRead != '\r') { //NOTE: the int value of '\r' is 13, while of '\n' is 10
if (maxSize > 0 && upSize > maxSize) room = false; //size limit violated, stop writing
if (room) {
save.write(charRead);
upSize += 1;
}
}
else found = matchBoundary(save, charRead); //boundary follows a '\r\n'
if (upSize - lastWritten > 1000000) { //flush to file every 1MB
save.flush(); //put to file
lastWritten = upSize;
}
if (found) break;
}
save.close();
//upload file probably already created, report problem if any
} catch (InterruptedIOException e) { //'reqTimeout' expired
WebServer.win.log.error(port + "-Timeout uploading: " + e);
WebServer.flog.writeln(port + "-Timeout uploading: " + e);
try {
if (save != null) save.close(); //close file anyway
} catch (IOException i) {
WebServer.win.log.error(port + "-Unable to close file " + upFilename);
WebServer.flog.writeln(port + "-Unable to close file " + upFilename);
}
return false;
} catch (IOException e) {
WebServer.win.log.error(port + "-Problem uploading: " + e);
WebServer.flog.writeln(port + "-Problem uploading: " + e);
try {
if (save != null) save.close(); //close file anyway
} catch (IOException i) {
WebServer.win.log.error(port + "-Unable to close file " + upFilename);
WebServer.flog.writeln(port + "-Unable to close file " + upFilename);
}
return false;
}
return true;
}
protected boolean matchBoundary(FileOutputStream save, int CR)
throws InterruptedIOException, IOException {
//this method calls itself recursively as long as it finds '\r'
//within the limits of the character sequence it tries to compare
//until the boundary is found, or until the match is negative
long maxSize = WebServer.obj.uploadMax; //size limit, 0 means no limit
boolean room = true; //becomes false if more bytes written than allowed
try { //catch thrown exceptions from recursive calls, forward them to 'saveUploadBody'
//check for a newline char after carriage return
int NL = in.read();
if (NL != '\n') { //not the expected sequence
if (maxSize > 0 && upSize > maxSize) room = false; //size limit violated, stop writing
if (room) {
save.write(CR); //save pending '\r'
save.write(NL);
upSize += 2;
}
return false;
}
//a '\r\n' is pending now, if no boundary match it must be saved
//start buffering, to attempt a boundary match
int bSize = upBoundary.length();
int[] waiting = new int[bSize]; //boundary candidate here, instead of saving the chars
StringBuffer buf = new StringBuffer(bSize); //same, but in string form for matching
int idx = 0;
while (idx < bSize) {
waiting[idx] = in.read();
//if another new line, no boundary match, save chars and repeat process
if (waiting[idx] == '\r') {
if (maxSize > 0 && upSize > maxSize) room = false; //size limit violated, stop writing
if (room) {
save.write(CR); //save pending '\r'
save.write(NL); //save pending '\n'
for (int i=0; i < idx; i++)
save.write(waiting[i]);
upSize = upSize + 2 + idx;
}
return matchBoundary(save, waiting[idx]); //recursion
}
else { //advance buffers
char ch = (char) waiting[idx];
buf.append(ch);
idx += 1;
}
}
//'size' chars following '\r\n' buffered now, no '\r' between them
if (upBoundary.equals(buf.toString())) return true;
else {
if (maxSize > 0 && upSize > maxSize) room = false; //size limit violated, stop writing
if (room) {
save.write(CR); //save pending '\r'
save.write(NL); //save pending '\n'
for (int i = 0; i < bSize; i++)
save.write(waiting[i]);
upSize = upSize + 2 + bSize;
}
}
} catch (InterruptedIOException e) { //'reqTimeout' expired
throw e; //to be catched by calling function (eventually by 'saveUploadBody')
} catch (IOException e) {
throw e; //to be catched by calling function (eventually by 'saveUploadBody')
}
return false;
}
protected boolean getRequestBody() {
//If request is for FILE and method is POST, fill 'reqBody' variable
//(HTTP header has already been read from the socket input stream)
try {
//read request body
int charRead;
StringBuffer buf = new StringBuffer();
while ((charRead = in.read()) != -1) { //until end of input stream
char ch = (char) charRead;
buf.append(ch);
//HTTP requests from browsers are not always followed by -1.
//If no more chars to read, assume end of stream is reached!
if (in.available() == 0) break;
}
reqBody = buf.substring(0);
} catch (InterruptedIOException e) { //'reqTimeout' expired
WebServer.win.log.error(port + "-Timeout reading request body: " + e);
WebServer.flog.writeln(port + "-Timeout reading request body: " + e);
return false;
} catch (IOException e) {
WebServer.win.log.error(port + "-Problem receiving body: " + e);
WebServer.flog.writeln(port + "-Problem receiving body: " + e);
return false;
}
//log info
WebServer.win.log.debug(port + "-Request body below:\n" + reqBody);
return true;
}
protected void getQueryStr() {
//fill 'queryStr' by removing possible spaces from the request body
queryStr = ""; //first initialize to empty string
if (reqBody == null) return;
int i = 0;
while (i < reqBody.length() && Character.isWhitespace(reqBody.charAt(i)))
i += 1;
//'i' now points to end, or to first char after spaces
int j = i;
while (j < reqBody.length() && ! Character.isWhitespace(reqBody.charAt(j)))
j += 1;
String rawQueryStr = reqBody.substring(i, j);
//decode query string
queryStr = URLDecoder.decode(rawQueryStr);
}
protected void parseQueryParam() {
//fill 'queryParam': each entry corresponds to an '=' in 'queryStr',
//but only if the param name (left of '=') is not the empty string.
//A pattern '&blabla&' without '=' will not be added in 'queryParam'.
//Note that possible spaces and special chars are inserted encoded!
//'queryStr' must not be null
if (queryStr == null) return; //not null if FILE request (but maybe "")
queryParam = new VectorMap(10, 20);
try {
StringTokenizer parser = new StringTokenizer(queryStr, "&", false);
while (parser.hasMoreTokens()) {
String pair = parser.nextToken();
int idx = pair.indexOf("=");
switch (idx) {
case -1: break; //'pair' is "", or without '=' (not added)
case 0: break; //param name is "" (not added)
//in case idx == pair.length()-1: param value is "" (added)
default: //'pair' normal case: name=value (added)
String name = pair.substring(0, idx);
String value = pair.substring(idx+1);
queryParam.add(name, value);
break;
}
}
} catch (NoSuchElementException ee) {} //not possible
//log info
WebServer.win.log.debug(port + "-Request query string:" + queryStr);
for (int i=0; i < queryParam.size(); i++)
WebServer.win.log.debug(port + "-Query param:" + queryParam.getKey(i)
+ "=" + queryParam.getVal(i));
String qs = "";
if (queryStr != null && ! queryStr.equals("")) qs = "?" + queryStr;
WebServer.flog.writeln(port + "- " + resURI + qs);
}
//-----------------------------------------------------------------------
//Requests for file resources and file uploads are supported.
//This method is intented to be overidden by subclasses.
protected void switchRespMode() {
//At this moment, request parameters have been set and the
//server can decide the type of the response based on request.
if (resURI.equals(upInitiate)) { //client requesting to upload
respMode = UPLOAD_MODE; //in order to use 'upAnswer' for responding
analyzeUploadInitiate();
return;
}
else if (isUpload) { //client is uploading file
respMode = UPLOAD_MODE;
analyzeUploadMode();
return;
}
else { //client requesting for a file resource
respMode = FILE_MODE;
analyzeFileMode();
return;
}
}
//-----------------------------------------------------------------------
protected void analyzeUploadInitiate() {
//if upload feature is off
if ( ! WebServer.obj.uploadOn) {
upAnswer = "Sorry, upload is de-activated.
";
WebServer.win.log.warn(port + "-Upload is de-activated, could not upload file");
WebServer.flog.writeln(port + "-Upload is de-activated, could not upload file");
return;
}
//if directory to download to is not accessible
File theDir = new File(WebServer.obj.uploadDir);
if ( ! theDir.isDirectory() || ! theDir.canWrite()) {
upAnswer = "Server error, cannot upload file!";
WebServer.win.log.error(port + "-Upload directory inaccessible, can not upload file");
WebServer.flog.writeln(port + "-Upload directory inaccessible, can not upload file");
return;
}
//if everything OK send the upload form
upAnswer = upForm;
WebServer.win.log.debug(port + "-Form for file upload sent");
}
protected void analyzeUploadMode() {
long maxSize = WebServer.obj.uploadMax; //size limit per upload, 0 means no limit
File upFile = new File(upFilename);
long size = upFile.length();
if (size != upSize) { //should be the same
WebServer.win.log.error(port + "-Application error, file size mismatch");
WebServer.flog.writeln(port + "-Application error, file size mismatch");
}
//if nothing saved, delete file and inform
if (size == 0) {
upFile.delete();
upAnswer = "No data uploaded!";
WebServer.win.log.warn(port + "-No data uploaded, file deleted");
WebServer.flog.writeln(port + "-No data uploaded, file deleted");
return;
}
//if size limit violated, delete and inform
if (maxSize > 0 && size > maxSize) { //if 'maxSize' is 0, no limit
upFile.delete();
upAnswer = "File to upload exceeds maximum size allowed!";
WebServer.win.log.warn(port + "-Written " + upSize + ", exceeding maximum of " +
maxSize + ", file deleted");
WebServer.flog.writeln(port + "-Written " + upSize + ", exceeding maximum of " +
maxSize + ", file deleted");
return;
}
//inform that file has been uploaded
upAnswer = "File uploaded (" + size + " bytes), thank you.";
WebServer.win.log.debug(port + "-Saved " + size + " bytes uploading file");
}
protected void analyzeFileMode() {
//fill physical file variables
resPath = resPhysPath(resURI);
resAccessible = accessibleFile(resPath);
resFileExt = getFileExt(resPath);
//set response code if resource does not exist
if ( ! resAccessible) respCode = NO_RESOURCE;
//log info
WebServer.win.log.debug(port + "-Resource file path:" + resPath);
WebServer.win.log.debug(port + "-Resource file extension:" + resFileExt);
WebServer.win.log.debug(port + "-Resource accessible:" + resAccessible);
}
//request methods: utility methods
protected String resPhysPath(String resLogURI) {
//given the resource logical path,
//returns the corresponding physical path.
if (resLogURI == null) return null;
StringBuffer physPath = new StringBuffer();
physPath.append(WebServer.obj.mainDir);
physPath.append(resLogURI);
if (resLogURI.charAt(resLogURI.length()-1) == '/')
physPath.append(WebServer.obj.defHTML);
return physPath.substring(0);
}
protected boolean accessibleFile(String physPath) {
//check if file specified by the physical path is accessible.
if (physPath == null) return false;
boolean accessible;
File theFile = new File(physPath);
accessible = theFile.canRead();
accessible = accessible && theFile.isFile(); //not a directory
return accessible;
}
protected String getFileExt(String filename) {
//returns the file extension of 'filename'
if (filename == null) return null;
String fileExt = ""; //default
StringBuffer sb = (new StringBuffer(filename)).reverse(); //reverse string
try {
StringTokenizer parser = new StringTokenizer(sb.substring(0), "./", true);
String ext, delim;
ext = parser.nextToken();
delim = parser.nextToken();
if (delim.compareTo(".") == 0)
fileExt = (new StringBuffer(ext)).reverse().substring(0);
} catch (NoSuchElementException e) {
return fileExt;
}
return fileExt;
}
//response methods: composing response
protected void respond() {
respBody();
httpHeader();
sendResponse();
}
protected void respBody() {
//response body must be attached to stream 'rbFile'
//if from a file, or 'rbString' if a string.
//body, length, and MIME type must be determined.
//first, consider response according to
//possible errors while analysing request
rbFile = null;
rbString = null;
rbLength = 0;
mimeType = null;
switch (respCode) {
case NORMAL : switchRespBody();
break;
//error - no response body (just HTTP header)
case NO_RESOURCE :
case REQUEST_ERR :
case NO_SERVICE :
case SERVER_ERR : break;
}
}
//-----------------------------------------------------------------------
//This method is intented to be overidden by subclasses.
protected void switchRespBody() {
//determines response body, length, and MIME type.
//No errors at this point (NORMAL respCode), consider response mode.
switch (respMode) {
case UPLOAD_MODE : rbString = upAnswer;
rbLength = rbString.length();
mimeType = getMIMEType("html");
break;
case FILE_MODE : rbFile = streamResponseFile(resPath);
rbLength = (new File(resPath)).length();
mimeType = getMIMEType(resFileExt);
break;
}
}
//-----------------------------------------------------------------------
protected void httpHeader() {
//sets 'status' and 'httpMsg' response variables.
//map response codes to HTTP codes
//NOTE: codes with confirmed meaning: 200, 404, 501
status = "501"; //default: server error
switch(respCode) {
case NORMAL : status = "200 OK"; break; //everything OK
case NO_RESOURCE : status = "404"; break; //client error
case REQUEST_ERR : status = "401"; break; //client error
case NO_SERVICE : status = "504"; break; //server error
case SERVER_ERR : status = "501"; break; //server error
}
//prepare HTTP message
httpMsg = new StringBuffer(
"HTTP/1.0 " + status + "\n" +
"Date: " + new Date() + "\n" +
"Server: " + WebServer.obj.appName + " "
+ WebServer.obj.appVers + "\n");
if (rbFile != null || rbString != null) //there exists a response body
httpMsg.append(
"MIME-version: 1.0\n" +
"Content-type: " + mimeType + "\n" +
"Content-length: " + rbLength + "\n");
httpMsg.append("\n"); //a blank line needed anyway
}
protected void sendResponse() {
//response is 'httpMsg' + contents of 'rbFile' or 'rbString' (if any =! null).
//'httpMsg' assumed to be not null
try {
//send HTTP header
out.print(httpMsg);
out.flush(); //flush data to output
//if there exists an HTTP body to be sent, send it
if (rbFile != null) { //for downloading files use 'raw' streams
BufferedInputStream body = new BufferedInputStream(rbFile);
BufferedOutputStream rawOut = new BufferedOutputStream(sock.getOutputStream());
int charRead;
while((charRead = body.read()) != -1)
rawOut.write(charRead);
rawOut.flush(); //flush data to output
body.close();
rbFile.close();
}
else if (rbString != null) { //for string, use character-oriented streams
StringReader body = new StringReader(rbString);
int charRead;
while((charRead = body.read()) != -1) {
char ch = (char) charRead;
out.print(ch);
}
out.flush(); //flush data to output
body.close();
}
//release class socket
sock.close();
} catch (IOException e) {
WebServer.win.log.error(port + "-Problem responding: " + e);
WebServer.flog.writeln(port + "-Problem responding: " + e);
}
WebServer.win.log.debug(port + "-RESPONSE: HTTP header follows:\n" + httpMsg);
}
//response methods: utility methods
protected FileInputStream streamResponseFile(String path) {
//response body from a file, attaches stream to 'path'
FileInputStream in;
try {
in = new FileInputStream(path);
} catch (IOException e) {
WebServer.win.log.error("Problem with response file stream: " + e);
WebServer.flog.writeln("Problem with response file stream: " + e);
in = null;
}
return in;
}
protected String getMIMEType(String fileExt) {
//set MIME with regard to file extension 'fileExt'.
//'fileExt' must not be null
if (fileExt == null) return null;
String mm = null;
mm = WebServer.mime.getProperty(fileExt);
if (fileExt.compareTo("") == 0) //no extension
mm = WebServer.mime.getProperty(".");
if (mm == null) //no correspondence found
mm = WebServer.mime.getProperty("default");
return mm;
}
//various helper methods
protected void logRequest() {
//log the client request
String dateTime = DateFormat.getDateTimeInstance
(DateFormat.SHORT, DateFormat.SHORT, Locale.UK).format(new Date());
WebServer.win.log.report(dateTime + " - Request from "
+ clientAddr.toString() + " (" + port + ")");
WebServer.flog.writeln("Request from "
+ clientAddr.toString() + " (" + port + ")");
}
}
//===================================================================
// WServer
//
// The actual web server. Waits for requests at the specified port
// and creates a new ReqWorker class to handle each request at
// another port.
//===================================================================
class WServer extends Thread {
private ServerSocket srvSock = null;
public WServer(int port, int backlog) {
super();
String localHost = null;
int localPort = -1;
try {
srvSock = new ServerSocket(port, backlog);
localHost = (InetAddress.getLocalHost()).toString();
localPort = srvSock.getLocalPort();
} catch (IOException e) {
JOptionPane.showMessageDialog(WebServer.win,
"Problem starting server: " + e, "Error", JOptionPane.ERROR_MESSAGE);
WebServer.flog.forceWriteln("Problem starting server: " + e);
WebServer.terminate(false); //quit application if cannot start
}
//log msgs
WebServer.win.log.markStart();
WebServer.win.log.report("Server machine is: " + localHost);
WebServer.win.log.report("Server local port is: " + localPort);
WebServer.win.log.report("======== SERVER UP ========");
WebServer.win.log.report(""); //empty line
WebServer.flog.writeln("Server started at " + localHost +
", port " + localPort);
}
//-----------------------------------------------------------------------
public void run() {
if (srvSock == null) return;
Socket sock;
ReqWorker worker;
while (true) {
try {
sock = srvSock.accept(); //waits until request arrives
//in case the 'ReqWorker' class is overriden, the following
//line must be adapted to create an object of the subclass
worker = new ReqWorker(sock);
worker.start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//-----------------------------------------------------------------------
protected void finalize(){
if (srvSock != null){
try {
srvSock.close();
} catch (IOException e) {
e.printStackTrace();
}
srvSock = null;
}
}
}
/********************************************************************
*********************** MAIN CLASSES *************************
********************************************************************/
//===================================================================
// WebServer
//
// The main class. One object of this class is created and assigned
// to a static variable, so that other classes can access its public
// variables. Creates the server daemon and the UI window plus a
// number of other classes, that are assigned to static variables so
// that they can access each other.
//
// In the frame of other applications, this class may be overriden
// by other classes that expand or modify the server functionality.
//===================================================================
public class WebServer {
//private variables
protected int port; //port server is running, free: > 1024
protected int maxReq; //max concurrent client requests
protected String logMode; //on: screen & file, screen: screen only, file: file only, off: no msgs
protected boolean debugMode; //if true (and logMode allowed), debug msgs displayed on screen
protected boolean uploadOn; //if true, file uploading is allowed
protected long uploadMax; //maximum number of bytes per upload, 0 means no limit
protected String prefFile; //application settings ini file
protected String prefHeader; //header in ini file
protected String mimeFile; //path to mime file
protected String logPath; //log file path
protected long maxFLSize; //max FileLog size in bytes
protected int maxALLines; //max ActivityLog (display msg) lines
//public variables
public int reqTimeout; //timeout for reading requests in millisecs, 0 for infinite
public String mainDir; //main HTML (public) directory
public String defHTML; //default HTML page
public String uploadDir; //directory for uploading
public String appName; //application name
public String appVers; //application version
public String appCapt; //application window caption
//one object for each of the main classes
static public WebServer obj; //the single WebServer object
static public WServer srv; //the actual web server
static public ControlWin win; //the UI window
static public Preferences pref; //application settings
static public Properties mime; //mime types correspondence
static public FileLog flog; //log file
//main method
static public void main(String[] args) {
obj = new WebServer();
}
//terminates properly the application
static public void terminate(boolean normal) {
pref.store(); //save application settings
flog.forceWriteln("Shutting down server, normal: " + normal);
if (normal) System.exit(0);
else System.exit(1);
}
//initializer
protected WebServer() {
//initialize variables
initVars();
//initialize variables from application settings
//(check 'Preferences.setDef()' for their default values
// and the 'prefFile' text file for their actual values)
pref = new Preferences(prefFile, prefHeader);
loadSettings();
//prepare log file
flog = new FileLog(logPath, maxFLSize);
//setup UI
win = new ControlWin(appCapt, maxALLines);
//load MIME data
mime = new Properties();
loadMime();
//start server
srv = new WServer(port, maxReq);
srv.setDaemon(true);
srv.start();
//set if msgs appear on screen and/or log file,
//and how detailed messages that appear on screen are
setMessageMode(); //after WServer creation, to allow initial lines to appear anyway
//report log mode anyway (irrespective of log mode)
win.log.forceReport("Messages log mode set to: " + logMode);
win.log.forceReport(""); //en empty line for aesthetics
flog.forceWriteln("Messages log mode set to: " + logMode);
}
//auxiliary methods
protected void initVars() {
appName = "YServer";
appVers = "2.0";
appCapt = " " + appName + " ver. " + appVers;
prefFile = "./server.ini";
prefHeader = appName + " settings";
mimeFile = "./mime.ini";
}
protected void loadSettings() {
port = Integer.parseInt(pref.getPref("port"));
maxReq = Integer.parseInt(pref.getPref("max_requests"));
logMode = pref.getPref("log_mode");
debugMode = (pref.getPref("debug_mode")).equals("on")? true: false; //on - off
uploadOn = (pref.getPref("upload_feature")).equals("on")? true: false; //on - off
uploadMax = Long.parseLong(pref.getPref("upload_max_size"));
reqTimeout = Integer.parseInt(pref.getPref("read_request_timeout"));
mainDir = pref.getPref("main_html_dir");
defHTML = pref.getPref("def_html_file");
uploadDir = pref.getPref("upload_dir");
logPath = pref.getPref("log_file_path");
maxFLSize = Long.parseLong(pref.getPref("max_log_size"));
maxALLines = Integer.parseInt(pref.getPref("max_screen_lines"));
}
protected void setMessageMode() {
//set if msgs are displayed on screen and/or written in log file
if (logMode.equals("on")) { //both screen and file
win.log.setLog(true);
flog.setLog(true);
}
else if (logMode.equals("screen")) { //screen only
win.log.setLog(true);
flog.setLog(false);
}
else if (logMode.equals("file")) { //file only
win.log.setLog(false);
flog.setLog(true);
}
else { //logMode is 'off', neither screen nor file
win.log.setLog(false);
flog.setLog(false);
}
//set detail of msgs displayed on screen (if they are displayed)
win.log.setDebug(debugMode);
}
protected void loadMime() {
try {
FileInputStream in = new FileInputStream(mimeFile);
mime.load(in);
} catch(IOException e) {
win.log.error("Problem loading mime types: " + e);
flog.writeln("Problem loading mime types: " + e);
}
}
}