/* (c) 2014 - 2015 Open Source Geospatial Foundation - all rights reserved
* (c) 2014 OpenPlans
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.wms.capabilities;
import it.geosolutions.imageio.plugins.png.PNGWriter;
import java.awt.Dimension;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.lang.ArrayUtils;
import org.geoserver.catalog.Catalog;
import org.geoserver.catalog.CatalogException;
import org.geoserver.catalog.StyleInfo;
import org.geoserver.catalog.event.CatalogAddEvent;
import org.geoserver.catalog.event.CatalogListener;
import org.geoserver.catalog.event.CatalogModifyEvent;
import org.geoserver.catalog.event.CatalogPostModifyEvent;
import org.geoserver.catalog.event.CatalogRemoveEvent;
import org.geoserver.config.impl.GeoServerLifecycleHandler;
import org.geoserver.platform.GeoServerResourceLoader;
import org.geoserver.platform.resource.Paths;
import org.geoserver.platform.resource.Resource;
import org.geoserver.platform.resource.Resource.Type;
import org.geoserver.wms.GetLegendGraphicOutputFormat;
import org.geoserver.wms.GetLegendGraphicRequest;
import org.geoserver.wms.legendgraphic.BufferedImageLegendGraphic;
import org.geoserver.wms.legendgraphic.PNGLegendOutputFormat;
import org.opengis.feature.type.FeatureType;
import ar.com.hjg.pngj.FilterType;
import ar.com.hjg.pngj.PngReader;
/**
* Default implementation of LegendSample. Implements samples caching to disk
* (png file on a dedicated data_dir folder for each sld file),
* to be able to read the dimensions when needed. Implements CatalogListener
* to be notified of style changes and update the cache accordingly and
* GeoServerLifecycleHandler to handle catalog reload events.
*
* @author Mauro Bartolomeoli (mauro.bartolomeoli @ geo-solutions.it)
*/
public class LegendSampleImpl implements CatalogListener, LegendSample,
GeoServerLifecycleHandler {
public static final String LEGEND_SAMPLES_FOLDER = "legendsamples";
private static final Logger LOGGER = org.geotools.util.logging.Logging
.getLogger(LegendSampleImpl.class.getPackage().getName());
private static final String DEFAULT_SAMPLE_FORMAT = "png";
private Catalog catalog;
private GeoServerResourceLoader loader;
private Set<String> invalidated = new HashSet<String>();
Resource baseDir;
public LegendSampleImpl(Catalog catalog, GeoServerResourceLoader loader) {
super();
this.catalog = catalog;
this.loader = loader;
this.baseDir = loader.get(Paths.BASE);
this.clean();
this.catalog.addListener(this);
}
/**
* Clean up no more valid samples: SLD updated from latest sample creation.
*/
private void clean() {
for (StyleInfo style : catalog.getStyles()) {
synchronized (style) {
Resource styleResource = getStyleResource(style);
Resource sampleFile;
try {
// remove old samples
sampleFile = getSampleFile(style);
if (isStyleNewerThanSample(styleResource, sampleFile)) {
sampleFile.delete();
}
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Error cleaning invalid legend sample for " + style.getName(), e);
}
}
}
invalidated = new HashSet<String>();
}
/**
* Checks if the given SLD resource is newer than the given sample file.
*
* @param styleResource
* @param sampleFile
*
*/
private boolean isStyleNewerThanSample(Resource styleResource,
Resource sampleFile) {
return styleResource != null
&& styleResource.getType() == Resource.Type.RESOURCE
&& styleResource.lastmodified() > sampleFile.lastmodified();
}
/**
* Returns the cached sample for the given file, if
* it exists, null otherwise.
*
* @param style
*
* @throws IOException
*/
private Resource getSampleFile(StyleInfo style) throws IOException {
String fileName = getSampleFileName(style);
return getSampleFile(fileName);
}
/**
* Returns the cached sample with the given name.
*
* @param fileName
*
*/
private Resource getSampleFile(String fileName) {
return getSamplesFolder().get(fileName);
}
/**
* Gets a unique fileName for a sample.
*
* @param style
*
*/
private String getSampleFileName(StyleInfo style) {
String prefix = "";
if (style.getWorkspace() != null) {
prefix = style.getWorkspace().getName() + "_";
}
String fileName = prefix + style.getName() + "." + DEFAULT_SAMPLE_FORMAT;
return fileName;
}
/**
* Gets an SLD resource for the given style.
*
* @param style
*
*/
private Resource getStyleResource(StyleInfo style) {
String[] prefix = new String[0];
if (style.getWorkspace() != null) {
prefix = new String[] { "workspaces", style.getWorkspace().getName() };
}
String fileName = style.getFilename();
String[] pathParts = (String[]) ArrayUtils.addAll(prefix, new String[] {
"styles", fileName });
String path = Paths.path(pathParts);
return loader.get(path);
}
/**
* Calculates legendURL size (width x height) for the given style.
*
* @param style
* @return legend dimensions
* @throws IOException
*/
public Dimension getLegendURLSize(StyleInfo style) throws Exception {
synchronized (style) {
GetLegendGraphicOutputFormat pngOutputFormat = new PNGLegendOutputFormat();
Resource sampleLegend = getSampleFile(style);
if (isSampleExisting(sampleLegend)
&& !isStyleSampleInvalid(style)) {
// using existing sample if sld has not been updated from
// latest sample update
return getSizeFromSample(sampleLegend);
} else {
// generates a new sample, and save it on disk (in the dedicated folder) for
// later usage
return createNewSample(style, pngOutputFormat);
}
}
}
private boolean isSampleExisting(Resource sampleLegend) {
return sampleLegend != null && sampleLegend.getType() == Type.RESOURCE;
}
/**
* Creates a new sample file for the given style and stores
* it on disk.
* The sample dimensions (width x height) are returned.
*
* @param style
* @param pngOutputFormat
*
*/
private Dimension createNewSample(StyleInfo style,
GetLegendGraphicOutputFormat pngOutputFormat) throws Exception {
GetLegendGraphicRequest legendGraphicRequest = new GetLegendGraphicRequest();
Resource sampleLegendFolder = getSamplesFolder();
legendGraphicRequest.setStrict(false);
legendGraphicRequest.setLayer((FeatureType) null);
legendGraphicRequest.setStyle(style.getStyle());
legendGraphicRequest.setFormat(pngOutputFormat.getContentType());
Object legendGraphic = pngOutputFormat
.produceLegendGraphic(legendGraphicRequest);
if (legendGraphic instanceof BufferedImageLegendGraphic) {
BufferedImage image = ((BufferedImageLegendGraphic) legendGraphic)
.getLegend();
PNGWriter writer = new PNGWriter();
OutputStream outStream = null;
try {
Resource sampleFile = sampleLegendFolder.get(getSampleFileName(style));
outStream = sampleFile.out();
writer.writePNG(image, outStream, 0.0f, FilterType.FILTER_NONE);
removeStyleSampleInvalidation(style);
return new Dimension(image.getWidth(), image.getHeight());
} finally {
if(outStream != null) {
outStream.close();
}
}
}
return null;
}
private Resource getSamplesFolder() {
return baseDir.get(LEGEND_SAMPLES_FOLDER);
}
/**
*
* @param sampleLegendFile
*
*/
private Dimension getSizeFromSample(Resource sampleLegendFile) {
PngReader pngReader = null;
try {
// reads size using PNGJ reader, that can read metadata without reading
// the full image
pngReader = new PngReader(sampleLegendFile.file());
return new Dimension(pngReader.imgInfo.cols, pngReader.imgInfo.rows);
} finally {
if (pngReader != null) {
pngReader.close();
}
}
}
@Override
public void handleAddEvent(CatalogAddEvent event) throws CatalogException {
}
@Override
public void handleRemoveEvent(CatalogRemoveEvent event) throws CatalogException {
if (event.getSource() instanceof StyleInfo) {
// invalidate removed styles (is this needed?)
invalidateStyleSample((StyleInfo) event.getSource());
}
}
@Override
public void handleModifyEvent(CatalogModifyEvent event) throws CatalogException {
}
@Override
public void handlePostModifyEvent(CatalogPostModifyEvent event)
throws CatalogException {
if (event.getSource() instanceof StyleInfo) {
// invalidate updated styles
invalidateStyleSample((StyleInfo) event.getSource());
}
}
/**
* Set the given style sample as invalid.
*
* @param event
*/
private void invalidateStyleSample(StyleInfo style) {
synchronized (style) {
invalidated.add(getStyleName(style));
}
}
/**
* Remove the given style sample from invalid ones.
*
* @param style
*/
private void removeStyleSampleInvalidation(StyleInfo style) {
invalidated.remove(getStyleName(style));
}
/**
* Checks if the given style sample is marked as invalid.
*
* @param style
*
*/
private boolean isStyleSampleInvalid(StyleInfo style) {
return invalidated.contains(getStyleName(style));
}
/**
* Gets a unique name for a style, considering the workspace definition, in the
* form worspacename:stylename (or stylename if the style is global).
*
* @param styleInfo
*
*/
private String getStyleName(StyleInfo styleInfo) {
return styleInfo.getWorkspace() != null ? (styleInfo.getWorkspace()
.getName() + ":" + styleInfo.getName()) : styleInfo.getName();
}
@Override
public void reloaded() {
clean();
}
@Override
public void onReset() {
}
@Override
public void onDispose() {
catalog.removeListener( this );
}
@Override
public void beforeReload() {
}
@Override
public void onReload() {
reloaded();
}
}