content: /**
   * ﻿Copyright (C) 2007 - 2019 52°North Initiative for Geospatial Open Source
   * Software GmbH
   *
   * This program is free software; you can redistribute it and/or modify it
   * under the terms of the GNU General Public License version 2 as published
   * by the Free Software Foundation.
   *
   * If the program is linked with libraries which are licensed under one of
   * the following licenses, the combination of the program with the linked
   * library is not considered a "derivative work" of the program:
   *
   *       • Apache License, version 2.0
   *       • Apache Software License, version 1.0
   *       • GNU Lesser General Public License, version 3
   *       • Mozilla Public License, versions 1.0, 1.1 and 2.0
   *       • Common Development and Distribution License (CDDL), version 1.0
   *
   * Therefore the distribution of the program linked with libraries licensed
   * under the aforementioned licenses, is permitted by the copyright holders
   * if the distribution is compliant with both the GNU General Public
   * License version 2 and the aforementioned licenses.
   *
   * As an exception to the terms of the GPL, you may copy, modify,
   * propagate, and distribute a work formed by combining 52°North WPS
   * GeoTools Modules with the Eclipse Libraries, or a work derivative of
   * such a combination, even if such copying, modification, propagation, or
   * distribution would otherwise violate the terms of the GPL. Nothing in
   * this exception exempts you from complying with the GPL in all respects
   * for all of the code used other than the Eclipse Libraries. You may
   * include this exception and its grant of permissions when you distribute
   * 52°North WPS GeoTools Modules. Inclusion of this notice with such a
   * distribution constitutes a grant of such permissions. If you do not wish
   * to grant these permissions, remove this paragraph from your
   * distribution. "52°North WPS GeoTools Modules" means the 52°North WPS
   * modules using GeoTools functionality - software licensed under version 2
   * or any later version of the GPL, or a work based on such software and
   * licensed under the GPL. "Eclipse Libraries" means Eclipse Modeling
   * Framework Project and XML Schema Definition software distributed by the
   * Eclipse Foundation and licensed under the Eclipse Public License Version
   * 1.0 ("EPL"), or a work based on such software and licensed under the EPL.
   *
   * 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 General
   * Public License for more details.
   */
  package org.n52.wps.io.datahandler.generator.mapserver;
  
  import java.io.File;
  import java.io.FileInputStream;
  import java.io.FileOutputStream;
  import java.io.IOException;
  import java.io.Serializable;
  import java.net.URISyntaxException;
  import java.net.URL;
  import java.nio.channels.FileChannel;
  import java.util.HashMap;
  import java.util.Map;
  
  import org.geotools.data.DefaultTransaction;
  import org.geotools.data.FeatureStore;
  import org.geotools.data.Transaction;
  import org.geotools.data.shapefile.ShapefileDataStore;
  import org.geotools.data.shapefile.ShapefileDataStoreFactory;
  import org.geotools.data.simple.SimpleFeatureCollection;
  import org.geotools.data.simple.SimpleFeatureSource;
  import org.geotools.data.simple.SimpleFeatureStore;
  import org.opengis.feature.simple.SimpleFeatureType;
  import org.opengis.referencing.crs.CoordinateReferenceSystem;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  import edu.umn.gis.mapscript.classObj;
  import edu.umn.gis.mapscript.layerObj;
  import edu.umn.gis.mapscript.mapObj;
  import edu.umn.gis.mapscript.styleObj;
  
  /**
   * This class managed the modifying of the mapfile of an UMN Mapserver. Right
   * now it only supports the adding of a Geotools FeatureCollection.
   * 
   * @author Jacob Mendt
   * 
   * @TODO better solution for replacing the old with the new mapfile.
   * @TODO Giving the ShapefileRepository as an external Input in the method
   *       addFeatureCollectionToMapfile is maybe not the best way.
   * @TODO check if layer is already in mapfile.
   * 
   */
  public final class MSMapfileBinding {
  
  	// thread-safe Singleton-Patterns
  	private static MSMapfileBinding instance = new MSMapfileBinding();
  
  	private static Logger LOGGER = LoggerFactory.getLogger(MSMapfileBinding.class);
  
  	/**
  	 * Default initialization of a new MSMapfileBinding Object.
  	 */
  	private MSMapfileBinding() {
  	}
  
  	/**
  	 * Implementation of a thread-safe Singleton Pattern
  	 * 
  	 * @return MSMapfileBinding.class
  	 */
  	public static synchronized MSMapfileBinding getInstance() {
  		return instance;
  	}
  
  	/**
  	 * This method adds a FeatureCollection as an extra layer to the default
  	 * mapfile of this MSMapfileBinding object.
  	 * 
  	 * @param ftColl
  	 *            Input FeatureCollection which should be added as a layer to
  	 *            the mapfile.
  	 * @param workspace
  	 *            Path to the workspace folder of the mapfile object.
  	 * @param mapfile
  	 *            Name of the mapfile object in the workspace folder.
  	 * @param dataRep
  	 *            Path to the data repository of the mapserver.
  	 * 
  	 * @return The name of the layer of featureCollection which was added to the
  	 *         mapfile.
  	 */
  	public synchronized String addFeatureCollectionToMapfile(
  			SimpleFeatureCollection ftColl, String workspacePath, String mapfilePath,
  			String dataRepPath) {
  
  		// saves the featureCollection as a shapefile in the dataRepository of
  		// the mapfile
  		String shapefilePath = null;
  		try {
  			shapefilePath = this.saveFeatureCollectionToShapefile(ftColl,
  					workspacePath, dataRepPath);
  			LOGGER.debug("Saving FeatureCollection as a Shapefile to the data repository of the mapfile successful.");
  		} catch (IOException e1) {
  			LOGGER.error("Error while saving FeatureCollection as a Shapefile to the data repository of the mapfile.");
  			e1.printStackTrace();
  		} catch (URISyntaxException e1) {
  			LOGGER.error("Error while saving FeatureCollection as a Shapefile to the data repository of the mapfile.");
  			e1.printStackTrace();
  		} catch (Exception e) {
  			LOGGER.error("Error while saving FeatureCollection as a Shapefile to the data repository of the mapfile.");
  			e.printStackTrace();
  		}
  
  		// makes a temporary copy of the mapfile
  		String orgMapfilePath = workspacePath + mapfilePath;
  		String tmpMapfilePath = workspacePath + System.currentTimeMillis()
  				+ ".map";
  		try {
  			if (copyMapfile(orgMapfilePath, tmpMapfilePath)) {
  				LOGGER.debug("Creating of a temporary copy of the mapfile successful.");
  			} else {
  				LOGGER.error("Error while creating a temporary copy of the mapfile.");
  				return null;
  			}
  		} catch (IOException e) {
  			LOGGER.error("Error while creating a temporary copy of the mapfile.");
  			e.printStackTrace();
  		}
  
  		// adds a the shapefile as a layer to the mapfile
  		String layerName = null;
  		try {
  			layerName = this.addShapefileAsLayerToMapfile(shapefilePath,
  					tmpMapfilePath, workspacePath);
  			LOGGER.debug("Adding shapefile as a layer to the mapfile successful.");
  		} catch (Exception e) {
  			LOGGER.error("Error while adding shapefile as a layer to the mapfile.");
  			e.printStackTrace();
  		}
  
  		// replace original mapfile through the temporary mapfile
  		try {
  			if (this.deleteMapfile(orgMapfilePath)) {
  				if (this.copyMapfile(tmpMapfilePath, orgMapfilePath)) {
  					this.deleteMapfile(tmpMapfilePath);
  					LOGGER.info("Adding a FeatureCollection as a layer to the mapfile successful.");
  					return layerName;
  				}
  			}
  		} catch (Exception e) {
  			LOGGER.error("Error while updating the original mapfile");
  			e.printStackTrace();
  		}
  
  		// default return
  		return null;
  	}
  
  	/**
  	 * This method adds a shapfile as an extra layer to the default mapfile of
  	 * this MSMapfileBinding object.
  	 * 
  	 * @param shapefilePath
  	 *            Path to the shapefile which should be added to the mapfile.
  	 * @param tmpMapfilePath
  	 *            Path to the temporary copy of the mapfile.
  	 * @param workspacePath
  	 *            to the workspace folder of the mapfile object
  	 * 
  	 * @return The name of the added layer.
  	 * 
  	 * @TODO Check if layer already exist in the mapfile.
  	 */
  	private synchronized String addShapefileAsLayerToMapfile(
  			String shapefilePath, String tmpMapfilePath, String workspacePath) {
  
  		// initalize mapfile object
  		mapObj mapfileObj = new mapObj(tmpMapfilePath);
  
  		try {
  			// initialize a BindingClass between the shapefile and mapscript
  			// objects
  			MSLayerBinding msLayer = null;
  			try {
  				msLayer = new MSLayerBinding(shapefilePath, workspacePath);
  				LOGGER.debug("Initialzing MSLayerBinding object successful.");
  			} catch (Exception e) {
  				LOGGER.error("Error while initializing MSLayerBinding object.");
  				e.printStackTrace();
  			}
  
  			if (msLayer != null) {
  
  				// initalize a new mapfile layer object
  				layerObj layer = new layerObj(mapfileObj);
  
  				/*
  				 * setting the propertys of the layer
  				 */
  				layer.setData(msLayer.getDataSourcePath());
  				layer.setDump(1); // True -> Switch to allow MapServer to return
  									// data in GML format
  				layer.setName(msLayer.getMdTitle());
  				layer.setProjection(msLayer.getMdCRS());
  				layer.setStatus(1); // means ON - Sets the current status of the
  									// layer. Often modified by MapServer
  									// itself. Default turns the layer on
  									// permanently.
  				layer.setType(msLayer.getGeometryType());
  
  				// set metadata
  				layer.setMetaData("wms_title", msLayer.getMdTitle());
  				layer.setMetaData("wms_timestamp", msLayer.getMdTimestamp()); // only date of the timestamp when the layer was added to the mapfile
  
  				/*
  				 * create a class and a style object of the layer and set styles
  				 * and properties
  				 */
  				classObj layClass = new classObj(layer);
  				layClass.setName(msLayer.getMdTitle());
  				styleObj layStyle = new styleObj(layClass);
  				layStyle.setColor(MSColorStyles.getDefaultColor());
  				layStyle.setOutlinecolor(MSColorStyles.getDefaultOutlineColor());
  
  				/*
  				 * checks if the layerCRS is already supported by the wms, if
  				 * not adds the layer CRS to the global metadata tag "wms_srs"
  				 */
  
  				String wmsSupportedCRS = mapfileObj.getMetaData("wms_srs");
  				mapfileObj.setMetaData(
  						"wms_srs",
  						this.getMDSupportedCRS(wmsSupportedCRS,
  								msLayer.getMdCRS()));
  				// saves the new layer
  				mapfileObj.save(tmpMapfilePath);
  
  				LOGGER.debug("Setting layer data successful.");
  				return msLayer.getMdTitle();
  			}
  		} catch (Exception e) {
  			LOGGER.error("Error while adding shapefile to mapfile.");
  			e.printStackTrace();
  		} finally {
  			mapfileObj = null;
  		}
  
  		// default return
  		return null;
  	}
  
  	/**
  	 * Checks if the metadata tag "wms_srs" already contains the crs of the
  	 * layer
  	 * 
  	 * @param wmsSupportedCrs
  	 *            content of the metadata tag "wms_srs"
  	 * @param mdCrs
  	 *            epsg code of the layer
  	 * @return
  	 */
  	private String getMDSupportedCRS(String wmsSupportedCrs, String mdCrs) {
  		String[] aCrs = wmsSupportedCrs.split(" ");
  		for (int i = 0; i < aCrs.length; i++) {
  			if (aCrs[i].equalsIgnoreCase(mdCrs)) {
  				return wmsSupportedCrs;
  			}
  		}
  		wmsSupportedCrs = wmsSupportedCrs + " " + mdCrs;
  		return wmsSupportedCrs;
  	}
  
  	/**
  	 * Creates a copy of the mapfile
  	 * 
  	 * @param String
  	 *            orgMapfilePath Path to the original mapfile.
  	 * @param String
  	 *            tmpMapfilePath Path to the temporary mapfile.
  	 * 
  	 * @return <tt>true</tt> if the copying of the mapfile went okay.
  	 * 
  	 * @throws IOException
  	 *             If something went wrong while copying the content of the
  	 *             original mapfile to the temporary mapfile.
  	 */
  	private synchronized boolean copyMapfile(String orgMapfilePath,
  			String tmpMapfilePath) throws IOException {
  
  		// does a tmp copy of the mapfile
  		File orgMapfile = new File(orgMapfilePath);
  		File tmpMapfile = new File(tmpMapfilePath);
  		FileChannel inCh = new FileInputStream(orgMapfile).getChannel();
  		FileChannel outCh = new FileOutputStream(tmpMapfile).getChannel();
  		try {
  			inCh.transferTo(0, inCh.size(), outCh);
  			LOGGER.debug("Creating of an temporary copy of the mapfile successful.");
  			return true;
  		} catch (IOException e) {
  			LOGGER.error("Error while copying mapfile");
  			e.printStackTrace();
  
  		} finally {
  			if (inCh != null)
  				inCh.close();
  			if (outCh != null)
  				outCh.close();
  		}
  
  		// default return
  		return false;
  	}
  
  	/**
  	 * Deletes the mapfile.
  	 * 
  	 * @param mapfilePath
  	 *            Path to the mapfile which should be delete.
  	 * 
  	 * @return <tt>true</tt> if the mapfile was deleted successful.
  	 */
  	private boolean deleteMapfile(String mapfilePath) {
  		File tmpFile = new File(mapfilePath);
  		if (tmpFile.exists()) {
  			tmpFile.delete();
  			return true;
  		} else {
  			LOGGER.debug("Mapfile doesn't exist.");
  			return false;
  		}
  	}
  
  	/**
  	 * Saves the FeatureCollection as a shapefile in the data repository of the
  	 * mapserver. If the CRS of the FeatureCollection is not WGS84 the
  	 * FeatureCollection will be transformed to WGS 84
  	 * 
  	 * @param ftColl
  	 *            FeatureCollection which should be saved as shapefile
  	 * @param workspacePath
  	 *            Path to the workspace folder of the mapfile object.
  	 * @param dataRepository
  	 *            Path to the data repository of the mapserver.
  	 * 
  	 * @return Path to the shapefile, which lies in the data repository of the
  	 *         mapserver.
  	 * 
  	 * @throws IOException
  	 * @throws URISyntaxException
  	 * @throws Exception
  	 */
  	private String saveFeatureCollectionToShapefile(
  			SimpleFeatureCollection ftColl, String workspacePath,
  			String dataRepository) throws IOException, URISyntaxException,
  			Exception {
  		
  		//FeatureCollection ftColl = createCorrectFeatureCollection(ftCollWrong);
  		//FeatureCollectionTester tester = new FeatureCollectionTester();
  		//tester.testFeatureCollection(ftColl);
  		
  		SimpleFeatureType TYPE = (SimpleFeatureType) ftColl.getSchema();
  
  		String shapefilePath;
  
  		// Create the shapefilePath
  		if (dataRepository.endsWith("/") || dataRepository.endsWith("\\")) {
  			shapefilePath = workspacePath + dataRepository + "shape_"
  					+ TYPE.getName().getLocalPart().toLowerCase() + "_"
  					+ System.currentTimeMillis() + ".shp";
  		} else
  			shapefilePath = workspacePath + dataRepository + "/shape_"
  					+ TYPE.getName().getLocalPart().toLowerCase() + "_"
  					+ System.currentTimeMillis() + ".shp";
  
  		/*
  		 * Get an output file name and create the new shapefile
  		 */
  		File shapefile = new File(shapefilePath);
  		URL url = shapefile.toURI().toURL();
  		ShapefileDataStoreFactory dataStoreFactory = new ShapefileDataStoreFactory();
  		Map<String, Serializable> params = new HashMap<String, Serializable>();
  		params.put("url", url);
  		params.put("create spatial index", Boolean.TRUE);
  		ShapefileDataStore newDataStore = (ShapefileDataStore) dataStoreFactory
  				.createNewDataStore(params);
  
  		// Initialize the CRS for transformation
  		CoordinateReferenceSystem sourceCrs = TYPE
  				.getCoordinateReferenceSystem();
  
  		newDataStore.createSchema(TYPE);
  		newDataStore.forceSchemaCRS(sourceCrs);
  
  		/*
  		 * Write the features to the shapefile
  		 */
  		Transaction transaction = new DefaultTransaction("create");
  		String typeName = newDataStore.getTypeNames()[0];
  		SimpleFeatureSource featureSource = newDataStore.getFeatureSource(typeName);
  		if (featureSource instanceof FeatureStore) {
  			SimpleFeatureStore featureStore = (SimpleFeatureStore) featureSource;
  			featureStore.setTransaction(transaction);
  			try {
  				featureStore.addFeatures(ftColl);
  				transaction.commit();
  				LOGGER.debug("Saving FeatureCollection as shapefile successful.");
  			} catch (Exception problem) {
  				LOGGER.error("Error while saving FeatureCollection as shapefile.");
  				problem.printStackTrace();
  				transaction.rollback();
  			} finally {
  				transaction.close();
  			}
  		} else {
  			LOGGER.error(typeName + " does not support read/write access");
  			return null;
  		}
  
  		// returns the path to the shapefile
  		return url.getPath();
  	}
  }
  