<commit_before>/*
 * This is part of Geomajas, a GIS framework, http://www.geomajas.org/.
 *
 * Copyright 2008-2013 Geosparc nv, http://www.geosparc.com/, Belgium.
 *
 * The program is available in open source according to the GNU Affero
 * General Public License. All contributions in this program are covered
 * by the Geomajas Contributors License Agreement. For full licensing
 * details, see LICENSE.txt in the project root.
 */
package org.geomajas.layer.wms;

import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLEncoder;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;

import javax.annotation.PostConstruct;
import javax.imageio.ImageIO;

import org.geomajas.annotation.Api;
import org.geomajas.configuration.Parameter;
import org.geomajas.configuration.RasterLayerInfo;
import org.geomajas.configuration.client.ScaleInfo;
import org.geomajas.geometry.Bbox;
import org.geomajas.geometry.Crs;
import org.geomajas.geometry.CrsTransform;
import org.geomajas.global.ExceptionCode;
import org.geomajas.global.GeomajasException;
import org.geomajas.layer.LayerException;
import org.geomajas.layer.LayerLegendImageSupport;
import org.geomajas.layer.RasterLayer;
import org.geomajas.layer.common.proxy.LayerAuthentication;
import org.geomajas.layer.common.proxy.LayerAuthenticationMethod;
import org.geomajas.layer.common.proxy.LayerHttpService;
import org.geomajas.layer.feature.Attribute;
import org.geomajas.layer.feature.Feature;
import org.geomajas.layer.feature.attribute.StringAttribute;
import org.geomajas.layer.tile.RasterTile;
import org.geomajas.layer.tile.TileCode;
import org.geomajas.plugin.caching.service.CacheManagerService;
import org.geomajas.security.SecurityContext;
import org.geomajas.service.DispatcherUrlService;
import org.geomajas.service.DtoConverterService;
import org.geomajas.service.GeoService;
import org.geotools.GML;
import org.geotools.GML.Version;
import org.geotools.data.ows.HTTPClient;
import org.geotools.data.ows.Layer;
import org.geotools.data.ows.SimpleHttpClient;
import org.geotools.data.ows.StyleImpl;
import org.geotools.data.ows.WMSCapabilities;
import org.geotools.data.wms.WebMapServer;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.FeatureIterator;
import org.geotools.ows.ServiceException;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.geometry.MismatchedDimensionException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Envelope;

/**
 * <p>
 * Layer model for accessing raster data from WMS servers. It has support for most WMS versions: up to 1.3.0. When using
 * this layer, note that the following fields are required:
 * <ul>
 * <li><b>baseWmsUrl</b>: The base URL to the WMS server.</li>
 * <li><b>format</b>: The format for the returned images.</li>
 * <li><b>version</b>: The version of WMS to use.</li>
 * <li><b>styles</b>: The styles to use when rendering the WMS images.</li>
 * <li><b>useProxy</b>: Set to true to use a proxy for rendering the WMS, hiding the URL from the client. This
 * automatically happens when setting the authentication object.</li>
 * </ul>
 * There always is the option of adding additional parameters to the WMS GetMap requests, by filling the
 * <code>parameters</code> list. Such parameters could include optional WMS GetMap parameters, such as "transparency",
 * but also "user" and "password".
 * </p>
 * <p>
 * This layer also supports BASIC and DIGEST authentication. To use this functionality, set the
 * <code>authentication</code> field.
 * </p>
 * 
 * @author <NAME>
 * @author <NAME>
 * @author <NAME>
 * @author <NAME>
 * @since 1.7.1
 */
