org.sipfoundry.sipxprovision.auto.Servlet.java Source code

Java tutorial

Introduction

Here is the source code for org.sipfoundry.sipxprovision.auto.Servlet.java

Source

/*
 *
 *
 * Copyright (C) 2009 Nortel, certain elements licensed under a Contributor Agreement.
 * Contributors retain copyright to elements licensed under a Contributor Agreement.
 * Licensed to the User under the LGPL license.
 *
 */
package org.sipfoundry.sipxprovision.auto;

import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.Writer;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Date;
import java.util.HashMap;
import java.util.Properties;
import java.util.regex.Pattern;

import javax.net.ssl.HttpsURLConnection;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.log4j.Logger;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.apache.velocity.exception.ParseErrorException;
import org.apache.velocity.exception.ResourceNotFoundException;
import org.mortbay.http.HttpContext;
import org.mortbay.http.HttpServer;
import org.mortbay.jetty.servlet.ServletHandler;
import org.sipfoundry.commons.util.ShortHash;

/**
 * A Jetty servlet that auto-provisions phones based on their HTTP requests.
 * 
 * @author Paul Mossman
 */
@SuppressWarnings("serial")
public class Servlet extends HttpServlet {

    private static final Logger LOG = Logger.getLogger("Servlet");

    protected static Configuration m_config = null;

    private static final String MAC_RE_STR = "[0-9a-fA-F]{12}";

    public static final String ID_PREFIX = "ID: ";

    /**
     * Returns a simple unique-ish ID string (hash) generated from the specified seed.
     * 
     * @see org.sipfoundry.commons.util.ShortHash
     */
    protected static String getUniqueId(String seed_string) {

        return ShortHash.get(seed_string);
    }

    protected static final String POLYCOM_PATH_PREFIX = "/";

    private static final String POLYCOM_PATH_FORMAT_RE_STR = "^" + POLYCOM_PATH_PREFIX + MAC_RE_STR + "-%s$";

    private static final Pattern POLYCOM_SIP_PATH_RE = Pattern
            .compile(String.format(POLYCOM_PATH_FORMAT_RE_STR, "sipx-sip.cfg"));

    private static final Pattern POLYCOM_PHONE1_PATH_RE = Pattern
            .compile(String.format(POLYCOM_PATH_FORMAT_RE_STR, "sipx-phone.cfg"));

    private static final Pattern POLYCOM_DEVICE_PATH_RE = Pattern
            .compile(String.format(POLYCOM_PATH_FORMAT_RE_STR, "sipx-device.cfg"));

    private static final Pattern POLYCOM_OVERRIDES_PATH_RE = Pattern
            .compile(String.format(POLYCOM_PATH_FORMAT_RE_STR, "phone.cfg"));

    private static final Pattern POLYCOM_CONTACTS_PATH_RE = Pattern
            .compile(String.format(POLYCOM_PATH_FORMAT_RE_STR, "directory.xml"));

    private static final Pattern POLYCOM_000000000000_CONTACTS_PATH_RE = Pattern
            .compile("^" + POLYCOM_PATH_PREFIX + "000000000000-directory.xml$");

    private static final Pattern POLYCOM_40_PATH_RE = Pattern.compile(String.format(POLYCOM_PATH_FORMAT_RE_STR,
            "sipx-((applications)|(sip-interop)|(reg-advanced)|(region)|(sip-basic)|(video)|(site)|(features)).cfg"));

    private static final String POLYCOM_UA_DELIMITER = "UA/";

    protected static final String NORTEL_IP_12X0_PATH_PREFIX = "/Nortel/config/SIP";

    private static final Pattern NORTEL_IP_12X0_PATH_RE = Pattern
            .compile("^" + NORTEL_IP_12X0_PATH_PREFIX + MAC_RE_STR + ".xml$");

    private static final String NORTEL12X0_UA_PREFIX_STR = "Nortel IP Phone 12";

