/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.openejb.server.httpd;

import org.apache.openejb.OpenEJBException;
import org.apache.openejb.core.ParentClassLoaderFinder;
import org.apache.openejb.loader.Options;
import org.apache.openejb.loader.SystemInstance;
import org.apache.openejb.server.ServiceException;
import org.apache.openejb.server.context.RequestInfos;
import org.apache.openejb.server.stream.CountingInputStream;
import org.apache.openejb.server.stream.CountingOutputStream;
import org.apache.openejb.util.LogCategory;
import org.apache.openejb.util.Logger;
import org.apache.openejb.util.OptionsLog;

import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.net.Socket;
import java.net.SocketException;
import java.net.URI;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

/**
 * This is the main class for the web administration.  It takes care of the
 * processing from the browser, sockets and threading.
 *
 * @since 11/25/2001
 */
public class OpenEJBHttpServer implements HttpServer {

    private static final Logger log = Logger.getInstance(LogCategory.HTTPSERVER, "org.apache.openejb.util.resources");

    private HttpListener listener;
    private Set<Output> print;
    private boolean indent;
    private boolean debugPayload;

    public OpenEJBHttpServer() {
        this(getHttpListenerRegistry());
    }

    public static HttpListenerRegistry getHttpListenerRegistry() {
        final SystemInstance systemInstance = SystemInstance.get();
        HttpListenerRegistry registry = systemInstance.getComponent(HttpListenerRegistry.class);
        if (registry == null) {
            registry = new HttpListenerRegistry();
            systemInstance.setComponent(HttpListenerRegistry.class, registry);
        }
        return registry;
    }

    public OpenEJBHttpServer(final HttpListener listener) {
        this.listener = new OpenEJBHttpRegistry.ClassLoaderHttpListener(listener, ParentClassLoaderFinder.Helper.get());
    }

    public static boolean isTextXml(final Map<String, String> headers) {
        final String contentType = headers.get("Content-Type");
        return contentType != null && contentType.contains("text/xml");
    }

    @Override
    public HttpListener getListener() {
        return listener;
    }

    @Override
    public void service(final Socket socket) throws ServiceException, IOException {
        /**
         * The InputStream used to receive incoming messages from the client.
         */
        InputStream in = null;
        /**
         * The OutputStream used to send outgoing response messages to the client.
         */
        OutputStream out = null;

        try {
            RequestInfos.initRequestInfo(socket);

            in = debugPayload ? new CountingInputStream(socket.getInputStream()) : socket.getInputStream();
            out = debugPayload ? new CountingOutputStream(socket.getOutputStream()) : socket.getOutputStream();

            //TODO: if ssl change to https
            final URI socketURI = new URI("http://" + socket.getLocalAddress().getHostAddress() + ":" + socket.getLocalPort());
            processRequest(socketURI, in, out);

        } catch (final Throwable e) {
            log.error("Unexpected error", e);
        } finally {
            if (out != null) {
                try {
                    out.flush();
                } catch (final Throwable e) {
                    //Ignore
                }
                try {
                    out.close();
                } catch (final Throwable e) {
                    //Ignore
                }
            }

            if (in != null) {
                try {
                    in.close();
                } catch (final Throwable e) {
                    //Ignore
                }
            }

            try {
                socket.close();
            } catch (final Throwable e) {
                log.error("Encountered problem while closing connection with client: " + e.getMessage());
            }
        }
    }

    @Override
    public void service(final InputStream in, final OutputStream out) throws ServiceException, IOException {
        throw new UnsupportedOperationException("Method not implemented: service(InputStream in, OutputStream out)");
    }

    @Override
    public void init(final Properties props) throws Exception {
        final Options options = new Options(props);
        options.setLogger(new OptionsLog(log));
        print = options.getAll("print", OpenEJBHttpServer.Output.class);
        indent = print.size() > 0 && options.get("indent.xml", false);
        debugPayload = "true".equalsIgnoreCase(props.getProperty("debugPayload", "true"));
    }

