/******************************************************************************* * * Copyright 2011-2014 Spiffy UI Team * * 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.spiffyui.spsample.server; import java.io.InputStream; import java.io.InputStreamReader; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.StringWriter; import java.io.Writer; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.Locale; import java.util.Map; import java.util.ResourceBundle; import java.util.Set; import java.util.logging.Logger; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.xml.stream.XMLEventFactory; import javax.xml.stream.XMLEventWriter; import javax.xml.stream.XMLOutputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.stream.events.Attribute; import javax.xml.stream.events.Characters; import javax.xml.stream.events.Namespace; import javax.xml.stream.events.StartDocument; import javax.xml.stream.events.StartElement; import javax.xml.stream.events.XMLEvent; /** * The main purpose of this servlet is to improve our SEO (Search * Engine Optimization). To that end it server three basic functions: * * 1. Generate a Google Sitemap for our site. Since * the entire application is generated with JavaScript the search * engines can't index the site very well. This servlet generates * a sitemap.xml file and adds the source HTML files to it. That * makes it possible to index all of the content in those files. * * 2. Server the index.html and index-debug.html pages. This servet * handles both pages since we need to serve at those URL to fulfill * the contract for AJAX crawling outlined here: * * http://code.google.com/web/ajaxcrawling/ * * This is especially complicated since Google AppEngine won't allow us * to serve any page with a servlet if there is a real file at that URL. * * * 3. Server the individual files or HTML snapshots of each page when * requested with the _escaped_fragment_ fragment parameter. */ public class SiteMapServlet extends HttpServlet { private static final Map<String, String> HASHES = new HashMap<String, String>(); static { HASHES.put("landing", "/ajax/LandingPanel.html"); HASHES.put("overview", "/ajax/OverviewPanel.html"); HASHES.put("getStarted", "/ajax/GetStartedPanel.html"); HASHES.put("hostedMode", "/ajax/HostedModePanel.html"); HASHES.put("css", "/ajax/CSSPanel.html"); HASHES.put("speed", "/ajax/BuildPanel.html"); HASHES.put("rest", "/ajax/RESTPanel.html"); HASHES.put("samples", "/ajax/SamplesPanel.html"); HASHES.put("help", "/ajax/HelpPanel.html"); HASHES.put("getInvolved", "/ajax/GetInvolvedPanel.html"); HASHES.put("auth", "/ajax/AuthPanel.html"); HASHES.put("l10n", "/ajax/DatePanel.html"); HASHES.put("forms", "/ajax/FormPanel.html"); HASHES.put("widgets", "/ajax/WidgetsPanel.html"); HASHES.put("authTest", "/ajax/AuthPanel.html"); HASHES.put("license", "/ajax/LicensePanel.html"); } private static final Logger LOGGER = Logger.getLogger(SiteMapServlet.class.getName()); private static final long serialVersionUID = -1L; private static final ResourceBundle BUILD_BUNDLE = ResourceBundle.getBundle("org/spiffyui/spsample/server/buildnum", Locale.getDefault()); private static String g_siteMap = null; @Override public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { if (request.getParameter("_escaped_fragment_") != null) { /* This means someone is the special escaped fragment version. This is probably a search engine and we'll give them the file */ String file = request.getParameter("_escaped_fragment_"); if (file.startsWith("b=")) { file = file.substring(2); } returnFile(file, response); return; } else if (request.getRequestURI().indexOf("sitemap.xml") > -1) { response.setContentType("text/xml"); PrintWriter out = new PrintWriter(new OutputStreamWriter(response.getOutputStream(), "UTF-8")); try { if (g_siteMap == null) { /* If we haven't created the sitemap yet we'll create it and cache it */ createSiteMap(request); } } catch (Exception e) { LOGGER.throwing(SiteMapServlet.class.getName(), "service", e); e.printStackTrace(); } out.write(g_siteMap); out.flush(); } else { returnFile(request.getServletPath(), response); } } private void returnFile(String name, HttpServletResponse response) throws ServletException, IOException { boolean addHeader = false; String file = null; if (name.equals("") || name.equals("/") || name.equals("index.html") || name.equals("/index.html")) { /* Google AppEngine won't let us use a servlet in place if a real file. The solution is to serve at index.html, but return the contents of the real file at index.htm. This is super hacky, but I can't find a better way around it. Hackito Ergo Sum. */ file = "/index.htm"; } else if (name.equals("index-debug.html") || name.equals("/index-debug.html")) { file = "/index-debug.htm"; } else if (HASHES.containsKey(name)) { addHeader = true; file = HASHES.get(name); } if (file == null) { file = name; } response.setContentType("text/html; charset=utf-8"); InputStream in = getServletConfig().getServletContext().getResourceAsStream(file); if (in == null) { /* This means they requested a file that doesn't exist so they get a 404 */ in = getServletConfig().getServletContext().getResourceAsStream("/404.html"); response.setStatus(HttpServletResponse.SC_NOT_FOUND); } else { response.setStatus(HttpServletResponse.SC_OK); } InputStreamReader reader = new InputStreamReader(in, "UTF-8"); try { char buf[] = new char[512]; OutputStreamWriter out = new OutputStreamWriter(response.getOutputStream(), "UTF-8"); if (addHeader) { addHeader(out, name); } int numRead; while ((numRead = reader.read(buf)) >= 0) { out.write(buf, 0, numRead); } if (addHeader) { addFooter(out); } out.flush(); } finally { if (reader != null) { reader.close(); } } } private void addHeader(Writer out, String name) throws IOException { /* The snippets are not full HTML pages so we add the header information to them. */ String header = "<!DOCTYPE html>\n\n" + "<html>\n" + "<head>\n" + "<title>Spiffy UI Framework - " + name + "</title>\n" + "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\n" + //"<script type=\"text/javascript\" src=\"jquery.min.js\"></script>\n" + //"<script type=\"text/javascript\" src=\"spsample.min.js\"></script>\n" + //"<script type=\"text/javascript\">\n" + //"spiffyui.autoloadCSS = false;\n" + //"spiffyui.autoloadHTML = false;\n" + //"</script>\n" + //"</head>\n" + "<body>\n"; out.write(header); } private void addFooter(Writer out) throws IOException { /* The snippets are not full HTML pages so we add the footer information to them. */ String footer = "</body>\n" + "</html>"; out.write(footer); } private synchronized void createSiteMap(HttpServletRequest request) throws Exception { if (g_siteMap != null) { /* then we've already created the sitemap */ return; } StringWriter out = new StringWriter(); XMLOutputFactory outputFactory = XMLOutputFactory.newInstance(); XMLEventWriter eventWriter = outputFactory.createXMLEventWriter(out); XMLEventFactory eventFactory = XMLEventFactory.newInstance(); XMLEvent end = eventFactory.createDTD("\n"); StartDocument startDocument = eventFactory.createStartDocument(); eventWriter.add(startDocument); eventWriter.add(end); ArrayList<Namespace> ns = new ArrayList<Namespace>(); ArrayList<Attribute> atts = new ArrayList<Attribute>(); atts.add(eventFactory.createAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")); atts.add(eventFactory.createAttribute("xsi:schemaLocation", "http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd")); atts.add(eventFactory.createAttribute("xmlns", "http://www.sitemaps.org/schemas/sitemap/0.9")); StartElement urlSetStartElement = eventFactory.createStartElement("", "", "urlset", atts.iterator(), ns.iterator()); eventWriter.add(urlSetStartElement); eventWriter.add(end); findFiles(request, getServletConfig().getServletContext(), eventWriter); eventWriter.add(eventFactory.createEndElement("", "", "urlset")); eventWriter.add(end); eventWriter.add(eventFactory.createEndDocument()); eventWriter.close(); g_siteMap = out.toString(); } private void findFiles(HttpServletRequest request, ServletContext context, XMLEventWriter eventWriter) throws XMLStreamException { /* First we add a node for the home page */ createNode(eventWriter, request.getRequestURL().substring(0, request.getRequestURL().length() - 11), "1.0"); for (String key : HASHES.keySet()) { createNode(eventWriter, request.getRequestURL().substring(0, request.getRequestURL().length() - 11) + "#!" + key, "0.8"); } /* Now we return all the content from the JavaDoc directory */ createNode(eventWriter, request.getRequestURL().substring(0, request.getRequestURL().length() - 11) + "javadoc", "0.7"); findStaticFiles(request, context, eventWriter, "/javadoc"); /* We also want to include the content from the Maven plugin documentation */ createNode(eventWriter, request.getRequestURL().substring(0, request.getRequestURL().length() - 11) + "maven-plugin", "0.7"); findStaticFiles(request, context, eventWriter, "/maven-plugin"); } private void findStaticFiles(HttpServletRequest request, ServletContext context, XMLEventWriter eventWriter, String ref) throws XMLStreamException { Set set = context.getResourcePaths(ref); Iterator iter = set.iterator(); while (iter.hasNext()) { String file = iter.next().toString(); if (file.endsWith("/")) { /* Then this is a directory and we want to look into it */ file = file.substring(0, file.length() - 1); file = file.substring(file.lastIndexOf('/') + 1); findStaticFiles(request, context, eventWriter, ref + "/" + file); } else { if (file.endsWith(".html") || file.endsWith(".htm")) { file = file.substring(file.lastIndexOf('/')); createNode(eventWriter, request.getRequestURL().substring(0, request.getRequestURL().length() - 12) + ref + file, "0.6"); } } } } private void createNode(XMLEventWriter eventWriter, String loc, String priority) throws XMLStreamException { XMLEventFactory eventFactory = XMLEventFactory.newInstance(); XMLEvent end = eventFactory.createDTD("\n"); XMLEvent tab = eventFactory.createDTD("\t"); eventWriter.add(tab); eventWriter.add(eventFactory.createStartElement("", "", "url")); /* Generate the location element */ eventWriter.add(end); eventWriter.add(tab); eventWriter.add(tab); eventWriter.add(eventFactory.createStartElement("", "", "loc")); Characters characters = eventFactory.createCharacters(loc); eventWriter.add(characters); eventWriter.add(eventFactory.createEndElement("", "", "loc")); eventWriter.add(end); /* Add the last modification date of this page */ eventWriter.add(tab); eventWriter.add(tab); eventWriter.add(eventFactory.createStartElement("", "", "lastmod")); /* We need to use the W3C date time format here */ characters = eventFactory.createCharacters( new SimpleDateFormat("yyyy-MM-dd").format(new Date(Long.parseLong(BUILD_BUNDLE.getString("build.date"))))); eventWriter.add(characters); eventWriter.add(eventFactory.createEndElement("", "", "lastmod")); eventWriter.add(end); /* Add the change frequency */ eventWriter.add(tab); eventWriter.add(tab); eventWriter.add(eventFactory.createStartElement("", "", "changefreq")); characters = eventFactory.createCharacters("daily"); eventWriter.add(characters); eventWriter.add(eventFactory.createEndElement("", "", "changefreq")); eventWriter.add(end); /* Add the priority of this page */ eventWriter.add(tab); eventWriter.add(tab); eventWriter.add(eventFactory.createStartElement("", "", "priority")); characters = eventFactory.createCharacters(priority); eventWriter.add(characters); eventWriter.add(eventFactory.createEndElement("", "", "priority")); eventWriter.add(end); eventWriter.add(tab); eventWriter.add(eventFactory.createEndElement("", "", "url")); eventWriter.add(end); } }