    private static final Pattern NORTEL12X0_UA_RE = Pattern
            .compile(String.format("^%s[1-3]0 \\(SIP12x0[0-9\\.]*\\)$", NORTEL12X0_UA_PREFIX_STR));

    private static final Pattern EXACT_MAC_RE = Pattern.compile("^" + MAC_RE_STR + "$");

    protected static boolean isMacAddress(String mac) {
        return EXACT_MAC_RE.matcher(mac).matches();
    }

    /**
     * Extracts the MAC from the specified path, starting immediately after the first occurrence
     * of the specified prefix.
     * 
     * @return the MAC, or null on failure.
     */
    protected static String extractMac(String path, String prefix) {
        try {
            int start = path.indexOf(prefix) + prefix.length();
            String mac = path.substring(start, start + 12);
            if (isMacAddress(mac)) {
                return mac.toLowerCase();
            }
        } catch (Exception e) {
        }

        return null;
    }

    /**
     * Starts the Jetty servlet that handles auto-provision requests
     */
    public static void start() {
        try {
            // The configuration.
            m_config = new Configuration();
            m_config.configureLog4j();

            LOG.info("START.");
            LOG.debug(String.format("Unique ID - length: %d  set size: %d  combinations: %d.", ShortHash.ID_LENGTH,
                    ShortHash.ID_CHARS.length,
                    new Double(Math.pow(ShortHash.ID_CHARS.length, ShortHash.ID_LENGTH)).intValue()));

            // Dump the configuration into the log.
            ByteArrayOutputStream stream = new ByteArrayOutputStream();
            m_config.dumpConfiguration(new PrintWriter(stream));
            String[] lines = stream.toString().split("\\n");
            for (String line : lines) {
                LOG.info(line);
            }

            // This is what causes non-provisioned Polycoms to the HTTP request to the servlet.
            initializeStaticConfig(m_config);

            // Start up jetty.
            HttpServer server = new HttpServer();

            // Bind the port on all interfaces.
            server.addListener(":" + m_config.getServletPort());

            HttpContext httpContext = new HttpContext();
            httpContext.setContextPath("/");

            // Setup the servlet to call the class when the URL is fetched.
            ServletHandler servletHandler = new ServletHandler();
            servletHandler.addServlet(Servlet.class.getCanonicalName(), m_config.getServletUriPath() + "/*",
                    Servlet.class.getName());
            httpContext.addHandler(servletHandler);
            server.addContext(httpContext);

            // Start it up.
            LOG.info(String.format("Starting %s servlet on *:%d%s", Servlet.class.getCanonicalName(),
                    m_config.getServletPort(), m_config.getServletUriPath()));
            server.start();
        } catch (Exception e) {
            LOG.error("Failed to start the servlet:", e);
        }
    }

    public static class PolycomVelocityCfg {

        Configuration m_config;

        PolycomVelocityCfg(Configuration config) {
            m_config = config;
        }

        public String getSipBinaryFilename() {
            return "sip.ld";
        }

        public String getRootUrlPath() {
            return String.format("http://%s:%d%s/", m_config.getHostname(), m_config.getServletPort(),
                    m_config.getServletUriPath());
        }

        public String getDefaultFirmware() {
            return "4.0.X";
        }
    }

