/* See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * Esri Inc. 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 com.esri.gpt.control.sitemap; import com.esri.gpt.framework.collection.StringAttributeMap; import com.esri.gpt.framework.context.ConfigurationException; import com.esri.gpt.framework.context.RequestContext; import com.esri.gpt.framework.sql.BaseDao; import com.esri.gpt.framework.util.Val; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Timestamp; import java.text.SimpleDateFormat; import java.util.logging.Level; import java.util.logging.Logger; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Handles the generation of sitemap files based upon the content of the metadata catalog. */ public class SitemapHandler extends BaseDao { /** class variables ========================================================= */ /** ISO 8601 data formatter. */ private static SimpleDateFormat ISO8601FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); /** The Logger. */ private static final Logger LOGGER = Logger.getLogger(SitemapHandler.class.getName()); /** Standard XML header. */ private static String XMLHEADER = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"; /** instance variables ====================================================== */ private String baseUrl = ""; private String changefreq = "weekly"; private String documentUrlPattern = "/rest/document/{0}?f=html"; private String namespaceUri = "http://www.sitemaps.org/schemas/sitemap/0.9"; private String priority = ""; private int startRecord = -1; private int urlsPerIndexFile = 1000; private int urlsPerSitemapFile = 40000; /** constructors ============================================================ */ /** Default constructor. */ protected SitemapHandler() {} /** methods ================================================================= */ /** * Counts the number of records that are publically available for the sitemap. * @return the record count * @throws SQLException if a database related exception occurs */ private int countRecords() throws SQLException { PreparedStatement st = null; try { Connection con = this.returnConnection().getJdbcConnection(); String sql = this.makeQuery(true); this.logExpression(sql); st = con.prepareStatement(sql); ResultSet rs = st.executeQuery(); if (rs.next()) { return rs.getInt(1); } } finally { closeStatement(st); } return 0; } /** * Handles a sitemap request. * @param request the HTTP request * @param response the HTTP response * @param context the request context * @throws Exception if an exception occurs */ protected void handle(HttpServletRequest request, HttpServletResponse response, RequestContext context) throws Exception { PrintWriter out = null; try { initialize(request,response,context); out = response.getWriter(); if (this.startRecord > 0) { this.writeSitemapFile(out); } else { int recordCount = this.countRecords(); if (recordCount <= this.urlsPerSitemapFile) { this.writeSitemapFile(out); } else { this.writeIndexFile(out,recordCount); } } } finally { try {if (out != null) out.close();} catch (Exception ef) {} } } /** * Initializes the request. * @param request the HTTP request * @param response the HTTP response * @param context the request context */ private void initialize(HttpServletRequest request, HttpServletResponse response, RequestContext context) { String sTmp; int nTmp = 0; this.setRequestContext(context); StringAttributeMap params = context.getCatalogConfiguration().getParameters(); String baseContextPath = RequestContext.resolveBaseContextPath(request); this.baseUrl = baseContextPath+"/sitemap"; this.startRecord = Val.chkInt(request.getParameter("startRecord"),-1); response.setContentType("text/xml; charset=UTF-8"); sTmp = Val.chkStr(params.getValue("sitemap.baseUrl")); if (sTmp.length() > 0) this.baseUrl = sTmp; if (this.baseUrl.endsWith("&")) { this.baseUrl = this.baseUrl.substring(0,this.baseUrl.length() - 1); } if (this.baseUrl.endsWith("?")) { this.baseUrl = this.baseUrl.substring(0,this.baseUrl.length() - 1); } sTmp = Val.chkStr(params.getValue("sitemap.documentUrlPattern")); if (sTmp.length() > 0) this.documentUrlPattern = sTmp; if (!this.documentUrlPattern.startsWith("http")) { if (!this.documentUrlPattern.startsWith("/")) { this.documentUrlPattern = "/"+this.documentUrlPattern; } this.documentUrlPattern = baseContextPath+this.documentUrlPattern; } nTmp = Val.chkInt(params.getValue("sitemap.urlsPerIndexFile"),0); if ((nTmp > 0) && (nTmp < 1000)) this.urlsPerIndexFile = nTmp; nTmp = Val.chkInt(params.getValue("sitemap.urlsPerSitemapFile"),0); if ((nTmp > 0) && (nTmp < 50000)) this.urlsPerSitemapFile = nTmp; sTmp = Val.chkStr(params.getValue("sitemap.namespaceUri")); if (sTmp.length() > 0) this.namespaceUri = sTmp; sTmp = Val.chkStr(params.getValue("sitemap.changefreq")); if (sTmp.length() > 0) this.changefreq = sTmp; sTmp = Val.chkStr(params.getValue("sitemap.priority")); if (sTmp.length() > 0) this.priority = sTmp; // error check String errPfx = "gpt.xml: gptConfig/catalog/parameter/"; if (this.documentUrlPattern.indexOf("{0}") == -1) { throw new ConfigurationException(errPfx+"@key=sitemap.documentUrlPattern must contain {0}"); } } /** * Builds the SQL query string. * @param countOnly if true a COUNT(*) query is returned * @return the SQL query string */ private String makeQuery(boolean countOnly) { String resourceTable = this.getRequestContext().getCatalogConfiguration().getResourceTableName(); StringBuilder sql = new StringBuilder(); if (countOnly) { sql.append("SELECT COUNT(*) FROM "); } else { sql.append("SELECT DOCUUID,UPDATEDATE FROM "); } sql.append(resourceTable); sql.append(" WHERE "); sql.append(" AND ((APPROVALSTATUS = 'approved') OR (APPROVALSTATUS = 'reviewed'))"); sql.append(" AND (ACL IS NULL)"); return sql.toString(); } /** * Writes the sitemap index file response. * @param out the HTTP response writer * @param recordCount the number of sitemap records */ private void writeIndexFile(PrintWriter out, int recordCount) { LOGGER.log(Level.INFO, "Writing sitemap index file response, recordCount={0}", recordCount); out.println(SitemapHandler.XMLHEADER); out.println("<sitemapindex xmlns=\""+this.namespaceUri+"\">"); String url; int nWritten = 0; for (int start = 1; start <= recordCount; start += this.urlsPerSitemapFile) { if (this.baseUrl.indexOf("?") == -1) { url = this.baseUrl+"?startRecord="+start; } else { url = this.baseUrl+"&startRecord="+start; } String modified = this.toIso8601(new Timestamp(System.currentTimeMillis())); out.println("<sitemap>"); writeTag(out,"loc",url); writeTag(out,"lastmod",modified); out.println("</sitemap>"); out.flush(); nWritten++; if (nWritten >= this.urlsPerIndexFile) break; if (Thread.interrupted()) {break;} } out.println("</sitemapindex>"); } /** * Writes a sitemap file response. * @param out the HTTP response writer * @throws SQLException if a database exception occurs */ private void writeSitemapFile(PrintWriter out) throws SQLException { if (this.startRecord > 0) { LOGGER.log(Level.INFO, "Writing sitemap file response, startRecord={0}", this.startRecord); } else { LOGGER.info("Writing sitemap file response."); } PreparedStatement st = null; try { out.println(SitemapHandler.XMLHEADER); out.println("<urlset xmlns=\""+this.namespaceUri+"\">"); // construct the query int nRecord = 0; int nWritten = 0; String sql = this.makeQuery(false); // execute thq query Connection con = this.returnConnection().getJdbcConnection(); this.logExpression(sql); st = con.prepareStatement(sql); ResultSet rs = st.executeQuery(); while (rs.next()) { nRecord++; if (nRecord >= this.startRecord) { String uuid = rs.getString(1); // determine the document URL String url = ""; try {url = this.documentUrlPattern.replace("{0}",URLEncoder.encode(uuid,"UTF-8"));} catch (UnsupportedEncodingException uee) {} // determine the modification timestamp Timestamp tsMod = rs.getTimestamp(2); String modified = this.toIso8601(tsMod); // write the URL element out.println("<url>"); writeTag(out,"loc",url); writeTag(out,"lastmod",modified); writeTag(out,"changefreq",this.changefreq); writeTag(out,"priority",this.priority); out.println("</url>"); out.flush(); nWritten++; if (nWritten >= this.urlsPerSitemapFile) break; if (Thread.interrupted()) break; } } } finally { closeStatement(st); try {out.println("</urlset>");} catch (Exception ef) {} } } /** * Converts a Timestamp to ISO-8601 format. * @param timestamp the timestamp * @return the formatted restlt */ private String toIso8601(Timestamp timestamp) { String sTimestamp = ""; if (timestamp != null) { sTimestamp = ISO8601FORMAT.format(timestamp); sTimestamp = sTimestamp.substring(0,sTimestamp.length()-2)+":"+sTimestamp.substring(sTimestamp.length()-2); } return sTimestamp; } /** * Writes an XML tag with text node content to the response. * @param out the HTTP response writer * @param tag the name of the tag * @param content the text node content */ private void writeTag(PrintWriter out, String tag, String content) { if ((content != null) && (content.length() > 0)) { out.println("<"+tag+">"+Val.escapeXml(content)+"</"+tag+">"); } } }