/*
 * Copyright 2004-2014 ICEsoft Technologies Canada Corp.
 *
 * Licensed 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.icefaces.samples.showcase.util;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.logging.LogManager;
import java.util.logging.Logger;

import javax.annotation.PostConstruct;
import javax.faces.bean.ApplicationScoped;
import javax.faces.bean.ManagedBean;
import javax.faces.context.FacesContext;
import javax.servlet.http.HttpServletRequest;

@ManagedBean
@ApplicationScoped  
/**
 * Manages and loads a cache of formatted source files rendered by the 
 * SourceCodeServlet.
 *
 * Path to an instance of SourceCodeLoaderServlet must be set by the 
 * context-param SOURCE_SERVLET_URL in web.xml.
 *
 * This class is intended to be called as if it were a map via EL.
 */
public class SourceCodeLoaderConnection implements Map, Serializable{
    // The max number of source code strings (one per file) to store 
    private static final Integer MAX_CACHED_SOURCES = 50;
    // String URL to the local instance of SourceCodeLoaderServlet
    private static String SOURCE_SERVLET_URL;
    
    private boolean brokenUrl = false;
    // Cached source code so we don't have to re-read strings forever
    // Note we use a LinkedHashMap so the order is predictable, for removing in case we reach full size
    // Uses the source code path as a the key, and the value is the formatted source
    private Map<String,String> cache = new LinkedHashMap<String,String>(MAX_CACHED_SOURCES);
    // Logger reference
    private Logger logger;
	// true if initial request was made using SSL
	private static boolean IS_SECURE;

    public SourceCodeLoaderConnection() {}         

    // Set logger, load the Servlet URL and cache size contstants from web.xml
    @PostConstruct
    private void initialize() {
        // If no logger exists for this class, create and add a new one
        if ((logger = LogManager.getLogManager().getLogger(this.getClass().getName())) == null) {
            logger = Logger.getLogger(this.getClass().getName());
            LogManager.getLogManager().addLogger(logger);
        }

        // Next we'll try to build a URL to load the source from
        // This is basically http://localhost:8080/showcase/, but the port and address could change
        // We'll try to dynamically get this from the ExternalContext request, otherwise we'll check it from the web.xml
        //  and finally just default it
        Object request = FacesContext.getCurrentInstance().getExternalContext().getRequest();
        if ((request != null) && (request instanceof HttpServletRequest)) {
            HttpServletRequest httpRequest = (HttpServletRequest)request;
            String hostName = "localhost";
            try {
            	//construct a url (SOURCE_SERVLET_URL) from getLocalName is not reliable
            	//should use host from requestURL.
            	URL requestUrl = new URL(httpRequest.getRequestURL().toString());
            	hostName = requestUrl.getHost();
            } catch (MalformedURLException mfException) {}
			IS_SECURE = httpRequest.isSecure();
            String protocol = IS_SECURE ? "https://" : "http://";
            SOURCE_SERVLET_URL = protocol +
                                 hostName +
                                 ":" +
                                 httpRequest.getLocalPort() +
                                 httpRequest.getContextPath() +
                                 "/";
        }
        else {
            String fromFile = FacesUtils.getFacesParameter("org.icefaces.samples.showcase.SOURCE_SERVLET_URL");
            if (fromFile != null) {
                SOURCE_SERVLET_URL = fromFile;
            }
            else {
                SOURCE_SERVLET_URL = "http://localhost:8080/showcase/";
            }
        }
        SOURCE_SERVLET_URL += "sourcecodeStream.html?path=";
    }