    /**
     * This content is static only while the servlet is running. A configuration change of the
     * servlet port will change the content, but this will be done during the servlet re-start.
     * 
     * @throws Exception
     * @throws ParseErrorException
     * @throws ResourceNotFoundException
     */
    private static void initializeStaticConfig(Configuration config) {

        // Generate the Polycom 000000000000.cfg, which will cause un-provisioned phones to send
        // HTTP requests to this servlet.
        File polycom_src_dir = new File(System.getProperty("conf.dir") + "/polycom");
        try {

            Properties p = new Properties();
            p.setProperty("resource.loader", "file");
            p.setProperty("class.resource.loader.class",
                    "org.apache.velocity.runtime.resource.loader.FileResourceLoader");
            p.setProperty("file.resource.loader.path", polycom_src_dir.getAbsolutePath());
            Velocity.init(p);

            Template template = Velocity.getTemplate("000000000000.cfg.vm");

            VelocityContext context = new VelocityContext();
            context.put("cfg", new PolycomVelocityCfg(config));

            Writer writer = new FileWriter(new File(config.getTftpPath(), "/000000000000.cfg"));
            template.merge(context, writer);
            writer.flush();

            LOG.info(String.format("Generated Polycom 000000000000.cfg from %s.",
                    polycom_src_dir.getAbsolutePath()));

        } catch (ResourceNotFoundException e) {
            LOG.error("Failed to generate Polycom 000000000000.cfg: ", e);
        } catch (ParseErrorException e) {
            LOG.error("Failed to generate Polycom 000000000000.cfg: ", e);
        } catch (Exception e) {
            LOG.error("Velocity initialization error:", e);
        }

        // Copy the Polycom static files. (See:
        // http://list.sipfoundry.org/archive/sipx-dev/msg20157.html)
        try {

            FilenameFilter cfg_filter = new FilenameFilter() {
                public boolean accept(File dir, String name) {
                    return name.startsWith("polycom_") && name.endsWith(".cfg");
                }
            };

            File[] files = polycom_src_dir.listFiles(cfg_filter);
            if (null == files) {
                LOG.error(String.format("No Polycom static files found at %s.", polycom_src_dir.getAbsolutePath()));
            } else {
                int copy_count = 0;
                for (File file : files) {

                    File dest_file = new File(config.getTftpPath(), file.getName());
                    if (!dest_file.exists()) {

                        OutputStream output = new BufferedOutputStream(new FileOutputStream(dest_file));
                        FileInputStream input = null;
                        try {
                            input = new FileInputStream(file);
                            IOUtils.copy(input, output);
                            copy_count++;
                        } catch (IOException e) {
                            LOG.error(String.format("Failed to copy Polycom static file %s.", file.getName()), e);
                        } finally {
                            IOUtils.closeQuietly(input);
                            IOUtils.closeQuietly(output);
                        }
                    }
                }

                LOG.info(String.format("Copied %d (of %d) Polycom static files from %s.", copy_count, files.length,
                        polycom_src_dir.getAbsolutePath()));
            }
        } catch (Exception e) {
            LOG.error("Failed to copy Polycom static files from " + polycom_src_dir.getAbsolutePath() + ":", e);
        }

        // Generate the Nortel IP 12x0 SIPdefault.xml, which will cause un-provisioned phones to
        // send
        // an HTTP request to this servlet.
        try {
            File dir = new File(config.getTftpPath() + "/Nortel/config");
            if (createTftpDirectory(dir)) {
                File file = new File(dir + "/SIPdefault.xml");

                FileOutputStream fos = new java.io.FileOutputStream(file);
                java.io.PrintStream ps = new java.io.PrintStream(fos);
                ps.println("<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>");
                ps.println(String.format("<redirect>http://%s:%d%s</redirect>", m_config.getHostname(),
                        m_config.getServletPort(), m_config.getServletUriPath()));
                fos.close();

                LOG.info("Generated Nortel IP 12x0 SIPdefault.xml.");

                file = new File(dir + "/DialPlan.txt");
                File sourceFile = new File(System.getProperty("conf.dir") + "/nortel/DialPlan.txt");
                FileUtils.copyFile(sourceFile, file);
            }
            createTftpDirectory(new File(config.getTftpPath() + "/Nortel/firmware"));
            createTftpDirectory(new File(config.getTftpPath() + "/Nortel/languages"));
        } catch (Exception e) {
            LOG.error("Failed to generate Nortel IP 12x0 SIPdefault.xml: ", e);
        }
    }

    private static boolean createTftpDirectory(File dir) {
        if (!dir.exists() && !dir.mkdirs()) {
            LOG.error("Failed to create directory: " + dir);
            return false;
        }
        return true;
    }

    private void logAndOut(String msg, PrintWriter out) {
        LOG.debug(msg);
        out.println(msg);
    }