    public enum Output {
        REQUEST,
        RESPONSE
    }

    @Override
    public void start() throws ServiceException {
    }

    @Override
    public void stop() throws ServiceException {
    }

    @Override
    public String getName() {
        return "httpd";
    }

    @Override
    public int getPort() {
        return 0;
    }

    @Override
    public String getIP() {
        return "";
    }

    /**
     * takes care of processing requests and creating the webadmin ejb's
     *
     * @param in  the input stream from the browser
     * @param out the output stream to the browser
     */
    private void processRequest(final URI socketURI, final InputStream in, final OutputStream out) {
        HttpResponseImpl response = null;
        try {
            response = process(socketURI, in);

        } catch (final Throwable t) {
            response = HttpResponseImpl.createError(t.getMessage(), t);
        } finally {
            try {
                if (response != null) {
                    response.writeMessage(out, false);

                    if (print.size() > 0 && print.contains(Output.RESPONSE)) {
                        response.writeMessage(new LoggerOutputStream(log, "debug"), indent);
                    }
                }
            } catch (final Throwable t2) {

                if (log.isDebugEnabled()) {
                    log.debug("Could not write response", t2);
                } else {
                    //SocketException is something a client can cause, so do not log it (potential DOS)
                    if (!SocketException.class.isInstance(t2)) {
                        log.warning("Could not write response:" + t2);
                    }
                }

            }
        }
    }

    private HttpResponseImpl process(final URI socketURI, final InputStream in) throws OpenEJBException {
        final HttpRequestImpl req = new HttpRequestImpl(socketURI);
        final HttpResponseImpl res = new HttpResponseImpl();

        try {
            req.readMessage(in);

            if (print.size() > 0 && print.contains(Output.REQUEST)) {
                req.print(log, indent);
            }

            res.setRequest(req);
        } catch (final Throwable t) {
            res.setCode(400);
            res.setResponseString("Could not read the request");
            try {
                res.getWriter().println(t.getMessage());
                t.printStackTrace(res.getWriter());
            } catch (final IOException e) {
                // no-op
            }
            log.error("BAD REQUEST", t);
            throw new OpenEJBException("Could not read the request.\n" + t.getClass().getName() + ":\n" + t.getMessage(), t);
        }

        final URI uri;
        String location = null;
        try {
            uri = req.getURI();
            location = uri.getPath();
            final int querry = location.indexOf("?");
            if (querry != -1) {
                location = location.substring(0, querry);
            }
        } catch (final Throwable t) {
            throw new OpenEJBException("Could not determine the module " + location + "\n" + t.getClass().getName() + ":\n" + t.getMessage());
        }

        try {
            listener.onMessage(req, res);
        } catch (final Throwable t) {
            throw new OpenEJBException("Error occurred while executing the module " + location + "\n" + t.getClass().getName() + ":\n" + t.getMessage(), t);
        }

        return res;
    }

    public static String reformat(final String raw) {
        if (raw.length() == 0) {
            return raw;
        }

        try {
            final TransformerFactory factory = TransformerFactory.newInstance();
            // bugged in some XML implementation
            // should we use another implementation?
            //factory.setAttribute("indent-number", 2);

            final Transformer transformer = factory.newTransformer();
            transformer.setOutputProperty(OutputKeys.INDENT, "yes");
            transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");

            final StreamResult result = new StreamResult(new StringWriter());

            transformer.transform(new StreamSource(new StringReader(raw)), result);

            return result.getWriter().toString();
        } catch (final TransformerException e) {
            e.printStackTrace();
            return raw;
        }
    }

    private static class LoggerOutputStream extends OutputStream {

        private final Logger logger;
        private final String level;

        public LoggerOutputStream(final Logger log, final String lvl) {
            logger = log;
            level = lvl;
        }

        @Override
        public void write(final int b) throws IOException {
            logger.log(level, Character.toString((char) b));
        }

        @Override // shortcut for String - because we know what we have ;)
        public void write(final byte[] b) throws IOException {
            logger.log(level, new String(b));
        }
    }
}