    /**
     * Returns formatted source located in the cache, or adds to the cache and
     * returns the source found at sourceCodePath, removing oldest cached files
     * if MAX_CACHE_SIZE exceeded.           
     * 
     * Implementing the map interface method `get` to allow parameter passing 
     * in EL versions prior to 2.0
     *
     * @param sourceCodePathObj The String location of the source file relative
     * to the web application root.
     * @return The XHTML formatted source code or a stack trace if the URL 
     * could not be UTF-8 encoded.
     */
    public String get(Object sourceCodePathObj) {
        if (SOURCE_SERVLET_URL == null || !(sourceCodePathObj instanceof String)) {
            return null;
        }
        
        String sourceCodePath;
        // Try encoding sourceCodePathObj parameter, return a stack trace
        // instead of source if it fails.
        try {
            sourceCodePath = URLEncoder.encode((String)sourceCodePathObj, "UTF-8");
        } catch (UnsupportedEncodingException e) {
           logger.severe("UTF-8 is not supported by this platform.");
           return "";
        }
        
        // Check our cache for the matching source file, to avoid re-reading it
        synchronized(cache) {
        	String cachedSource = null;
        	if ((cachedSource = cache.get(sourceCodePath)) != null) {
        		return cachedSource;
        	}
        }
        
        URL servletUrl = null;
        InputStream inputStream = null;
        InputStreamReader inputReader = null;
        try{
			if (!IS_SECURE) {
				servletUrl = new URL(SOURCE_SERVLET_URL + sourceCodePath);
				inputStream = (InputStream)servletUrl.getContent();
			} else {
				// don't use a connection, just access methods directly
				inputStream = SourceCodeLoaderServlet.getServlet().getSource((String)sourceCodePathObj);
			}
            
            brokenUrl = false;
        } catch (Exception e) {
            e.printStackTrace();
            logger.severe("Broken URL for the source code loader (" + SOURCE_SERVLET_URL + "), check your web.xml.");
            brokenUrl = true;
        }
        
        if (!brokenUrl) {
            try {
                // Set up streams and buffers for source servlet reading
                inputReader = new InputStreamReader(inputStream, "UTF-8"); 
                StringBuilder buf = new StringBuilder(16384);
    
                // Read into stringBuilder until EOF
                int readChar;
                while ((readChar = inputReader.read()) != -1) { 
                    buf.append((char)readChar);
                }
    
                // Extract page content from <body> tag and fix nbsp for valid XHTML
                String ret = buf.indexOf("&nbsp;") != -1
                              ?
                              buf.substring(buf.indexOf("<body>")+6, buf.lastIndexOf("</body>")).replace("&nbsp;","&#160;")
                              :
                              buf.toString();
                
                // Add the formatted source string to our cache
                if (cache != null) {
                	// If we're full remove the first item by key
                	if (cache.size() >= MAX_CACHED_SOURCES) {
                		logger.fine("Cache is full (size " + MAX_CACHED_SOURCES + "), removing an element to make space");
                		cache.remove(cache.keySet().iterator().next());
                	}
                	
                	synchronized(cache) {
                		cache.put(sourceCodePath, ret);
                	}
                }
                
                return ret; 
            } catch (MalformedURLException e) {
               logger.severe("Attempted to connect to malformed URL.");
               logger.severe("Likely either EL param or web.xml SOURCE_SERVLET_URL param is incorrectly set");
               e.printStackTrace();
            } catch (UnsupportedEncodingException e) {
               logger.severe("UTF-8 is not supported by this platform.");
               e.printStackTrace();
            } catch (IOException e) {
               logger.severe("IOException raised while reading characters from Servlet response stream.");
               e.printStackTrace();
            } catch (Exception e) {
               e.printStackTrace();
            } finally {
                if (inputStream != null) {
                    try {
                        inputStream.close();
                    } catch (Exception ignoredClose) { }
                }
                if (inputReader != null) {
                    try {
                        inputReader.close();
                    } catch (Exception ignoredClose) { }
                }
            }
        }
        
        return "";
    }
    
    /**
    * Map interface stub
    */
    public Collection values() {return null;}
    public Object put(Object key, Object value) {return null;}
    public Set keySet() {return null;}
    public boolean isEmpty() {return false;}
    public int size() {return 0;}
    public void putAll(Map t) {}
    public void clear() {}
    public boolean containsValue(Object value) {return false;}
    public Object remove(Object key) {return null;  }
    public boolean containsKey(Object key) {return false;}
    public Set entrySet() {return null;}    
}