    protected void writeDebuggingResponse(HttpServletRequest request, HttpServletResponse response)
            throws IOException {

        // Do some debugging.
        LOG.debug("Debugging: " + request.getPathInfo());
        response.setContentType("text/plain");
        PrintWriter out = response.getWriter();

        logAndOut("getPathInfo(): '" + request.getPathInfo() + "'", out);
        logAndOut("getQueryString(): '" + request.getQueryString() + "'", out);
        logAndOut("getRequestURI(): '" + request.getRequestURI() + "'", out);
        logAndOut("getRemoteUser(): '" + request.getRemoteUser() + "'", out);
        logAndOut("getServletPath(): '" + request.getServletPath() + "'", out);
        logAndOut("getContextPath(): '" + request.getContextPath() + "'", out);
        logAndOut("getLocalAddr(): '" + request.getLocalAddr() + "'", out);
        logAndOut("getServerName(): '" + request.getServerName() + "'", out);
        logAndOut("getServerPort(): '" + request.getServerPort() + "'", out);
        logAndOut("getHeader(\"User-Agent\"): '" + request.getHeader("User-Agent") + "'", out);
        logAndOut("", out);

        logAndOut("request: " + request, out);
        m_config.dumpConfiguration(out);
    }

    /**
     * Creates a (single-use) HTTPS connection to the sipXconfig REST Phones resource server.
     * 
     * @return the HTTPS connection, or null upon failure.
     */
    protected HttpURLConnection createRestConnection(String method, String string_url) {
        LOG.debug("Creating connection: " + method + " " + string_url);

        HttpURLConnection connection = null;
        try {
            URL url = new URL(string_url);
            connection = (HttpURLConnection) url.openConnection();

            connection.setDoOutput(true);
            connection.setDoInput(true);
            connection.setUseCaches(false);
            connection.setRequestMethod(method);
            LOG.debug("Connection is good.");

        } catch (Exception e) {
            LOG.error("Failed to create HttpsURLConnection:", e);
            connection = null;
        }

        return connection;
    }

    /**
     * Create a Description text for the specified phone.
     * 
     * @see provisionPhones
     * @return the string description.
     */
    protected static String getPhoneDescription(DetectedPhone phone, Date date) {
        return String.format("Auto-provisioned\n  %s%s\n  Version: %s\n", ID_PREFIX, getUniqueId(phone.mac),
                phone.version);
    }

    protected static String extractPolycomVersion(DetectedPhone phone) {
        if (phone.model.sipxconfig_id != null && phone.model.sipxconfig_id.contains("polycom")) {
            return formatPolycomVersion(phone.version);
        }
        return "";
    }

    /**
     * Format the Polycom version as defined in PolycomModel bean
     * @param version
     * @return
     */
    protected static String formatPolycomVersion(String version) {
        return (new StringBuilder(version.substring(0, 3).concat(".X"))).toString();
    }

    protected boolean doProvisionPhone(DetectedPhone phone) {

        if (null == phone) {
            return false;
        }

        boolean success = false;
        try {
            LOG.info("doProvisionPhone - " + phone);

            // Write the REST representation of the phone(s).
            HttpURLConnection connection = createRestConnection("POST",
                    m_config.getConfigurationUri() + "/rest/phone");
            DataOutputStream dstream = new java.io.DataOutputStream(connection.getOutputStream());
            dstream.writeBytes("<phones>");
            dstream.writeBytes(String.format(
                    "<phone><serialNumber>%s</serialNumber>"
                            + "<model>%s</model><description>%s</description><version>%s</version></phone>",
                    phone.mac.toLowerCase(), phone.model.sipxconfig_id, getPhoneDescription(phone, new Date()),
                    extractPolycomVersion(phone)));
            dstream.writeBytes("</phones>");

            // Do the HTTPS POST.
            dstream.close();

            // Record the result. (Only transport, internal, etc. errors will be reported.
            // Duplicate and/or invalid MACs for example will result in 200 OK.)
            if (HttpsURLConnection.HTTP_OK == connection.getResponseCode()) {
                success = true;
                LOG.info("REST HTTPS POST success.");
            } else {
                LOG.error("REST HTTPS POST failed:" + connection.getResponseCode() + " - "
                        + connection.getResponseMessage());
            }

        } catch (Exception e) {
            LOG.error("REST HTTPS POST failed:", e);
        }

        return success;
    }