@Api
public class WmsLayer implements RasterLayer, LayerLegendImageSupport, LayerFeatureInfoSupport,
		LayerFeatureInfoAsHtmlSupport, LayerFeatureInfoAsGmlSupport {

	private static final String GFI_UNAVAILABLE_MSG = "GetFeatureInfo-support not available on this layer";

	private static final int DEFAULT_CAPABILITIES_CONNECTION_TIMEOUT_MS = 10000;

	private static final int EMPTY_LEGEND_WIDTH = -1;

	private static final int EMPTY_LEGEND_HEIGHT = -1;

	private static final String EMPTY_LEGENDURL = "";

	private static final boolean IS_GML_REQUEST = false;

	private static final boolean IS_HTML_REQUEST = true;
	
	private final Logger log = LoggerFactory.getLogger(WmsLayer.class);

	private final List<Resolution> resolutions = new ArrayList<Resolution>();

	// @NotNull this seems to cause problems, it is tested in @PostConstruct
	// anyway
	private String baseWmsUrl;

	private String format = "image/png";

	private String version = "1.1.1";

	private String styles = EMPTY_LEGENDURL;

	private List<Parameter> parameters;

	private boolean enableFeatureInfoSupport;

	private boolean enableFeatureInfoSupportAsHtml;

	private RasterLayerInfo layerInfo;

	private Crs crs;

	private String id;

	/**
	 * @deprecated use layerAuthentication
	 */
	@Deprecated
	private WmsAuthentication authentication;

	private LayerAuthentication layerAuthentication;

	private boolean useProxy;

	private boolean useCache;

	@Autowired
	private GeoService geoService;

	@Autowired
	private LayerHttpService httpService;

	@Autowired
	private DtoConverterService converterService;

	@Autowired(required = false)
	private DispatcherUrlService dispatcherUrlService;

	@Autowired(required = false)
	private CacheManagerService cacheManagerService;

	@Autowired
	private SecurityContext securityContext;

	private boolean enableFeatureInfoSupportAsGml;

	private String legendImageUrl;

	private int legendImageHeight;

	private int legendImageWidth;

	/**
	 * Return the layers identifier.
	 * 
	 * @return layer id
	 */
	public String getId() {
		return id;
	}

	/**
	 * Set the layer identifier.
	 * 
	 * @param id
	 *            layer id
	 */
	public void setId(String id) {
		this.id = id;
	}

	/**
	 * Return the layer info object.
	 * 
	 * @since 1.7.1
	 */
	public RasterLayerInfo getLayerInfo() {
		return layerInfo;
	}

	/**
	 * Get coordinate reference system for this layer.
	 * 
	 * @return Coordinate reference system for this layer.
	 * @deprecated use {@link org.geomajas.layer.LayerService#getCrs(org.geomajas.layer.Layer)}
	 */
	@Deprecated
	public CoordinateReferenceSystem getCrs() {
		return crs;
	}

	@PostConstruct
	protected void postConstruct() throws GeomajasException {
		if (null == baseWmsUrl) {
			throw new GeomajasException(ExceptionCode.PARAMETER_MISSING, "baseWmsUrl");
		}

		crs = geoService.getCrs2(getLayerInfo().getCrs());

		// calculate resolutions
		List<ScaleInfo> scales = layerInfo.getZoomLevels();
		if (scales != null) {
			List<Double> r = new ArrayList<Double>();
			for (ScaleInfo scale : scales) {
				r.add(1. / scale.getPixelPerUnit());
			}

			// sort in decreasing order !!!
			Collections.sort(r);
			Collections.reverse(r);

			int level = 0;
			for (double resolution : r) {
				resolutions
						.add(new Resolution(resolution, level++, layerInfo.getTileWidth(), layerInfo.getTileHeight()));
			}
		}
		retrieveAndSetLegendImageParameters(baseWmsUrl);
	}

	/**
	 * Set the layer configuration.
	 * 
	 * @param layerInfo
	 *            layer information
	 * @throws LayerException
	 *             oops
	 * @since 1.7.1
	 */
	@Api
	public void setLayerInfo(RasterLayerInfo layerInfo) throws LayerException {
		this.layerInfo = layerInfo;
	}

	/** {@inheritDoc}. */
	public List<Feature> getFeaturesByLocation(Coordinate layerCoordinate, double layerScale, int pixelTolerance)
			throws LayerException {
		if (!isEnableFeatureInfoSupport()) {
			return Collections.emptyList();
		}
		List<Feature> features = new ArrayList<Feature>();
		Resolution bestResolution = getResolutionForScale(layerScale);
		RasterGrid grid = getRasterGrid(new Envelope(layerCoordinate), bestResolution.getTileWidth(),
				bestResolution.getTileHeight());
		int x = (int) (((layerCoordinate.x - grid.getLowerLeft().x) * bestResolution.getTileWidthPx()) / grid
				.getTileWidth());
		int y = (int) (bestResolution.getTileHeightPx() - (((layerCoordinate.y - grid.getLowerLeft().y) * bestResolution
				.getTileHeightPx()) / grid.getTileHeight()));

		Bbox layerBox = new Bbox(grid.getLowerLeft().x, grid.getLowerLeft().y, grid.getTileWidth(),
				grid.getTileHeight());

		InputStream stream = null;
		try {
			String url = buildRequestUrl(layerCoordinate, layerScale, IS_GML_REQUEST);
			log.debug("getFeaturesByLocation: {} {} {} {}", new Object[] { layerCoordinate, layerScale, pixelTolerance,
					url });
			GML gml = new GML(Version.GML3);

			stream = httpService.getStream(url, getLayerAuthentication(), getId());
			FeatureCollection<?, SimpleFeature> collection = gml.decodeFeatureCollection(stream);
			FeatureIterator<SimpleFeature> it = collection.features();

			while (it.hasNext()) {
				features.add(toDto(it.next()));
			}
		} catch (LayerException le) {
			throw le; // NOSONAR
		} catch (Exception e) { // NOSONAR
			throw new LayerException(e, ExceptionCode.UNEXPECTED_PROBLEM);
		} finally {
			if (null != stream) {
				try {
					stream.close();
				} catch (IOException ioe) {
					// ignore, closing anyway
				}
			}
		}

		return features;
	}

	@Override
	public List<Feature> getFeatureInfoAsGml(Coordinate coordinate, double layerScale, int pixelTolerance)
			throws LayerException {
		if (!isEnableFeatureInfoAsGmlSupport()) {
			return Collections.emptyList();
		}
		return getFeaturesByLocation(coordinate, layerScale, pixelTolerance);
	}

	@Override
	public String getFeatureInfoAsHtml(Coordinate coordinate, double layerScale, int pixelTolerance)
			throws LayerException {
		if (!isEnableFeatureInfoAsHtmlSupport()) {
			return GFI_UNAVAILABLE_MSG;
		}
		InputStream stream = null;
		String url;
		try {
			url = buildRequestUrl(coordinate, layerScale, IS_HTML_REQUEST);

			log.debug("getFeaturesByLocation: {} {} {} {}",
					new Object[] { coordinate, layerScale, pixelTolerance, url });
			stream = httpService.getStream(url, getLayerAuthentication(), getId());
		} catch (Exception e) {
			throw new LayerException(e, ExceptionCode.UNEXPECTED_PROBLEM);
		}
		return getStringFromInputStream(stream);
	}

	private static String getStringFromInputStream(InputStream is) {

		BufferedReader br = null;
		StringBuilder sb = new StringBuilder();

		String line;
		try {

			br = new BufferedReader(new InputStreamReader(is, "utf-8"));
			while ((line = br.readLine()) != null) {
				sb.append(line);
			}

		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			if (br != null) {
				try {
					br.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}

		return sb.toString();

	}

	protected String buildRequestUrl(Coordinate layerCoordinate, double layerScale, boolean isHtmlRequest)
			throws GeomajasException {
		Resolution bestResolution = getResolutionForScale(layerScale);
		RasterGrid grid = getRasterGrid(new Envelope(layerCoordinate), bestResolution.getTileWidth(),
				bestResolution.getTileHeight());
		int x = (int) (((layerCoordinate.x - grid.getLowerLeft().x) * bestResolution.getTileWidthPx()) / grid
				.getTileWidth());
		int y = (int) (bestResolution.getTileHeightPx() - (((layerCoordinate.y - grid.getLowerLeft().y) * bestResolution
				.getTileHeightPx()) / grid.getTileHeight()));

		Bbox layerBox = new Bbox(grid.getLowerLeft().x, grid.getLowerLeft().y, grid.getTileWidth(),
				grid.getTileHeight());
		String url = formatGetFeatureInfoUrl(bestResolution.getTileWidthPx(), bestResolution.getTileHeightPx(),
				layerBox, x, y, isHtmlRequest);
		return url;
	}

	private Feature toDto(SimpleFeature feature) {
		if (feature == null) {
			return null;
		}
		Feature dto = new Feature(feature.getID());

		HashMap<String, Attribute> attributes = new HashMap<String, Attribute>();

		for (AttributeDescriptor desc : feature.getType().getAttributeDescriptors()) {
			Object obj = feature.getAttribute(desc.getName());
			if (null != obj) {
				attributes.put(desc.getLocalName(), new StringAttribute(obj.toString()));
			}
		}
		dto.setAttributes(attributes);
		dto.setId(feature.getID());

		dto.setUpdatable(false);
		dto.setDeletable(false);
		return dto;

	}

	/**
	 * Paints the specified bounds optimized for the specified scale in pixel/unit.
	 * 
	 * @param targetCrs
	 *            Coordinate reference system used for bounds
	 * @param bounds
	 *            bounds to request images for
	 * @param scale
	 *            scale or zoom level (unit?)
	 * @return a list of raster images that covers the bounds
	 * @throws GeomajasException
	 *             oops
	 */
	public List<RasterTile> paint(CoordinateReferenceSystem targetCrs, Envelope bounds, double scale)
			throws GeomajasException {
		Envelope layerBounds = bounds;
		double layerScale = scale;
		CrsTransform layerToMap = null;
		boolean needTransform = !crs.equals(targetCrs);

		try {
			// We don't necessarily need to split into same CRS and different
			// CRS cases, the latter implementation uses
			// identity transform if crs's are equal for map and layer but might
			// introduce bugs in rounding and/or
			// conversions.
			if (needTransform) {
				layerToMap = geoService.getCrsTransform(crs, targetCrs);
				CrsTransform mapToLayer = geoService.getCrsTransform(targetCrs, crs);

				// Translate the map coordinates to layer coordinates, assumes
				// equal x-y orientation
				layerBounds = geoService.transform(bounds, mapToLayer);
				layerScale = bounds.getWidth() * scale / layerBounds.getWidth();
			}
		} catch (MismatchedDimensionException e) {
			throw new GeomajasException(e, ExceptionCode.RENDER_DIMENSION_MISMATCH);
		}
		layerBounds = clipBounds(layerBounds);
		if (layerBounds.isNull()) {
			return new ArrayList<RasterTile>(0);
		}

		// Grid is in layer coordinate space!
		Resolution bestResolution = getResolutionForScale(layerScale);
		RasterGrid grid = getRasterGrid(layerBounds, bestResolution.getTileWidth(), bestResolution.getTileHeight());

		// We calculate the first tile's screen box with this assumption
		List<RasterTile> result = new ArrayList<RasterTile>();
		for (int i = grid.getXmin(); i < grid.getXmax(); i++) {
			for (int j = grid.getYmin(); j < grid.getYmax(); j++) {
				double x = grid.getLowerLeft().x + (i - grid.getXmin()) * grid.getTileWidth();
				double y = grid.getLowerLeft().y + (j - grid.getYmin()) * grid.getTileHeight();
				// layer coordinates
				Bbox worldBox;
				Bbox layerBox;
				if (needTransform) {
					layerBox = new Bbox(x, y, grid.getTileWidth(), grid.getTileHeight());
					// Transforming back to map coordinates will only result in
					// a proper grid if the transformation
					// is nearly affine
					worldBox = geoService.transform(layerBox, layerToMap);
				} else {
					worldBox = new Bbox(x, y, grid.getTileWidth(), grid.getTileHeight());
					layerBox = worldBox;
				}
				// Rounding to avoid white space between raster tiles lower-left
				// becomes upper-left in inverted y-space
				Bbox screenBox = new Bbox(Math.round(scale * worldBox.getX()), -Math.round(scale * worldBox.getMaxY()),
						Math.round(scale * worldBox.getMaxX()) - Math.round(scale * worldBox.getX()), Math.round(scale
								* worldBox.getMaxY())
								- Math.round(scale * worldBox.getY()));

				RasterTile image = new RasterTile(screenBox, getId() + "." + bestResolution.getLevel() + "." + i + ","
						+ j);

				image.setCode(new TileCode(bestResolution.getLevel(), i, j));
				String url = formatUrl(bestResolution.getTileWidthPx(), bestResolution.getTileHeightPx(), layerBox);
				image.setUrl(url);
				result.add(image);
			}
		}

		return result;
	}

	@Override
	public String getLegendImageUrl() {
		return legendImageUrl;
	}

	private String getWmsTargetUrl() {
		if (useProxy || null != authentication || useCache) {
			if (null != dispatcherUrlService) {
				String url = dispatcherUrlService.getDispatcherUrl();
				if (!url.endsWith("/")) {
					url += "/";
				}
				return url + "wms/" + getId() + "/";
			} else {
				return "./d/wms/" + getId() + "/";
			}
		} else {
			return baseWmsUrl;
		}
	}

	private String formatGetFeatureInfoUrl(int width, int height, Bbox box, int x, int y, boolean isHtmlRequest)
			throws GeomajasException {
		// Always use direct url
		try {
			StringBuilder url = formatBaseUrl(baseWmsUrl, width, height, box);
			String layers = getId();
			if (layerInfo.getDataSourceName() != null) {
				layers = layerInfo.getDataSourceName();
			}
			url.append("&QUERY_LAYERS=");
			url.append(URLEncoder.encode(layers, "UTF8"));
			url.append("&request=GetFeatureInfo");
			url.append("&X=");
			url.append(Integer.toString(x));
			url.append("&Y=");
			url.append(Integer.toString(y));
			url.append("&INFO_FORMAT=");
			url.append(retrieveFormatString(isHtmlRequest));
			log.debug("Requesting GetFeatureInfo with URL {}", url);
			return url.toString();
		} catch (UnsupportedEncodingException uee) {
			throw new IllegalStateException("Cannot find UTF8 encoding?", uee);
		}
	}

	private String retrieveFormatString(boolean isHtmlRequest) {
		if (isHtmlRequest) {
			return ("text/html");
		} else {
			return ("application/vnd.ogc.gml");
		}
	}

	private String formatUrl(int width, int height, Bbox box) throws GeomajasException {
		StringBuilder url = formatBaseUrl(getWmsTargetUrl(), width, height, box);
		url.append("&request=GetMap");
		String token = securityContext.getToken();
		if (null != token) {
			url.append("&userToken=");
			url.append(token);
		}
		return url.toString();
	}

	/**
	 * Build the base part of the url (doesn't change for getMap or getFeatureInfo requests).
	 * 
	 * @param targetUrl
	 *            base url
	 * @param width
	 *            image width
	 * @param height
	 *            image height
	 * @param box
	 *            bounding box
	 * @return base WMS url
	 * @throws GeomajasException
	 *             missing parameter
	 */
	private StringBuilder formatBaseUrl(String targetUrl, int width, int height, Bbox box) throws GeomajasException {
		try {
			StringBuilder url = new StringBuilder(targetUrl);
			int pos = url.lastIndexOf("?");
			if (pos > 0) {
				url.append("&SERVICE=WMS");
			} else {
				url.append("?SERVICE=WMS");
			}
			String layers = getId();
			if (layerInfo.getDataSourceName() != null) {
				layers = layerInfo.getDataSourceName();
			}
			url.append("&layers=");
			url.append(URLEncoder.encode(layers, "UTF8"));
			url.append("&WIDTH=");
			url.append(Integer.toString(width));
			url.append("&HEIGHT=");
			url.append(Integer.toString(height));
			DecimalFormat decimalFormat = new DecimalFormat(); // create new as
																// this is not
																// thread safe
			decimalFormat.setDecimalSeparatorAlwaysShown(false);
			decimalFormat.setGroupingUsed(false);
			decimalFormat.setMinimumFractionDigits(0);
			decimalFormat.setMaximumFractionDigits(100);
			DecimalFormatSymbols symbols = new DecimalFormatSymbols();
			symbols.setDecimalSeparator('.');
			decimalFormat.setDecimalFormatSymbols(symbols);

			url.append("&bbox=");
			url.append(decimalFormat.format(box.getX()));
			url.append(",");
			url.append(decimalFormat.format(box.getY()));
			url.append(",");
			url.append(decimalFormat.format(box.getMaxX()));
			url.append(",");
			url.append(decimalFormat.format(box.getMaxY()));
			url.append("&format=");
			url.append(format);
			url.append("&version=");
			url.append(version);
			if ("1.3.0".equals(version)) {
				url.append("&crs=");
			} else {
				url.append("&srs=");
			}
			url.append(URLEncoder.encode(layerInfo.getCrs(), "UTF8"));
			url.append("&styles=");
			url.append(styles);
			if (null != parameters) {
				for (Parameter p : parameters) {
					url.append("&");
					url.append(URLEncoder.encode(p.getName(), "UTF8"));
					url.append("=");
					url.append(URLEncoder.encode(p.getValue(), "UTF8"));
				}
			}
			return url;
		} catch (UnsupportedEncodingException uee) {
			throw new IllegalStateException("Cannot find UTF8 encoding?", uee);
		}
	}

	private String formatGetCapabilitiesUrl(String targetUrl) {
		StringBuilder url = new StringBuilder(targetUrl);
		int pos = url.lastIndexOf("?");
		if (pos > 0) {
			url.append("&SERVICE=WMS");
		} else {
			url.append("?SERVICE=WMS");
		}
		url.append("&version=");
		url.append(version);
		url.append("&REQUEST=");
		url.append("getCapabilities");
		return url.toString();
	}

	private void retrieveAndSetLegendImageParameters(String baseWmsUrl) {
		// set default values
		setDefaultLegendImageValues();
		String capabilitiesUrl = formatGetCapabilitiesUrl(baseWmsUrl);
		try {
			HTTPClient httpClient = createConfiguredWmsHttpClient();
			WebMapServer wms = new WebMapServer(new URL(capabilitiesUrl), httpClient);
			WMSCapabilities capabilities = wms.getCapabilities();
			String searchedLayerId = getId();
			if (layerInfo.getDataSourceName() != null) {
				searchedLayerId = layerInfo.getDataSourceName();
			}
			String searchedStyleName = styles;
			parseNodeLayer(searchedStyleName, searchedLayerId, capabilities.getLayer());
		} catch (IOException e) {
			log.warn(e.getMessage());
		} catch (ServiceException e) {
			log.warn("Could not parse capabilities from {}", capabilitiesUrl);
		}
	}

	private HTTPClient createConfiguredWmsHttpClient() {
		HTTPClient httpClient = new SimpleHttpClient();
		httpClient.setConnectTimeout(DEFAULT_CAPABILITIES_CONNECTION_TIMEOUT_MS);
		if (layerAuthentication != null) {
			httpClient.setUser(layerAuthentication.getUser());
			httpClient.setPassword(layerAuthentication.getPassword());
		}
		return httpClient;
	}

	private void setDefaultLegendImageValues() {
		legendImageUrl = EMPTY_LEGENDURL;
		legendImageHeight = EMPTY_LEGEND_HEIGHT;
		legendImageWidth = EMPTY_LEGEND_WIDTH;
	}

	private boolean parseNodeLayer(String searchedStyleName, String searchedLayerId, Layer layer) {
		// check if the layer itself matches the given id
		boolean isThisLayerMatched = parseLeafLayer(searchedStyleName, searchedLayerId, layer);
		if (isThisLayerMatched) {
			return true; // the layer is matched. stop search.
		}

		List<Layer> childLayers = layer.getLayerChildren();
		for (Layer childLayer : childLayers) {
			boolean isChildMatched = false;
			if (layer.getChildren().length > 0) {
				isChildMatched = parseNodeLayer(searchedStyleName, searchedLayerId, childLayer);
			} else {
				isChildMatched = parseLeafLayer(searchedStyleName, searchedLayerId, childLayer);
			}
			if (isChildMatched) {
				return true; // A child layer matched. stop search.
			}
		}
		// neither the layer itself nor its childs matched. return to parent layer.
		return false;
	}

	private boolean parseLeafLayer(String searchedStyleName, String searchedLayerId, Layer layer) {
		if (searchedLayerId.equals(layer.getName())) {
			legendImageUrl = retrieveStyleForLayer(searchedStyleName, layer);
			// this layer matched. stop search.
			return true;
		}
		return false; // this leaf did not match.
	}

	private String retrieveStyleForLayer(String searchedStyleName, Layer layer) {
		List<StyleImpl> layerStyles = layer.getStyles();
		if (layerStyles != null && !layerStyles.isEmpty()) {
			String retrievedLegendUrl = searchStyle(searchedStyleName, layerStyles);
			if (!EMPTY_LEGENDURL.equals(retrievedLegendUrl)) {
				return retrievedLegendUrl;
			}
			return retrieveFirstStyle(layerStyles);
		} else {
			return EMPTY_LEGENDURL;
		}
	}

	private String retrieveFirstStyle(List<StyleImpl> layerStyles) {
		String legendUrl = EMPTY_LEGENDURL;
		StyleImpl style = layerStyles.get(0);
		List<?> legendURLs = style.getLegendURLs();
		if (legendURLs != null && !legendURLs.isEmpty()) {
			legendUrl = legendURLs.get(0).toString();
			retrieveAndSetHeightAndWidth(legendUrl);
		}
		return legendUrl;
	}

	private String searchStyle(String styleName, List<StyleImpl> layerStyles) {
		String legendUrl = EMPTY_LEGENDURL;
		for (StyleImpl style : layerStyles) {
			List<?> legendUrls = style.getLegendURLs();
			if (styleName.equals(style.getName()) && legendUrls != null && !legendUrls.isEmpty()) {
				legendUrl = legendUrls.get(0).toString();
				retrieveAndSetHeightAndWidth(legendUrl);
			}
		}
		return legendUrl;
	}

	private void retrieveAndSetHeightAndWidth(String legendUrl) {
		try {
			InputStream stream = httpService.getStream(legendUrl, null, null);
			BufferedImage img = ImageIO.read(stream);
			setLegendImageHeight(img.getHeight());
			setLegendImageWidth(img.getWidth());
		} catch (IOException e) {
			log.debug("Could not retrieve legend image height and size. Reason: ", e);
		}
	}

	private Resolution getResolutionForScale(double scale) {
		if (null == resolutions || resolutions.size() == 0) {
			return calculateBestQuadTreeResolution(scale);
		} else {
			double screenResolution = 1.0 / scale;
			if (screenResolution >= resolutions.get(0).getResolution()) {
				return resolutions.get(0);
			} else if (screenResolution <= resolutions.get(resolutions.size() - 1).getResolution()) {
				return resolutions.get(resolutions.size() - 1);<commit_msg>Fixed NPE while retrieving WMS layer images<commit_after>/*
 * This is part of Geomajas, a GIS framework, http://www.geomajas.org/.
 *
 * Copyright 2008-2013 Geosparc nv, http://www.geosparc.com/, Belgium.
 *
 * The program is available in open source according to the GNU Affero
 * General Public License. All contributions in this program are covered
 * by the Geomajas Contributors License Agreement. For full licensing
 * details, see LICENSE.txt in the project root.
 */
package org.geomajas.layer.wms;

import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLEncoder;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;

import javax.annotation.PostConstruct;
import javax.imageio.ImageIO;

import org.geomajas.annotation.Api;
import org.geomajas.configuration.Parameter;
import org.geomajas.configuration.RasterLayerInfo;
import org.geomajas.configuration.client.ScaleInfo;
import org.geomajas.geometry.Bbox;
import org.geomajas.geometry.Crs;
import org.geomajas.geometry.CrsTransform;
import org.geomajas.global.ExceptionCode;
import org.geomajas.global.GeomajasException;
import org.geomajas.layer.LayerException;
import org.geomajas.layer.LayerLegendImageSupport;
import org.geomajas.layer.RasterLayer;
import org.geomajas.layer.common.proxy.LayerAuthentication;
import org.geomajas.layer.common.proxy.LayerAuthenticationMethod;
import org.geomajas.layer.common.proxy.LayerHttpService;
import org.geomajas.layer.feature.Attribute;
import org.geomajas.layer.feature.Feature;
import org.geomajas.layer.feature.attribute.StringAttribute;
import org.geomajas.layer.tile.RasterTile;
import org.geomajas.layer.tile.TileCode;
import org.geomajas.plugin.caching.service.CacheManagerService;
import org.geomajas.security.SecurityContext;
import org.geomajas.service.DispatcherUrlService;
import org.geomajas.service.DtoConverterService;
import org.geomajas.service.GeoService;
import org.geotools.GML;
import org.geotools.GML.Version;
import org.geotools.data.ows.HTTPClient;
import org.geotools.data.ows.Layer;
import org.geotools.data.ows.SimpleHttpClient;
import org.geotools.data.ows.StyleImpl;
import org.geotools.data.ows.WMSCapabilities;
import org.geotools.data.wms.WebMapServer;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.FeatureIterator;
import org.geotools.ows.ServiceException;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.geometry.MismatchedDimensionException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Envelope;

/**
 * <p>
 * Layer model for accessing raster data from WMS servers. It has support for most WMS versions: up to 1.3.0. When using
 * this layer, note that the following fields are required:
 * <ul>
 * <li><b>baseWmsUrl</b>: The base URL to the WMS server.</li>
 * <li><b>format</b>: The format for the returned images.</li>
 * <li><b>version</b>: The version of WMS to use.</li>
 * <li><b>styles</b>: The styles to use when rendering the WMS images.</li>
 * <li><b>useProxy</b>: Set to true to use a proxy for rendering the WMS, hiding the URL from the client. This
 * automatically happens when setting the authentication object.</li>
 * </ul>
 * There always is the option of adding additional parameters to the WMS GetMap requests, by filling the
 * <code>parameters</code> list. Such parameters could include optional WMS GetMap parameters, such as "transparency",
 * but also "user" and "password".
 * </p>
 * <p>
 * This layer also supports BASIC and DIGEST authentication. To use this functionality, set the
 * <code>authentication</code> field.
 * </p>
 * 
 * @author <NAME>
 * @author <NAME>
 * @author <NAME>
 * @author <NAME>
 * @since 1.7.1
 */
@Api
public class WmsLayer implements RasterLayer, LayerLegendImageSupport, LayerFeatureInfoSupport,
		LayerFeatureInfoAsHtmlSupport, LayerFeatureInfoAsGmlSupport {

	private static final String GFI_UNAVAILABLE_MSG = "GetFeatureInfo-support not available on this layer";

	private static final int DEFAULT_CAPABILITIES_CONNECTION_TIMEOUT_MS = 10000;

	private static final int EMPTY_LEGEND_WIDTH = -1;

	private static final int EMPTY_LEGEND_HEIGHT = -1;

	private static final String EMPTY_LEGENDURL = "";

	private static final boolean IS_GML_REQUEST = false;

	private static final boolean IS_HTML_REQUEST = true;
	
	private final Logger log = LoggerFactory.getLogger(WmsLayer.class);

	private final List<Resolution> resolutions = new ArrayList<Resolution>();

	// @NotNull this seems to cause problems, it is tested in @PostConstruct
	// anyway
	private String baseWmsUrl;

	private String format = "image/png";

	private String version = "1.1.1";

	private String styles = EMPTY_LEGENDURL;

	private List<Parameter> parameters;

	private boolean enableFeatureInfoSupport;

	private boolean enableFeatureInfoSupportAsHtml;

	private RasterLayerInfo layerInfo;

	private Crs crs;

	private String id;

	/**
	 * @deprecated use layerAuthentication
	 */
	@Deprecated
	private WmsAuthentication authentication;

	private LayerAuthentication layerAuthentication;

	private boolean useProxy;

	private boolean useCache;

	@Autowired
	private GeoService geoService;

	@Autowired
	private LayerHttpService httpService;

	@Autowired
	private DtoConverterService converterService;

	@Autowired(required = false)
	private DispatcherUrlService dispatcherUrlService;

	@Autowired(required = false)
	private CacheManagerService cacheManagerService;

	@Autowired
	private SecurityContext securityContext;

	private boolean enableFeatureInfoSupportAsGml;

	private String legendImageUrl;

	private int legendImageHeight;

	private int legendImageWidth;

	/**
	 * Return the layers identifier.
	 * 
	 * @return layer id
	 */
	public String getId() {
		return id;
	}

	/**
	 * Set the layer identifier.
	 * 
	 * @param id
	 *            layer id
	 */
	public void setId(String id) {
		this.id = id;
	}

	/**
	 * Return the layer info object.
	 * 
	 * @since 1.7.1
	 */
	public RasterLayerInfo getLayerInfo() {
		return layerInfo;
	}

	/**
	 * Get coordinate reference system for this layer.
	 * 
	 * @return Coordinate reference system for this layer.
	 * @deprecated use {@link org.geomajas.layer.LayerService#getCrs(org.geomajas.layer.Layer)}
	 */
	@Deprecated
	public CoordinateReferenceSystem getCrs() {
		return crs;
	}

	@PostConstruct
	protected void postConstruct() throws GeomajasException {
		if (null == baseWmsUrl) {
			throw new GeomajasException(ExceptionCode.PARAMETER_MISSING, "baseWmsUrl");
		}

		crs = geoService.getCrs2(getLayerInfo().getCrs());

		// calculate resolutions
		List<ScaleInfo> scales = layerInfo.getZoomLevels();
		if (scales != null) {
			List<Double> r = new ArrayList<Double>();
			for (ScaleInfo scale : scales) {
				r.add(1. / scale.getPixelPerUnit());
			}

			// sort in decreasing order !!!
			Collections.sort(r);
			Collections.reverse(r);

			int level = 0;
			for (double resolution : r) {
				resolutions
						.add(new Resolution(resolution, level++, layerInfo.getTileWidth(), layerInfo.getTileHeight()));
			}
		}
		retrieveAndSetLegendImageParameters(baseWmsUrl);
	}

	/**
	 * Set the layer configuration.
	 * 
	 * @param layerInfo
	 *            layer information
	 * @throws LayerException
	 *             oops
	 * @since 1.7.1
	 */
	@Api
	public void setLayerInfo(RasterLayerInfo layerInfo) throws LayerException {
		this.layerInfo = layerInfo;
	}

	/** {@inheritDoc}. */
	public List<Feature> getFeaturesByLocation(Coordinate layerCoordinate, double layerScale, int pixelTolerance)
			throws LayerException {
		if (!isEnableFeatureInfoSupport()) {
			return Collections.emptyList();
		}
		List<Feature> features = new ArrayList<Feature>();
		Resolution bestResolution = getResolutionForScale(layerScale);
		RasterGrid grid = getRasterGrid(new Envelope(layerCoordinate), bestResolution.getTileWidth(),
				bestResolution.getTileHeight());
		int x = (int) (((layerCoordinate.x - grid.getLowerLeft().x) * bestResolution.getTileWidthPx()) / grid
				.getTileWidth());
		int y = (int) (bestResolution.getTileHeightPx() - (((layerCoordinate.y - grid.getLowerLeft().y) * bestResolution
				.getTileHeightPx()) / grid.getTileHeight()));

		Bbox layerBox = new Bbox(grid.getLowerLeft().x, grid.getLowerLeft().y, grid.getTileWidth(),
				grid.getTileHeight());

		InputStream stream = null;
		try {
			String url = buildRequestUrl(layerCoordinate, layerScale, IS_GML_REQUEST);
			log.debug("getFeaturesByLocation: {} {} {} {}", new Object[] { layerCoordinate, layerScale, pixelTolerance,
					url });
			GML gml = new GML(Version.GML3);

			stream = httpService.getStream(url, getLayerAuthentication(), getId());
			FeatureCollection<?, SimpleFeature> collection = gml.decodeFeatureCollection(stream);
			FeatureIterator<SimpleFeature> it = collection.features();

			while (it.hasNext()) {
				features.add(toDto(it.next()));
			}
		} catch (LayerException le) {
			throw le; // NOSONAR
		} catch (Exception e) { // NOSONAR
			throw new LayerException(e, ExceptionCode.UNEXPECTED_PROBLEM);
		} finally {
			if (null != stream) {
				try {
					stream.close();
				} catch (IOException ioe) {
					// ignore, closing anyway
				}
			}
		}

		return features;
	}

	@Override
	public List<Feature> getFeatureInfoAsGml(Coordinate coordinate, double layerScale, int pixelTolerance)
			throws LayerException {
		if (!isEnableFeatureInfoAsGmlSupport()) {
			return Collections.emptyList();
		}
		return getFeaturesByLocation(coordinate, layerScale, pixelTolerance);
	}

	@Override
	public String getFeatureInfoAsHtml(Coordinate coordinate, double layerScale, int pixelTolerance)
			throws LayerException {
		if (!isEnableFeatureInfoAsHtmlSupport()) {
			return GFI_UNAVAILABLE_MSG;
		}
		InputStream stream = null;
		String url;
		try {
			url = buildRequestUrl(coordinate, layerScale, IS_HTML_REQUEST);

			log.debug("getFeaturesByLocation: {} {} {} {}",
					new Object[] { coordinate, layerScale, pixelTolerance, url });
			stream = httpService.getStream(url, getLayerAuthentication(), getId());
		} catch (Exception e) {
			throw new LayerException(e, ExceptionCode.UNEXPECTED_PROBLEM);
		}
		return getStringFromInputStream(stream);
	}

	private static String getStringFromInputStream(InputStream is) {

		BufferedReader br = null;
		StringBuilder sb = new StringBuilder();

		String line;
		try {

			br = new BufferedReader(new InputStreamReader(is, "utf-8"));
			while ((line = br.readLine()) != null) {
				sb.append(line);
			}

		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			if (br != null) {
				try {
					br.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}

		return sb.toString();

	}

	protected String buildRequestUrl(Coordinate layerCoordinate, double layerScale, boolean isHtmlRequest)
			throws GeomajasException {
		Resolution bestResolution = getResolutionForScale(layerScale);
		RasterGrid grid = getRasterGrid(new Envelope(layerCoordinate), bestResolution.getTileWidth(),
				bestResolution.getTileHeight());
		int x = (int) (((layerCoordinate.x - grid.getLowerLeft().x) * bestResolution.getTileWidthPx()) / grid
				.getTileWidth());
		int y = (int) (bestResolution.getTileHeightPx() - (((layerCoordinate.y - grid.getLowerLeft().y) * bestResolution
				.getTileHeightPx()) / grid.getTileHeight()));

		Bbox layerBox = new Bbox(grid.getLowerLeft().x, grid.getLowerLeft().y, grid.getTileWidth(),
				grid.getTileHeight());
		String url = formatGetFeatureInfoUrl(bestResolution.getTileWidthPx(), bestResolution.getTileHeightPx(),
				layerBox, x, y, isHtmlRequest);
		return url;
	}

	private Feature toDto(SimpleFeature feature) {
		if (feature == null) {
			return null;
		}
		Feature dto = new Feature(feature.getID());

		HashMap<String, Attribute> attributes = new HashMap<String, Attribute>();

		for (AttributeDescriptor desc : feature.getType().getAttributeDescriptors()) {
			Object obj = feature.getAttribute(desc.getName());
			if (null != obj) {
				attributes.put(desc.getLocalName(), new StringAttribute(obj.toString()));
			}
		}
		dto.setAttributes(attributes);
		dto.setId(feature.getID());

		dto.setUpdatable(false);
		dto.setDeletable(false);
		return dto;

	}

	/**
	 * Paints the specified bounds optimized for the specified scale in pixel/unit.
	 * 
	 * @param targetCrs
	 *            Coordinate reference system used for bounds
	 * @param bounds
	 *            bounds to request images for
	 * @param scale
	 *            scale or zoom level (unit?)
	 * @return a list of raster images that covers the bounds
	 * @throws GeomajasException
	 *             oops
	 */
	public List<RasterTile> paint(CoordinateReferenceSystem targetCrs, Envelope bounds, double scale)
			throws GeomajasException {
		Envelope layerBounds = bounds;
		double layerScale = scale;
		CrsTransform layerToMap = null;
		boolean needTransform = !crs.equals(targetCrs);

		try {
			// We don't necessarily need to split into same CRS and different
			// CRS cases, the latter implementation uses
			// identity transform if crs's are equal for map and layer but might
			// introduce bugs in rounding and/or
			// conversions.
			if (needTransform) {
				layerToMap = geoService.getCrsTransform(crs, targetCrs);
				CrsTransform mapToLayer = geoService.getCrsTransform(targetCrs, crs);

				// Translate the map coordinates to layer coordinates, assumes
				// equal x-y orientation
				layerBounds = geoService.transform(bounds, mapToLayer);
				layerScale = bounds.getWidth() * scale / layerBounds.getWidth();
			}
		} catch (MismatchedDimensionException e) {
			throw new GeomajasException(e, ExceptionCode.RENDER_DIMENSION_MISMATCH);
		}
		layerBounds = clipBounds(layerBounds);
		if (layerBounds.isNull()) {
			return new ArrayList<RasterTile>(0);
		}

		// Grid is in layer coordinate space!
		Resolution bestResolution = getResolutionForScale(layerScale);
		RasterGrid grid = getRasterGrid(layerBounds, bestResolution.getTileWidth(), bestResolution.getTileHeight());

		// We calculate the first tile's screen box with this assumption
		List<RasterTile> result = new ArrayList<RasterTile>();
		for (int i = grid.getXmin(); i < grid.getXmax(); i++) {
			for (int j = grid.getYmin(); j < grid.getYmax(); j++) {
				double x = grid.getLowerLeft().x + (i - grid.getXmin()) * grid.getTileWidth();
				double y = grid.getLowerLeft().y + (j - grid.getYmin()) * grid.getTileHeight();
				// layer coordinates
				Bbox worldBox;
				Bbox layerBox;
				if (needTransform) {
					layerBox = new Bbox(x, y, grid.getTileWidth(), grid.getTileHeight());
					// Transforming back to map coordinates will only result in
					// a proper grid if the transformation
					// is nearly affine
					worldBox = geoService.transform(layerBox, layerToMap);
				} else {
					worldBox = new Bbox(x, y, grid.getTileWidth(), grid.getTileHeight());
					layerBox = worldBox;
				}
				// Rounding to avoid white space between raster tiles lower-left
				// becomes upper-left in inverted y-space
				Bbox screenBox = new Bbox(Math.round(scale * worldBox.getX()), -Math.round(scale * worldBox.getMaxY()),
						Math.round(scale * worldBox.getMaxX()) - Math.round(scale * worldBox.getX()), Math.round(scale
								* worldBox.getMaxY())
								- Math.round(scale * worldBox.getY()));

				RasterTile image = new RasterTile(screenBox, getId() + "." + bestResolution.getLevel() + "." + i + ","
						+ j);

				image.setCode(new TileCode(bestResolution.getLevel(), i, j));
				String url = formatUrl(bestResolution.getTileWidthPx(), bestResolution.getTileHeightPx(), layerBox);
				image.setUrl(url);
				result.add(image);
			}
		}

		return result;
	}

	@Override
	public String getLegendImageUrl() {
		return legendImageUrl;
	}

	private String getWmsTargetUrl() {
		if (useProxy || null != authentication || useCache) {
			if (null != dispatcherUrlService) {
				String url = dispatcherUrlService.getDispatcherUrl();
				if (!url.endsWith("/")) {
					url += "/";
				}
				return url + "wms/" + getId() + "/";
			} else {
				return "./d/wms/" + getId() + "/";
			}
		} else {
			return baseWmsUrl;
		}
	}

	private String formatGetFeatureInfoUrl(int width, int height, Bbox box, int x, int y, boolean isHtmlRequest)
			throws GeomajasException {
		// Always use direct url
		try {
			StringBuilder url = formatBaseUrl(baseWmsUrl, width, height, box);
			String layers = getId();
			if (layerInfo.getDataSourceName() != null) {
				layers = layerInfo.getDataSourceName();
			}
			url.append("&QUERY_LAYERS=");
			url.append(URLEncoder.encode(layers, "UTF8"));
			url.append("&request=GetFeatureInfo");
			url.append("&X=");
			url.append(Integer.toString(x));
			url.append("&Y=");
			url.append(Integer.toString(y));
			url.append("&INFO_FORMAT=");
			url.append(retrieveFormatString(isHtmlRequest));
			log.debug("Requesting GetFeatureInfo with URL {}", url);
			return url.toString();
		} catch (UnsupportedEncodingException uee) {
			throw new IllegalStateException("Cannot find UTF8 encoding?", uee);
		}
	}

	private String retrieveFormatString(boolean isHtmlRequest) {
		if (isHtmlRequest) {
			return ("text/html");
		} else {
			return ("application/vnd.ogc.gml");
		}
	}

	private String formatUrl(int width, int height, Bbox box) throws GeomajasException {
		StringBuilder url = formatBaseUrl(getWmsTargetUrl(), width, height, box);
		url.append("&request=GetMap");
		String token = securityContext.getToken();
		if (null != token) {
			url.append("&userToken=");
			url.append(token);
		}
		return url.toString();
	}

	/**
	 * Build the base part of the url (doesn't change for getMap or getFeatureInfo requests).
	 * 
	 * @param targetUrl
	 *            base url
	 * @param width
	 *            image width
	 * @param height
	 *            image height
	 * @param box
	 *            bounding box
	 * @return base WMS url
	 * @throws GeomajasException
	 *             missing parameter
	 */
	private StringBuilder formatBaseUrl(String targetUrl, int width, int height, Bbox box) throws GeomajasException {
		try {
			StringBuilder url = new StringBuilder(targetUrl);
			int pos = url.lastIndexOf("?");
			if (pos > 0) {
				url.append("&SERVICE=WMS");
			} else {
				url.append("?SERVICE=WMS");
			}
			String layers = getId();
			if (layerInfo.getDataSourceName() != null) {
				layers = layerInfo.getDataSourceName();
			}
			url.append("&layers=");
			url.append(URLEncoder.encode(layers, "UTF8"));
			url.append("&WIDTH=");
			url.append(Integer.toString(width));
			url.append("&HEIGHT=");
			url.append(Integer.toString(height));
			DecimalFormat decimalFormat = new DecimalFormat(); // create new as
																// this is not
																// thread safe
			decimalFormat.setDecimalSeparatorAlwaysShown(false);
			decimalFormat.setGroupingUsed(false);
			decimalFormat.setMinimumFractionDigits(0);
			decimalFormat.setMaximumFractionDigits(100);
			DecimalFormatSymbols symbols = new DecimalFormatSymbols();
			symbols.setDecimalSeparator('.');
			decimalFormat.setDecimalFormatSymbols(symbols);

			url.append("&bbox=");
			url.append(decimalFormat.format(box.getX()));
			url.append(",");
			url.append(decimalFormat.format(box.getY()));
			url.append(",");
			url.append(decimalFormat.format(box.getMaxX()));
			url.append(",");
			url.append(decimalFormat.format(box.getMaxY()));
			url.append("&format=");
			url.append(format);
			url.append("&version=");
			url.append(version);
			if ("1.3.0".equals(version)) {
				url.append("&crs=");
			} else {
				url.append("&srs=");
			}
			url.append(URLEncoder.encode(layerInfo.getCrs(), "UTF8"));
			url.append("&styles=");
			url.append(styles);
			if (null != parameters) {
				for (Parameter p : parameters) {
					url.append("&");
					url.append(URLEncoder.encode(p.getName(), "UTF8"));
					url.append("=");
					url.append(URLEncoder.encode(p.getValue(), "UTF8"));
				}
			}
			return url;
		} catch (UnsupportedEncodingException uee) {
			throw new IllegalStateException("Cannot find UTF8 encoding?", uee);
		}
	}

	private String formatGetCapabilitiesUrl(String targetUrl) {
		StringBuilder url = new StringBuilder(targetUrl);
		int pos = url.lastIndexOf("?");
		if (pos > 0) {
			url.append("&SERVICE=WMS");
		} else {
			url.append("?SERVICE=WMS");
		}
		url.append("&version=");
		url.append(version);
		url.append("&REQUEST=");
		url.append("getCapabilities");
		return url.toString();
	}

	private void retrieveAndSetLegendImageParameters(String baseWmsUrl) {
		// set default values
		setDefaultLegendImageValues();
		String capabilitiesUrl = formatGetCapabilitiesUrl(baseWmsUrl);
		try {
			HTTPClient httpClient = createConfiguredWmsHttpClient();
			WebMapServer wms = new WebMapServer(new URL(capabilitiesUrl), httpClient);
			WMSCapabilities capabilities = wms.getCapabilities();
			String searchedLayerId = getId();
			if (layerInfo.getDataSourceName() != null) {
				searchedLayerId = layerInfo.getDataSourceName();
			}
			String searchedStyleName = styles;
			parseNodeLayer(searchedStyleName, searchedLayerId, capabilities.getLayer());
		} catch (IOException e) {
			log.warn(e.getMessage());
		} catch (ServiceException e) {
			log.warn("Could not parse capabilities from {}", capabilitiesUrl);
		}
	}

	private HTTPClient createConfiguredWmsHttpClient() {
		HTTPClient httpClient = new SimpleHttpClient();
		httpClient.setConnectTimeout(DEFAULT_CAPABILITIES_CONNECTION_TIMEOUT_MS);
		if (layerAuthentication != null) {
			httpClient.setUser(layerAuthentication.getUser());
			httpClient.setPassword(layerAuthentication.getPassword());
		}
		return httpClient;
	}

	private void setDefaultLegendImageValues() {
		legendImageUrl = EMPTY_LEGENDURL;
		legendImageHeight = EMPTY_LEGEND_HEIGHT;
		legendImageWidth = EMPTY_LEGEND_WIDTH;
	}

	private boolean parseNodeLayer(String searchedStyleName, String searchedLayerId, Layer layer) {
		// check if the layer itself matches the given id
		boolean isThisLayerMatched = parseLeafLayer(searchedStyleName, searchedLayerId, layer);
		if (isThisLayerMatched) {
			return true; // the layer is matched. stop search.
		}

		List<Layer> childLayers = layer.getLayerChildren();
		for (Layer childLayer : childLayers) {
			boolean isChildMatched = false;
			if (layer.getChildren().length > 0) {
				isChildMatched = parseNodeLayer(searchedStyleName, searchedLayerId, childLayer);
			} else {
				isChildMatched = parseLeafLayer(searchedStyleName, searchedLayerId, childLayer);
			}
			if (isChildMatched) {
				return true; // A child layer matched. stop search.
			}
		}
		// neither the layer itself nor its childs matched. return to parent layer.
		return false;
	}

	private boolean parseLeafLayer(String searchedStyleName, String searchedLayerId, Layer layer) {
		if (searchedLayerId.equals(layer.getName())) {
			legendImageUrl = retrieveStyleForLayer(searchedStyleName, layer);
			// this layer matched. stop search.
			return true;
		}
		return false; // this leaf did not match.
	}

	private String retrieveStyleForLayer(String searchedStyleName, Layer layer) {
		List<StyleImpl> layerStyles = layer.getStyles();
		if (layerStyles != null && !layerStyles.isEmpty()) {
			String retrievedLegendUrl = searchStyle(searchedStyleName, layerStyles);
			if (!EMPTY_LEGENDURL.equals(retrievedLegendUrl)) {
				return retrievedLegendUrl;
			}
			return retrieveFirstStyle(layerStyles);
		} else {
			return EMPTY_LEGENDURL;
		}
	}

	private String retrieveFirstStyle(List<StyleImpl> layerStyles) {
		String legendUrl = EMPTY_LEGENDURL;
		StyleImpl style = layerStyles.get(0);
		List<?> legendURLs = style.getLegendURLs();
		if (legendURLs != null && !legendURLs.isEmpty()) {
			legendUrl = legendURLs.get(0).toString();
			retrieveAndSetHeightAndWidth(legendUrl);
		}
		return legendUrl;
	}

	private String searchStyle(String styleName, List<StyleImpl> layerStyles) {
		String legendUrl = EMPTY_LEGENDURL;
		for (StyleImpl style : layerStyles) {
			List<?> legendUrls = style.getLegendURLs();
			if (styleName.equals(style.getName()) && legendUrls != null && !legendUrls.isEmpty()) {
				legendUrl = legendUrls.get(0).toString();
				retrieveAndSetHeightAndWidth(legendUrl);
			}
		}
		return legendUrl;
	}

	private void retrieveAndSetHeightAndWidth(String legendUrl) {
		try {
			InputStream stream = httpService.getStream(legendUrl, null, null);
			BufferedImage img = ImageIO.read(stream);
			if (img != null) {
				setLegendImageHeight(img.getHeight());
				setLegendImageWidth(img.getWidth());
			}
		} catch (IOException e) {
			log.debug("Could not retrieve legend image height and size. Reason: ", e);
		}
	}

	private Resolution getResolutionForScale(double scale) {
		if (null == resolutions || resolutions.size() == 0) {
			return calculateBestQuadTreeResolution(scale);
		} else {
			double screenResolution = 1.0 / scale;
			if (screenResolution >= resolutions.get(0).getResolution()) {
				return resolutions.get(0);
			} else if (screenResolution <= resolutions.get(resolutions.size() - 1).getResolution()) {
				return resolutions.get(resolutions.size() - 1);
{'fraction_non_alphanumeric': 0.11765397964586152, 'fraction_numerical': 0.0025491695589936665, 'mean_word_length': 4.253734418460905, 'pattern_counts': {'":': 0, '<': 125, '<?xml version=': 0, '>': 127, 'https://': 0, 'lorem ipsum': 0, 'www.': 4, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4673300', 'n_tokens_mistral': 17640, 'n_tokens_neox': 15526, 'n_words': 3716}