/*
* Copyright (C) 2009 Camptocamp
*
* This file is part of MapFish Server
*
* MapFish Server 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 3 of the License, or
* (at your option) any later version.
*
* MapFish Server 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 MapFish Server. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mapfish.print.config;
import org.apache.commons.httpclient.HostConfiguration;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethodBase;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
import org.apache.commons.httpclient.params.HttpConnectionManagerParams;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.ho.yaml.CustomYamlConfig;
import org.ho.yaml.YamlConfig;
import org.json.JSONException;
import org.json.JSONWriter;
import org.mapfish.print.InvalidValueException;
import org.mapfish.print.config.layout.Layout;
import org.mapfish.print.config.layout.Layouts;
import org.mapfish.print.map.MapTileTask;
import org.mapfish.print.map.readers.WMSServerInfo;
import org.mapfish.print.output.OutputFactory;
import org.mapfish.print.output.OutputFormat;
import org.pvalsecc.concurrent.OrderedResultsExecutor;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.SocketException;
import java.net.URI;
import java.net.UnknownHostException;
import java.text.NumberFormat;
import java.util.*;
/**
* Bean mapping the root of the configuration file.
*/
public class Config {
public static final Logger LOGGER = Logger.getLogger(Config.class);
private Layouts layouts;
private TreeSet<Integer> dpis;
private TreeSet<Integer> scales;
private TreeSet<String> fonts = null;
private List<HostMatcher> hosts = new ArrayList<HostMatcher>();
private TreeSet<Key> keys;
private int globalParallelFetches = 5;
private int perHostParallelFetches = 5;
private int socketTimeout = 3*60*1000;
private int connectionTimeout = 30*1000;
private boolean tilecacheMerging = false;
private String outputFilename = "mapfish-print.pdf";
/**
* How much of the asked map we tolerate to be outside of the printed area.
* Used only in case of bbox printing (use by the PrintAction JS component).
*/
private static final double BEST_SCALE_TOLERANCE = 0.98;
/**
* The bunch of threads that will be used to do the // fetching of the map
* chunks
*/
private OrderedResultsExecutor<MapTileTask> mapRenderingExecutor = null;
private MultiThreadedHttpConnectionManager connectionManager;
private TreeSet<String> formats;
public Config() {
hosts.add(new LocalHostMatcher());
}
/**
* Create an instance out of the given file.
*/
public static Config fromYaml(File file) throws FileNotFoundException {
YamlConfig config = new CustomYamlConfig();
Config result = config.loadType(file, Config.class);
result.validate();
return result;
}
public static Config fromInputStream(InputStream instream) {
YamlConfig config = new CustomYamlConfig();
Config result = config.loadType(instream, Config.class);
result.validate();
return result;
}
public static Config fromString(String strConfig) {
YamlConfig config = new CustomYamlConfig();
Config result = config.loadType(strConfig, Config.class);
result.validate();
return result;
}
public Layout getLayout(String name) {
return layouts.get(name);
}
public void setLayouts(Layouts layouts) {
this.layouts = layouts;
}
public void setDpis(TreeSet<Integer> dpis) {
this.dpis = dpis;
}
public TreeSet<Integer> getDpis() {
return dpis;
}
public void printClientConfig(JSONWriter json) throws JSONException {
json.key("scales");
json.array();
for (Integer scale : scales) {
json.object();
json.key("name").value("1:" + NumberFormat.getIntegerInstance().format(scale));
json.key("value").value(scale.toString());
json.endObject();
}
json.endArray();
json.key("dpis");
json.array();
for (Integer dpi : dpis) {
json.object();
json.key("name").value(dpi.toString());
json.key("value").value(dpi.toString());
json.endObject();
}
json.endArray();
json.key("outputFormats");
json.array();
for (String format : OutputFactory.getSupportedFormats(this)) {
json.object();
json.key("name").value(format);
json.endObject();
}
json.endArray();
json.key("layouts");
json.array();
ArrayList<String> sortedLayouts = new ArrayList<String>();
sortedLayouts.addAll(layouts.keySet());
Collections.sort(sortedLayouts);
for (int i = 0; i < sortedLayouts.size(); i++) {
String key = sortedLayouts.get(i);
json.object();
json.key("name").value(key);
layouts.get(key).printClientConfig(json);
json.endObject();
}
json.endArray();
}
public void setScales(TreeSet<Integer> scales) {
this.scales = scales;
}
public boolean isScalePresent(int scale) {
return scales.contains(scale);
}
public void setHosts(List<HostMatcher> hosts) {
this.hosts = hosts;
}
public void setFonts(TreeSet<String> fonts) {
this.fonts = fonts;
}
public TreeSet<String> getFonts() {
return fonts;
}
public void setKeys(TreeSet<Key> keys) {
this.keys = keys;
}
public TreeSet<Key> getKeys() {
TreeSet<Key> k = keys;
if(k == null) k = new TreeSet<Key>();
return k;
}
/**
* Make sure an URI is authorized
*/
public boolean validateUri(URI uri) throws UnknownHostException, SocketException, MalformedURLException {
for (int i = 0; i < hosts.size(); i++) {
HostMatcher matcher = hosts.get(i);
if (matcher.validate(uri)) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("URI [" + uri + "] accepted by: " + matcher);
}
return true;
}
}
return false;
}
/**
* Called just after the config has been loaded to check it is valid.
*
* @throws InvalidValueException When there is a problem
*/
public void validate() {
if (layouts == null) throw new InvalidValueException("layouts", "null");
layouts.validate();
if (dpis == null) throw new InvalidValueException("dpis", "null");
if (dpis.size() < 1) throw new InvalidValueException("dpis", "[]");
if (scales == null) throw new InvalidValueException("scales", "null");
if (scales.size() < 1) throw new InvalidValueException("scales", "[]");
if (hosts == null) throw new InvalidValueException("hosts", "null");
if (hosts.size() < 1) throw new InvalidValueException("hosts", "[]");
if (globalParallelFetches < 1) {
throw new InvalidValueException("globalParallelFetches", globalParallelFetches);
}
if (perHostParallelFetches < 1) {
throw new InvalidValueException("perHostParallelFetches", perHostParallelFetches);
}
if (socketTimeout < 0) {
throw new InvalidValueException("socketTimeout", socketTimeout);
}
if (connectionTimeout < 0) {
throw new InvalidValueException("connectionTimeout", connectionTimeout);
}
for (Key key : getKeys()) {
key.validate();
}
}
/**
* @return The first scale that is bigger or equal than the target.
*/
public int getBestScale(double target) {
target *= BEST_SCALE_TOLERANCE;
for (Integer scale : scales) {
if (scale >= target) {
return scale;
}
}
return scales.last();
}
public synchronized OrderedResultsExecutor<MapTileTask> getMapRenderingExecutor() {
if (mapRenderingExecutor == null && globalParallelFetches > 1) {
mapRenderingExecutor = new OrderedResultsExecutor<MapTileTask>(globalParallelFetches, "tilesReader");
mapRenderingExecutor.start();
}
return mapRenderingExecutor;
}
/**
* Stop all the threads and stuff used for this config.
*/
public synchronized void stop() {
WMSServerInfo.clearCache();
if (mapRenderingExecutor != null) {
mapRenderingExecutor.stop();
}
if(connectionManager != null) {
connectionManager.shutdown();
}
}
public void setGlobalParallelFetches(int globalParallelFetches) {
this.globalParallelFetches = globalParallelFetches;
}
public void setPerHostParallelFetches(int perHostParallelFetches) {
this.perHostParallelFetches = perHostParallelFetches;
System.getProperties().setProperty("http.maxConnections", Integer.toString(perHostParallelFetches));
}
/**
* Get or create the http client to be used to fetch all the map data.
*/
public HttpClient getHttpClient(URI uri) {
MultiThreadedHttpConnectionManager connectionManager = getConnectionManager();
HttpClient httpClient = new HttpClient(connectionManager);
// httpclient is a bit pesky about loading everything in memory...
// disabling the warnings.
Logger.getLogger(HttpMethodBase.class).setLevel(Level.ERROR);
// configure proxies for URI
ProxySelector selector = ProxySelector.getDefault();
List<Proxy> proxyList = selector.select(uri);
Proxy proxy = proxyList.get(0);
if (!proxy.equals(Proxy.NO_PROXY)) {
InetSocketAddress socketAddress = (InetSocketAddress) proxy.address();
String hostName = socketAddress.getHostName();
int port = socketAddress.getPort();
httpClient.getHostConfiguration().setProxy(hostName, port);
}
return httpClient;
}
private synchronized MultiThreadedHttpConnectionManager getConnectionManager() {
if(connectionManager == null) {
connectionManager = new MultiThreadedHttpConnectionManager();
final HttpConnectionManagerParams params = connectionManager.getParams();
params.setDefaultMaxConnectionsPerHost(perHostParallelFetches);
params.setMaxTotalConnections(globalParallelFetches);
params.setSoTimeout(socketTimeout);
params.setConnectionTimeout(connectionTimeout);
}
return connectionManager;
}
public void setTilecacheMerging(boolean tilecacheMerging) {
this.tilecacheMerging = tilecacheMerging;
}
public boolean isTilecacheMerging() {
return tilecacheMerging;
}
public void setSocketTimeout(int socketTimeout) {
this.socketTimeout = socketTimeout;
}
public void setConnectionTimeout(int connectionTimeout) {
this.connectionTimeout = connectionTimeout;
}
public String getOutputFilename(String layoutName) {
Layout layout = layouts.get(layoutName);
String name = null;
if(layout != null) {
name = layout.getOutputFilename();
}
return name == null ? outputFilename : name;
}
public String getOutputFilename() {
return outputFilename;
}
public void setOutputFilename(String outputFilename) {
this.outputFilename = outputFilename;
}
public TreeSet<String> getFormats() {
if(formats == null) return new TreeSet<String>();
return formats;
}
public void setFormats(TreeSet<String> formats) {
this.formats = formats;
}
}