    protected boolean doUpdatePhone(String mac, String version, String model, HttpServletResponse response) {

        boolean success = false;
        try {
            LOG.info(String.format("update phone %s to version %s", mac, formatPolycomVersion(version)));

            // Construct the REST request.
            String uri = String.format("%s/rest/updatephone/%s/%s/%s", m_config.getConfigurationUri(), mac,
                    formatPolycomVersion(version), model);
            HttpURLConnection connection = createRestConnection("GET", uri);

            // Do the HTTPS GET, and write the content.
            connection.connect();
            IOUtils.copy(connection.getInputStream(), response.getOutputStream());

            // Record the result. (Only transport, internal, etc. errors will be reported.
            // Duplicate and/or invalid MACs for example will result in 200 OK.)
            if (HttpsURLConnection.HTTP_OK == connection.getResponseCode()) {
                success = true;
                LOG.info("REST HTTPS GET success.");
            } else {
                LOG.error("REST HTTPS GET failed:" + connection.getResponseCode() + " - "
                        + connection.getResponseMessage());
            }

        } catch (Exception e) {
            LOG.error("REST HTTPS GET failed:", e);
        }

        return success;
    }

    protected boolean isDebugTestUserAgent(String useragent) {
        return (useragent.contains("Gecko") || useragent.contains("curl")) && m_config.isDebugOn();
    }

    protected static boolean isPolycomConfigurationFilePath(String path) {
        return POLYCOM_SIP_PATH_RE.matcher(path).matches() || POLYCOM_PHONE1_PATH_RE.matcher(path).matches()
                || POLYCOM_DEVICE_PATH_RE.matcher(path).matches()
                || POLYCOM_OVERRIDES_PATH_RE.matcher(path).matches()
                || POLYCOM_CONTACTS_PATH_RE.matcher(path).matches()
                || POLYCOM_000000000000_CONTACTS_PATH_RE.matcher(path).matches()
                || POLYCOM_40_PATH_RE.matcher(path).matches();
    }

    static protected boolean isNortelIp12x0ConfigurationFilePath(String path) {
        return null != extractMac(path, NORTEL_IP_12X0_PATH_PREFIX);
    }

    // TODO: Test case (x2 phone types) when MAC is too short (should not invoke the do___Get...)
    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        String path = request.getPathInfo();
        String useragent = request.getHeader("User-Agent");
        LOG.info("GET " + path + "  User-Agent: " + useragent);

        // Examine the User-Agent.
        if (null == useragent || useragent.isEmpty()) {

            // Can't do anything without this.
            writeUiForwardResponse(request, response);
            return;
        }

        // For debugging.
        if (isDebugTestUserAgent(useragent)) {
            if (Servlet.isPolycomConfigurationFilePath(path)) {
                useragent = "FileTransport PolycomSoundStationIP-SSIP_6000-UA/3.2.0.0157";
            } else if (isNortelIp12x0ConfigurationFilePath(path)) {
                useragent = "Nortel IP Phone 1210 (SIP12x0.45.02.05.00)";
            }
        }

