Java tutorial
/* * Weblounge: Web Content Management System * Copyright (c) 2011 The Weblounge Team * http://weblounge.o2it.ch * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package ch.entwine.weblounge.preview.xhtmlrenderer; import ch.entwine.weblounge.common.content.PreviewGenerator; import ch.entwine.weblounge.common.content.Resource; import ch.entwine.weblounge.common.content.ResourceURI; import ch.entwine.weblounge.common.content.image.ImagePreviewGenerator; import ch.entwine.weblounge.common.content.image.ImageStyle; import ch.entwine.weblounge.common.content.page.Page; import ch.entwine.weblounge.common.content.page.PagePreviewGenerator; import ch.entwine.weblounge.common.impl.testing.MockHttpServletRequest; import ch.entwine.weblounge.common.impl.testing.MockHttpServletResponse; import ch.entwine.weblounge.common.impl.util.html.HTMLUtils; import ch.entwine.weblounge.common.language.Language; import ch.entwine.weblounge.common.request.WebloungeRequest; import ch.entwine.weblounge.common.site.Environment; import ch.entwine.weblounge.common.site.Site; import ch.entwine.weblounge.common.url.UrlUtils; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.params.ClientPNames; import org.apache.http.client.params.CookiePolicy; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.message.BasicHeader; import org.apache.http.util.EntityUtils; import org.htmlcleaner.CleanerProperties; import org.htmlcleaner.HtmlCleaner; import org.htmlcleaner.Serializer; import org.htmlcleaner.SimpleXmlSerializer; import org.htmlcleaner.TagNode; import org.osgi.framework.BundleContext; import org.osgi.framework.Filter; import org.osgi.framework.InvalidSyntaxException; import org.osgi.framework.ServiceReference; import org.osgi.service.component.ComponentContext; import org.osgi.util.tracker.ServiceTracker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xhtmlrenderer.swing.Java2DRenderer; import org.xhtmlrenderer.swing.NaiveUserAgent; import org.xhtmlrenderer.util.FSImageWriter; import org.xhtmlrenderer.util.XRLog; import org.xhtmlrenderer.util.XRRuntimeException; import java.awt.HeadlessException; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.servlet.Servlet; import javax.servlet.ServletException; import javax.servlet.http.HttpServletResponse; /** * A <code>PreviewGenerator</code> that will generate previews for pages. */ public class XhtmlRendererPagePreviewGenerator implements PagePreviewGenerator { /** Logger factory */ private static final Logger logger = LoggerFactory.getLogger(XhtmlRendererPagePreviewGenerator.class); /** Page request handler path prefix */ protected static final String PAGE_HANDLER_PREFIX = "/weblounge-pages/"; /** Format for the preview images */ private static final String PREVIEW_FORMAT = "png"; /** Format for the preview images */ private static final String PREVIEW_CONTENT_TYPE = "image/png"; /** Default width for taking screenshots */ private static final int DEFAULT_SCREENSHOT_WIDTH = 1024; /** Default height for taking screenshots */ private static final int DEFAULT_SCREENSHOT_HEIGHT = 768; /** The site servlets */ private static Map<String, Servlet> siteServlets = new HashMap<String, Servlet>(); /** The preview generators */ private List<ImagePreviewGenerator> previewGenerators = new ArrayList<ImagePreviewGenerator>(); /** The user agents per site */ private static Map<String, WebloungeUserAgent> userAgents = new HashMap<String, WebloungeUserAgent>(); /** Warning flags */ private boolean isRenderingEnvironmentSane = true; /** The site servlet service tracker */ private ServiceTracker siteServletTracker = null; /** The preview generator service tracker */ private ServiceTracker previewGeneratorTracker = null; /** Filter expression used to look up site servlets */ private static final String serviceFilter = "(&(objectclass=" + Servlet.class.getName() + ")(" + Site.class.getName().toLowerCase() + "=*))"; /** * Callback from OSGi declarative services on component startup. * * @param ctx * the component context */ public void activate(ComponentContext ctx) { try { Filter filter = ctx.getBundleContext().createFilter(serviceFilter); siteServletTracker = new SiteServletTracker(ctx.getBundleContext(), filter); siteServletTracker.open(); previewGeneratorTracker = new ImagePreviewGeneratorTracker(ctx.getBundleContext()); previewGeneratorTracker.open(); } catch (InvalidSyntaxException e) { throw new IllegalStateException(e); } } /** * Callback from OSGi declarative services on component shutdown. */ public void deactivate() { if (siteServletTracker != null) { siteServletTracker.close(); } if (previewGeneratorTracker != null) { previewGeneratorTracker.close(); } } /** * {@inheritDoc} * * @see ch.entwine.weblounge.common.content.PreviewGenerator#supports(ch.entwine.weblounge.common.content.Resource) */ public boolean supports(Resource<?> resource) { return (resource instanceof Page); } /** * {@inheritDoc} * * @see ch.entwine.weblounge.common.content.PreviewGenerator#supports(java.lang.String) */ public boolean supports(String format) { for (ImagePreviewGenerator generator : previewGenerators) { if (generator.supports(PREVIEW_FORMAT) && generator.supports(format)) return true; } return false; } /** * {@inheritDoc} * * @see ch.entwine.weblounge.common.content.PreviewGenerator#getPriority() */ public int getPriority() { return 0; } /** * {@inheritDoc} * * @see ch.entwine.weblounge.common.content.PreviewGenerator#createPreview(ch.entwine.weblounge.common.content.Resource, * ch.entwine.weblounge.common.site.Environment, * ch.entwine.weblounge.common.language.Language, * ch.entwine.weblounge.common.content.image.ImageStyle, String, * java.io.InputStream, java.io.OutputStream) */ public void createPreview(Resource<?> resource, Environment environment, Language language, ImageStyle style, String format, InputStream is, OutputStream os) throws IOException { if (!isRenderingEnvironmentSane) { logger.debug("Skipping page preview rendering as environment is not sane"); return; } if (resource == null) throw new IllegalArgumentException("Resource cannot be null"); ImagePreviewGenerator imagePreviewGenerator = null; synchronized (previewGenerators) { if (previewGenerators.size() == 0) { logger.debug("Unable to generate page previews since no image renderer is available"); return; } imagePreviewGenerator = previewGenerators.get(0); } ResourceURI uri = resource.getURI(); Site site = uri.getSite(); String html = null; try { URL pageURL = new URL(UrlUtils.concat(site.getHostname(environment).toExternalForm(), PAGE_HANDLER_PREFIX, uri.getIdentifier())); html = render(pageURL, site, environment, language, resource.getVersion()); if (StringUtils.isBlank(html)) { logger.warn("Error rendering preview of page " + uri.getPath()); return; } html = HTMLUtils.escapeHtml(HTMLUtils.unescape(html)); } catch (ServletException e) { logger.warn("Error rendering page " + uri.getPath(), e); throw new IOException(e); } // Try to convert html to xhtml HtmlCleaner cleaner = new HtmlCleaner(); CleanerProperties xhtmlProperties = cleaner.getProperties(); TagNode xhtmlNode = cleaner.clean(html); if (xhtmlNode == null) { logger.warn("Error creating well-formed document from page {}", resource); return; } File xhtmlFile = null; is = new ByteArrayInputStream(html.getBytes("UTF-8")); // Write the resource content to disk. This step is needed, as the preview // generator can only handle files. try { xhtmlFile = File.createTempFile("xhtml", ".xml"); Serializer xhtmlSerializer = new SimpleXmlSerializer(xhtmlProperties); xhtmlSerializer.writeToFile(xhtmlNode, xhtmlFile.getAbsolutePath(), "UTF-8"); } catch (IOException e) { logger.error("Error creating temporary copy of file content at " + xhtmlFile, e); FileUtils.deleteQuietly(xhtmlFile); throw e; } finally { IOUtils.closeQuietly(is); } File imageFile = File.createTempFile("xhtml-preview", "." + PREVIEW_FORMAT); FileOutputStream imageFos = null; // Render the page and write back to client try { int screenshotWidth = DEFAULT_SCREENSHOT_WIDTH; int screenshotHeight = DEFAULT_SCREENSHOT_HEIGHT; if (style != null && style.getWidth() > 0 && style.getHeight() > 0) { screenshotHeight = (int) ((float) screenshotWidth / (float) style.getWidth() * style.getHeight()); } // Create the renderer. Due to a synchronization bug in the software, // this needs to be synchronized Java2DRenderer renderer = null; try { synchronized (this) { renderer = new Java2DRenderer(xhtmlFile, screenshotWidth, screenshotHeight); } } catch (Throwable t) { if (isRenderingEnvironmentSane) { logger.warn("Error creating Java 2D renderer for previews: {}" + t.getMessage()); logger.warn("Page preview rendering will be switched off"); isRenderingEnvironmentSane = false; } logger.debug("Error creating Java 2D renderer for preview of page {}: {}" + uri.getPath(), t.getMessage()); return; } // Configure the renderer renderer.getSharedContext().setBaseURL(site.getHostname().toExternalForm()); renderer.getSharedContext().setInteractive(false); // Make sure the renderer is using a user agent that will correctly // resolve urls WebloungeUserAgent agent = userAgents.get(site.getIdentifier()); if (agent == null) { agent = new WebloungeUserAgent(site.getHostname().getURL()); userAgents.put(site.getIdentifier(), agent); } renderer.getSharedContext().setUserAgentCallback(agent); // Render the page to an image BufferedImage img = renderer.getImage(); FSImageWriter imageWriter = new FSImageWriter(PREVIEW_FORMAT); imageFos = new FileOutputStream(imageFile); imageWriter.write(img, imageFos); } catch (IOException e) { logger.error("Error creating temporary copy of file content at " + xhtmlFile, e); throw e; } catch (XRRuntimeException e) { logger.warn("Error rendering page content at " + uri + ": " + e.getMessage()); throw e; } catch (HeadlessException e) { logger.warn("Headless error while trying to render page preview: " + e.getMessage()); logger.warn("Page preview rendering will be switched off"); isRenderingEnvironmentSane = false; throw e; } catch (Throwable t) { logger.warn("Error rendering page content at " + uri + ": " + t.getMessage(), t); throw new IOException(t); } finally { IOUtils.closeQuietly(imageFos); FileUtils.deleteQuietly(xhtmlFile); } FileInputStream imageIs = null; // Scale the image to the correct size try { imageIs = new FileInputStream(imageFile); imagePreviewGenerator.createPreview(resource, environment, language, style, PREVIEW_FORMAT, imageIs, os); } catch (IOException e) { logger.error("Error creating temporary copy of file content at " + xhtmlFile, e); throw e; } catch (Throwable t) { logger.warn("Error scaling page preview at " + uri + ": " + t.getMessage(), t); throw new IOException(t); } finally { IOUtils.closeQuietly(imageIs); FileUtils.deleteQuietly(imageFile); } } /** * Renders the page located at <code>rendererURL</code> in the given language. * * @param rendererURL * the page url * @param site * the site * @param environment * the environment * @param language * the language * @param version * the version * @return the rendered <code>HTML</code> * @throws ServletException * if rendering fails * @throws IOException * if reading from the servlet fails */ private String render(URL rendererURL, Site site, Environment environment, Language language, long version) throws ServletException, IOException { Servlet servlet = siteServlets.get(site.getIdentifier()); String httpContextURI = UrlUtils.concat("/weblounge-sites", site.getIdentifier()); int httpContextURILength = httpContextURI.length(); String url = rendererURL.toExternalForm(); int uriInPath = url.indexOf(httpContextURI); // Are we trying to render a site resource (e. g. a jsp during // precompilation)? if (uriInPath > 0) { String pathInfo = url.substring(uriInPath + httpContextURILength); // Prepare the mock request MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); request.setServerName(site.getHostname(environment).getURL().getHost()); request.setServerPort(site.getHostname(environment).getURL().getPort()); request.setMethod(site.getHostname(environment).getURL().getProtocol()); request.setAttribute(WebloungeRequest.LANGUAGE, language); request.setPathInfo(pathInfo); request.setRequestURI(UrlUtils.concat(httpContextURI, pathInfo)); MockHttpServletResponse response = new MockHttpServletResponse(); servlet.service(request, response); return response.getContentAsString(); } else { HttpClient httpClient = new DefaultHttpClient(); httpClient.getParams().setParameter(ClientPNames.COOKIE_POLICY, CookiePolicy.BEST_MATCH); try { if (version == Resource.WORK) { rendererURL = new URL(UrlUtils.concat(rendererURL.toExternalForm(), "work_" + language.getIdentifier() + ".html")); } else { rendererURL = new URL(UrlUtils.concat(rendererURL.toExternalForm(), "index_" + language.getIdentifier() + ".html")); } HttpGet getRequest = new HttpGet(rendererURL.toExternalForm()); getRequest.addHeader(new BasicHeader("X-Weblounge-Special", "Page-Preview")); HttpResponse response = httpClient.execute(getRequest); if (response.getStatusLine().getStatusCode() != HttpServletResponse.SC_OK) return null; String responseText = EntityUtils.toString(response.getEntity(), "utf-8"); return responseText; } finally { httpClient.getConnectionManager().shutdown(); } } } /** * {@inheritDoc} * * @see ch.entwine.weblounge.common.content.PreviewGenerator#getContentType(ch.entwine.weblounge.common.content.Resource, * ch.entwine.weblounge.common.language.Language, * ch.entwine.weblounge.common.content.image.ImageStyle) */ public String getContentType(Resource<?> resource, Language language, ImageStyle style) { return PREVIEW_CONTENT_TYPE; } /** * {@inheritDoc} * * @see ch.entwine.weblounge.common.content.PreviewGenerator#getSuffix(ch.entwine.weblounge.common.content.Resource, * ch.entwine.weblounge.common.language.Language, * ch.entwine.weblounge.common.content.image.ImageStyle) */ public String getSuffix(Resource<?> resource, Language language, ImageStyle style) { return PREVIEW_FORMAT; } /** * Adds the preview generator to the list of registered preview generators. * * @param generator * the generator */ void addPreviewGenerator(ImagePreviewGenerator generator) { synchronized (previewGenerators) { previewGenerators.add(generator); Collections.sort(previewGenerators, new Comparator<PreviewGenerator>() { public int compare(PreviewGenerator a, PreviewGenerator b) { return Integer.valueOf(a.getPriority()).compareTo(b.getPriority()); } }); } } /** * Removes the preview generator from the list of registered preview * generators. * * @param generator * the generator */ void removePreviewGenerator(ImagePreviewGenerator generator) { synchronized (previewGenerators) { previewGenerators.remove(generator); } } /** * Adds the site servlet to the list of servlets. * * @param id * the site identifier * @param servlet * the site servlet */ void addSiteServlet(String id, Servlet servlet) { logger.debug("Site servlet attached to {} workbench", id); siteServlets.put(id, servlet); } /** * Removes the site servlet from the list of servlets * * @param site * the site identifier */ void removeSiteServlet(String id) { logger.debug("Site servlet detached from {} workbench", id); siteServlets.remove(id); userAgents.remove(id); } /** * Implementation of a <code>ServiceTracker</code> that is tracking instances * of type {@link Servlet} with an associated <code>site</code> attribute. */ private class SiteServletTracker extends ServiceTracker { /** * Creates a new servlet tracker that is using the given bundle context to * look up service instances. * * @param ctx * the bundle context * @param filter * the service filter */ SiteServletTracker(BundleContext ctx, Filter filter) { super(ctx, filter, null); } /** * {@inheritDoc} * * @see org.osgi.util.tracker.ServiceTracker#addingService(org.osgi.framework.ServiceReference) */ @Override public Object addingService(ServiceReference reference) { Servlet servlet = (Servlet) super.addingService(reference); String site = (String) reference.getProperty(Site.class.getName().toLowerCase()); addSiteServlet(site, servlet); return servlet; } /** * {@inheritDoc} * * @see org.osgi.util.tracker.ServiceTracker#removedService(org.osgi.framework.ServiceReference, * java.lang.Object) */ @Override public void removedService(ServiceReference reference, Object service) { String site = (String) reference.getProperty("site"); removeSiteServlet(site); } } /** * Implementation of a <code>ServiceTracker</code> that is tracking instances * of type {@link ImagePreviewGenerator} with an associated <code>site</code> * attribute. */ private class ImagePreviewGeneratorTracker extends ServiceTracker { /** * Creates a new service tracker that is using the given bundle context to * look up service instances. * * @param ctx * the bundle context */ ImagePreviewGeneratorTracker(BundleContext ctx) { super(ctx, ImagePreviewGenerator.class.getName(), null); } /** * {@inheritDoc} * * @see org.osgi.util.tracker.ServiceTracker#addingService(org.osgi.framework.ServiceReference) */ @Override public Object addingService(ServiceReference reference) { ImagePreviewGenerator previewGenerator = (ImagePreviewGenerator) super.addingService(reference); addPreviewGenerator(previewGenerator); return previewGenerator; } /** * {@inheritDoc} * * @see org.osgi.util.tracker.ServiceTracker#removedService(org.osgi.framework.ServiceReference, * java.lang.Object) */ @Override public void removedService(ServiceReference reference, Object service) { removePreviewGenerator((ImagePreviewGenerator) service); } } /** * This class provides a bug fix to the {@link NaiveUserAgent} class from the * xhtml renderer. */ static class WebloungeUserAgent extends NaiveUserAgent { /** The base URL */ private String baseURL = null; /** * Creates a user agent that will use <code>baseURL</code> to resolve uris * without a protocol (paths, that is). * * @param baseURL * the base url */ WebloungeUserAgent(URL baseURL) { this.baseURL = baseURL.toExternalForm(); } /** * {@inheritDoc} * * @see org.xhtmlrenderer.swing.NaiveUserAgent#getBaseURL() */ @Override public String getBaseURL() { return baseURL; } /** * {@inheritDoc} * * @see org.xhtmlrenderer.swing.NaiveUserAgent#resolveURI(java.lang.String) */ @Override public String resolveURI(String uri) { if (uri == null) return null; try { URL result = new URL(uri); return result.toExternalForm(); } catch (MalformedURLException e1) { try { URL result = new URL(UrlUtils.concat(baseURL, uri)); return result.toString(); } catch (MalformedURLException e2) { XRLog.exception("The default NaiveUserAgent cannot resolve the URL " + uri + " with base URL " + getBaseURL()); return null; } } } } }