package org.mapfish.print.servlet; import com.google.common.base.Optional; import com.google.common.collect.Maps; import com.google.common.io.Files; import com.vividsolutions.jts.util.Assert; import org.mapfish.print.MapPrinter; import org.mapfish.print.MapPrinterFactory; import org.mapfish.print.config.ConfigurationFactory; import org.mapfish.print.servlet.fileloader.ConfigFileLoaderManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import java.io.File; import java.net.URI; import java.net.URISyntaxException; import java.nio.channels.ClosedByInterruptException; import java.util.HashMap; import java.util.Map; import java.util.Set; import javax.annotation.Nullable; import javax.annotation.PostConstruct; /** * A {@link org.mapfish.print.MapPrinterFactory} that reads configuration from files and uses servlet's methods for resolving * the paths to the files. * <p></p> */ public class ServletMapPrinterFactory implements MapPrinterFactory { private static final Logger LOGGER = LoggerFactory.getLogger(ServletMapPrinterFactory.class); /** * The name of the default app. This is always required to be one of the apps that are registered. */ public static final String DEFAULT_CONFIGURATION_FILE_KEY = "default"; @Autowired private ApplicationContext applicationContext; @Autowired private ConfigurationFactory configurationFactory; @Autowired private ConfigFileLoaderManager configFileLoader; private Map<String, URI> configurationFiles = new HashMap<String, URI>(); private final Map<String, MapPrinter> printers = Maps.newConcurrentMap(); private final HashMap<String, Long> configurationFileLastModifiedTimes = new HashMap<String, Long>(); @PostConstruct private void validateConfigurationFiles() { if (!this.configurationFiles.containsKey(DEFAULT_CONFIGURATION_FILE_KEY)) { throw new BeanCreationException(getClass().getName() + " requires that one of the configurationFiles is called '" + DEFAULT_CONFIGURATION_FILE_KEY + "'"); } for (URI file : this.configurationFiles.values()) { Assert.isTrue(this.configFileLoader.isAccessible(file), file + " does not exist or is not accessible."); } } @Override public final synchronized MapPrinter create(@Nullable final String app) throws NoSuchAppException { String finalApp = app; if (app == null) { finalApp = DEFAULT_CONFIGURATION_FILE_KEY; } URI configFile = this.configurationFiles.get(finalApp); if (configFile == null) { throw new NoSuchAppException("There is no configurationFile registered in the " + getClass().getName() + " bean with the " + "id: " + "'" + finalApp + "'"); } final long lastModified; if (this.configurationFileLastModifiedTimes.containsKey(finalApp)) { lastModified = this.configurationFileLastModifiedTimes.get(finalApp); } else { lastModified = 0L; } MapPrinter printer = this.printers.get(finalApp); Optional<Long> configFileLastModified = this.configFileLoader.lastModified(configFile); if (configFileLastModified.isPresent() && configFileLastModified.get() > lastModified) { // file modified, reload it LOGGER.info("Configuration file modified. Reloading..."); this.printers.remove(finalApp); printer = null; } if (printer == null) { if (configFileLastModified.isPresent()) { this.configurationFileLastModifiedTimes.put(finalApp, configFileLastModified.get()); } try { LOGGER.info("Loading configuration file: " + configFile); printer = this.applicationContext.getBean(MapPrinter.class); byte[] bytes = this.configFileLoader.loadFile(configFile); printer.setConfiguration(configFile, bytes); this.printers.put(finalApp, printer); } catch (Throwable e) { if (e instanceof ClosedByInterruptException) { // because of a bug in the JDK, the interrupted status might not be set // when throwing a ClosedByInterruptException. so, we do it manually. // see also http://bugs.java.com/view_bug.do?bug_id=7043425 Thread.currentThread().interrupt(); } LOGGER.error("Error occurred while reading configuration file", e); throw new RuntimeException("Error occurred while reading configuration file '" + configFile + "': ", e); } } return printer; } @Override public final Set<String> getAppIds() { return this.configurationFiles.keySet(); } /** * The setter for setting configuration file. It will convert the value to a URI. * * @param configurationFiles the configuration file map. */ public final void setConfigurationFiles(final Map<String, String> configurationFiles) throws URISyntaxException { this.configurationFiles.clear(); this.configurationFileLastModifiedTimes.clear(); for (Map.Entry<String, String> entry : configurationFiles.entrySet()) { if (!entry.getValue().contains(":/")) { // assume is a file this.configurationFiles.put(entry.getKey(), new File(entry.getValue()).toURI()); } else { this.configurationFiles.put(entry.getKey(), new URI(entry.getValue())); } } if (this.configFileLoader != null) { this.validateConfigurationFiles(); } } /** * Set a single directory that contains one or more subdirectories, each one that contains a config.yaml file will * be considered a print app. * * This can be called multiple times and each directory will add to the apps found in the other directories. However * the appId is based on the directory names so if there are 2 directories with the same name the second will overwrite the * first encounter. * * @param directory the root directory containing the sub-app-directories. This must resolve to a file with the */ public final void setAppsRootDirectory(final String directory) throws URISyntaxException { final Iterable<File> children; if (!directory.contains(":/")) { children = Files.fileTreeTraverser().children(new File(directory)); } else { final Optional<File> fileOptional = this.configFileLoader.toFile(new URI(directory)); if (fileOptional.isPresent()) { children = Files.fileTreeTraverser().children(fileOptional.get()); } else { throw new IllegalArgumentException(directory + " does not refer to a file on the current system."); } } for (File child : children) { final File configFile = new File(child, "config.yaml"); if (configFile.exists()) { this.configurationFiles.put(child.getName(), configFile.toURI()); } } if (this.configurationFiles.isEmpty()) { throw new IllegalArgumentException(directory + " is an emptry directory. There must be at least one subdirectory " + "containing a config.yaml file"); } // ensure there is a "default" app if (!this.configurationFiles.containsKey(DEFAULT_CONFIGURATION_FILE_KEY)) { final String next = this.configurationFiles.keySet().iterator().next(); final URI uri = this.configurationFiles.get(next); this.configurationFiles.put(DEFAULT_CONFIGURATION_FILE_KEY, uri); } } }