        // Is this a path that triggers provisioning of a phone with sipXconfig?
        DetectedPhone phone = null;
        if (POLYCOM_SIP_PATH_RE.matcher(path).matches() || POLYCOM_40_PATH_RE.matcher(path).matches()) {

            // Polycom SoundPoint IP / SoundStation IP / VVX
            phone = new DetectedPhone();

            // MAC, Model, & Version
            phone.mac = extractMac(path, POLYCOM_PATH_PREFIX);
            if (null == phone.mac || !extractPolycomModelAndVersion(phone, useragent)) {
                writeUiForwardResponse(request, response);
                return;
            }
        } else if (isNortelIp12x0ConfigurationFilePath(path)) {

            // Nortel IP 1210/1220/1230
            phone = new DetectedPhone();

            // MAC, Model, & Version
            phone.mac = extractMac(path, NORTEL_IP_12X0_PATH_PREFIX);
            if (null == phone.mac || !extractNortelIp12X0ModelAndVersion(phone, useragent)) {
                writeUiForwardResponse(request, response);
                return;
            }
        } else if (path.startsWith("/debug") && m_config.isDebugOn()) {
            // Debugging.
            writeDebuggingResponse(request, response);
            return;
        } else if (path.contains("firmwareUpdate")) {
            phone = new DetectedPhone();
            extractPolycomModelAndVersion(phone, useragent);
            phone.mac = extractMac(path, POLYCOM_PATH_PREFIX);
            if (null != phone.mac && extractPolycomModelAndVersion(phone, useragent)) {
                doUpdatePhone(phone.mac, phone.version, phone.model.sipxconfig_id, response);
            }
            return;
        }
        /*
         * TODO: Test case to catch this problem.... else { writeUiForwardResponse(request,
         * response); return; }
         */
        // Provision the phone with sipXconfig. (If it wasn't a provision trigger path, then phone
        // will
        // be null, which fails cleanly.)
        doProvisionPhone(phone);

        // Return the appropriate actual profile content.
        response.setContentType("application/text");
        if (isPolycomConfigurationFilePath(path)) {

            if (POLYCOM_000000000000_CONTACTS_PATH_RE.matcher(path).matches()) {

                // This file won't be found, so save the REST request overhead.
                response.sendError(HttpURLConnection.HTTP_NOT_FOUND);
            } else if (POLYCOM_OVERRIDES_PATH_RE.matcher(path).matches()) {

                // sipXconfig doesn't currently generate this Polycom config file. (See XX-5450)
                PrintWriter out = response.getWriter();
                out.println("<?xml version=\"1.0\" standalone=\"yes\"?>");
                out.println("<PHONE_CONFIG><OVERRIDES/></PHONE_CONFIG>");
            } else {
                writeProfileConfigurationResponse(path, extractMac(path, POLYCOM_PATH_PREFIX), response);
            }
        } else if (isNortelIp12x0ConfigurationFilePath(path)) {

            int index = path.indexOf(NORTEL_IP_12X0_PATH_PREFIX);
            if (-1 != index) {
                path = path.substring(index, path.length());
            }

            writeProfileConfigurationResponse(path, extractMac(path, NORTEL_IP_12X0_PATH_PREFIX), response);
        } else {

            // Unknown configuration file path. (Wouldn't know how to extract a MAC from the
            // path.)
            writeUiForwardResponse(request, response);
            return;
        }
    }

    protected boolean writeProfileConfigurationResponse(String path, String mac, HttpServletResponse response) {

        boolean success = false;
        try {
            LOG.info("writeConfigurationResponse - " + path);

            String encoded_path = new String() + path.charAt(0) + URLEncoder.encode(path.substring(1), "UTF-8");

            // Construct the REST request.
            String uri = String.format("%s/rest/phone/%s/profile%s", m_config.getConfigurationUri(), mac,
                    encoded_path);
            HttpURLConnection connection = createRestConnection("GET", uri);

            // Do the HTTPS GET, and write the content.
            connection.connect();
            IOUtils.copy(connection.getInputStream(), response.getOutputStream());

            // Record the result. (Only transport, internal, etc. errors will be reported.
            // Duplicate and/or invalid MACs for example will result in 200 OK.)
            if (HttpsURLConnection.HTTP_OK == connection.getResponseCode()) {
                success = true;
                LOG.info("REST HTTPS GET success.");
            } else {
                LOG.error("REST HTTPS GET failed:" + connection.getResponseCode() + " - "
                        + connection.getResponseMessage());
            }

        } catch (Exception e) {
            LOG.error("REST HTTPS GET failed:", e);
        }

        return success;
    }

    protected static boolean extractPolycomModelAndVersion(DetectedPhone phone, String useragent) {

        if (null == useragent || null == phone
        // TODO regexp match, makes below parsing easier --> !
        // POLYCOM_UA_RE.matcher(useragent).matches()
        ) {
            return false;
        }

        // Model
        int i1 = useragent.indexOf("-");
        int i2 = useragent.indexOf("-", i1 + 1);
        if (-1 == i1 || -1 == i2) {
            return false;
        }
        String model_id = useragent.substring(i1 + 1, i2);
        phone.model = lookupPhoneModel(model_id);
        if (null == phone.model) {
            return false;
        }

        // Version (optional)
        i1 = useragent.indexOf(POLYCOM_UA_DELIMITER);
        if (-1 != i1) {
            i2 = useragent.indexOf(" ", i1 + POLYCOM_UA_DELIMITER.length());
            if (-1 == i2) {
                i2 = useragent.length();
            }
            phone.version = useragent.substring(i1 + POLYCOM_UA_DELIMITER.length(), i2);
        }

        return true;
    }

    protected static boolean extractNortelIp12X0ModelAndVersion(DetectedPhone phone, String useragent) {

        if (null == useragent || null == phone || !NORTEL12X0_UA_RE.matcher(useragent).matches()) {
            return false;
        }

        // Model
        String model_string = String.format("12%c0", useragent.charAt(NORTEL12X0_UA_PREFIX_STR.length()));
        phone.model = lookupPhoneModel(model_string);
        if (null == phone.model) {
            return false;
        }

        // Version
        int i1 = useragent.indexOf("(");
        phone.version = useragent.substring(i1 + 1, useragent.indexOf(")", i1 + 1));

        return true;
    }

    // TODO - test cases for various problems that could occur with
    private static void writeUiForwardResponse(HttpServletRequest request, HttpServletResponse response)
            throws IOException {

        LOG.debug("Un-recognized user-agent or path.  Redirecting to sipXconfig.");

        // Try to redirect with the servername used in the request. Helpful if the client can't
        // resolve the FQDN that we're using.
        String redirect_location = m_config.getConfigurationUri();
        try {
            URI config_uri = new URI(redirect_location);
            URI redirect_uri = new URI(config_uri.getScheme(), null, request.getServerName(), config_uri.getPort(),
                    config_uri.getPath(), null, null);

            redirect_location = redirect_uri.toString();
        } catch (URISyntaxException e) {
            LOG.debug("Failed to customize re-direct URI based on request parameters: ", e);
        }

        // The JavaScript to redirect.
        response.setContentType("text/html");
        PrintWriter out = response.getWriter();
        out.println("<html><head><script language=\"JavaScript\">");
        out.println("<!-- Hide script from old browsers");
        out.println("location = '" + redirect_location + "'");
        out.println("// End script hiding from old browsers -->");
        out.println("</script></head><body></body></html>");
    }

    public static class PhoneModel {
        PhoneModel(String id, String label) {
            sipxconfig_id = id;
            full_label = label;
        }

        public final String sipxconfig_id;
        public final String full_label;
    }

    public static class DetectedPhone {
        public PhoneModel model = new PhoneModel("unknown", "unknown");
        public String version = new String("unknown");
        public String mac = null;

        @Override
        public String toString() {
            return "id: " + getUniqueId(mac) + "  mac: " + mac + "  version: " + version + "  model: "
                    + model.full_label;
        }
    }

    static public PhoneModel lookupPhoneModel(String index) {
        return PHONE_MODEL_MAP.get(index);
    }

    private static final HashMap<String, PhoneModel> PHONE_MODEL_MAP;
    static {
        PHONE_MODEL_MAP = new HashMap<String, PhoneModel>();

        // Polycom SoundPoing IP family, see:
        // - http://wiki.sipfoundry.org/display/xecsuser/Polycom
        // - plugins/polycom/src/org/sipfoundry/sipxconfig/phone/polycom/polycom-models.beans.xml
        PHONE_MODEL_MAP.put("SPIP_300", new PhoneModel("polycom300", "SoundPoint IP 300"));
        PHONE_MODEL_MAP.put("SPIP_301", new PhoneModel("polycom301", "SoundPoint IP 301"));

        PHONE_MODEL_MAP.put("SPIP_320", new PhoneModel("polycom320", "SoundPoint IP 320"));
        PHONE_MODEL_MAP.put("SPIP_330", new PhoneModel("polycom330", "SoundPoint IP 330"));

        PHONE_MODEL_MAP.put("SPIP_321", new PhoneModel("polycom321", "SoundPoint IP 321"));

        PHONE_MODEL_MAP.put("SPIP_331", new PhoneModel("polycom331", "SoundPoint IP 331"));

        PHONE_MODEL_MAP.put("SPIP_335", new PhoneModel("polycom335", "SoundPoint IP 335"));

        PHONE_MODEL_MAP.put("SPIP_430", new PhoneModel("polycom430", "SoundPoint IP 430"));

        PHONE_MODEL_MAP.put("SPIP_450", new PhoneModel("polycom450", "SoundPoint IP 450"));

        PHONE_MODEL_MAP.put("SPIP_500", new PhoneModel("polycom500", "SoundPoint IP 500"));
        PHONE_MODEL_MAP.put("SPIP_501", new PhoneModel("polycom501", "SoundPoint IP 501"));

        PHONE_MODEL_MAP.put("SPIP_550", new PhoneModel("polycom550", "SoundPoint IP 550"));
        PHONE_MODEL_MAP.put("SPIP_560", new PhoneModel("polycom560", "SoundPoint IP 560"));

        PHONE_MODEL_MAP.put("SPIP_600", new PhoneModel("polycom600", "SoundPoint IP 600"));
        PHONE_MODEL_MAP.put("SPIP_601", new PhoneModel("polycom601", "SoundPoint IP 601"));

        PHONE_MODEL_MAP.put("SPIP_650", new PhoneModel("polycom650", "SoundPoint IP 650"));
        PHONE_MODEL_MAP.put("SPIP_670", new PhoneModel("polycom650", "SoundPoint IP 670"));

        PHONE_MODEL_MAP.put("SSIP_4000", new PhoneModel("polycom4000", "SoundStation IP 4000"));

        PHONE_MODEL_MAP.put("SSIP_5000", new PhoneModel("polycom5000", "SoundStation IP 5000"));

        PHONE_MODEL_MAP.put("SSIP_6000", new PhoneModel("polycom6000", "SoundStation IP 6000"));

        PHONE_MODEL_MAP.put("SSIP_7000", new PhoneModel("polycom7000", "SoundStation IP 7000"));

        PHONE_MODEL_MAP.put("VVX_1500", new PhoneModel("polycomVVX1500", "Polycom VVX 1500"));

        PHONE_MODEL_MAP.put("VVX_500", new PhoneModel("polycomVVX500", "Polycom VVX 500"));
        PHONE_MODEL_MAP.put("VVX_600", new PhoneModel("polycomVVX600", "Polycom VVX 600"));

        // Nortel IP 12x0, see:
        // -
        // plugins/nortel12x0/src/org/sipfoundry/sipxconfig/phone/nortel12x0/nortel12x0-models.beans.xml
        PHONE_MODEL_MAP.put("1210", new PhoneModel("avaya-1210", "Avaya 1210 IP Deskphone"));
        PHONE_MODEL_MAP.put("1220", new PhoneModel("avaya-1220", "Avaya 1220 IP Deskphone"));
        PHONE_MODEL_MAP.put("1230", new PhoneModel("avaya-1230", "Avaya 1230 IP Deskphone"));
    }

    public static void main(String[] args) throws Exception {

        // Send log4j output to the console.
        org.apache.log4j.BasicConfigurator.configure();

        System.out.println("MAIN - " + Servlet.class.getCanonicalName().toString());

        Velocity.init();

        System.out.println("Velocity initialized...");
    }
}