/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


package org.apache.oodt.cas.curation.service;

//JDK imports

import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import net.sf.json.JSONSerializer;
import org.apache.oodt.cas.curation.structs.ExtractorConfig;
import org.apache.oodt.cas.curation.util.CurationXmlStructFactory;
import org.apache.oodt.cas.curation.util.ExtractorConfigReader;
import org.apache.oodt.cas.curation.util.exceptions.CurationException;
import org.apache.oodt.cas.filemgr.catalog.Catalog;
import org.apache.oodt.cas.filemgr.repository.XMLRepositoryManager;
import org.apache.oodt.cas.filemgr.structs.Element;
import org.apache.oodt.cas.filemgr.structs.Product;
import org.apache.oodt.cas.filemgr.structs.ProductType;
import org.apache.oodt.cas.filemgr.structs.Reference;
import org.apache.oodt.cas.filemgr.structs.exceptions.CatalogException;
import org.apache.oodt.cas.filemgr.structs.exceptions.RepositoryManagerException;
import org.apache.oodt.cas.filemgr.structs.exceptions.ValidationLayerException;
import org.apache.oodt.cas.filemgr.system.FileManagerClient;
import org.apache.oodt.cas.filemgr.util.GenericFileManagerObjectFactory;
import org.apache.oodt.cas.filemgr.validation.XMLValidationLayer;
import org.apache.oodt.cas.metadata.MetExtractor;
import org.apache.oodt.cas.metadata.Metadata;
import org.apache.oodt.cas.metadata.SerializableMetadata;
import org.apache.oodt.cas.metadata.exceptions.MetExtractionException;
import org.apache.oodt.cas.metadata.util.GenericMetadataObjectFactory;
import org.springframework.util.StringUtils;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.*;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;

@Path("metadata")
/**
 *
 * A web-service endpoint for dealing with CAS {@link Metadata} object.
 *
 * @author pramirez
 * @version $Id$
 */
public class MetadataResource extends CurationService {

  @Context
  UriInfo uriInfo;

  @Context
  private ServletContext context;

  private static final long serialVersionUID = 1930946924218765724L;

  public static final String STAGING = "staging";

  public static final String CATALOG = "catalog";

  public static final String PRODUCT_TYPE = "productType";

  public static final String UPDATE = "update";

  public static final String DELETE = "delete";
  private static Logger LOG = Logger.getLogger(MetadataResource.class.getName());
  // single instance of CAS catalog shared among all requests
  private Catalog catalog = null;

  public MetadataResource(){

  }

  public MetadataResource(@Context ServletContext context) {

  }

  @GET
  @Path(STAGING)
  @Produces("text/plain")
  public String getStagingMetadata(@QueryParam("id") String id,
      @DefaultValue(FORMAT_HTML) @QueryParam("format") String format,
      @QueryParam("configId") String configId,
      @DefaultValue("false") @QueryParam("overwrite") Boolean overwrite,
      @Context HttpServletRequest req, @Context HttpServletResponse res) {

      Metadata metadata;

      try {
        metadata = this.getStagingMetadata(id, configId, overwrite);
      } catch (Exception e) {
        return "<div class=\"error\">" + e.getMessage() + "</div>";
      }

      if (FORMAT_HTML.equals(format)) {
        return this.getMetadataAsHTML(metadata);
      }
      return this.getMetadataAsJSON(metadata).toString();
  }

  @GET
  @Path("extractor/config")
  @Produces("text/plain")
  public String getMetExtractorConfigList(
      @DefaultValue("") @QueryParam("current") String current,
      @DefaultValue(FORMAT_HTML) @QueryParam("format") String format) {
    String[] configIds = this.getFilesInDirectory(config
        .getMetExtrConfUploadPath(), false);

    if (FORMAT_HTML.equals(format)) {
      return this.getExtractorConfigIdsAsHTML(configIds, current);
    }
    return this.getExtractorConfigIdsAsJSON(configIds);
  }

  protected String getExtractorConfigIdsAsHTML(String[] configIds,
      String current) {
    StringBuilder html = new StringBuilder();
    for (String configId : configIds) {
      html.append("<option ");
      if (configId.equals(current)) {
        html.append("selected ");
      }
      html.append("value=\"");
      html.append(configId);
      html.append("\">");
      html.append(configId);
      html.append("</option>\r\n");
    }
    return html.toString();
  }

  protected String getExtractorConfigIdsAsJSON(String[] configIds) {
    // TODO: Support JSON
    return "Not Implemented...";
  }

  /**
   *
   * @param id
   *          Relative path from staging root to product. The met extension will
   *          be added to this id to look up and see if a met file exists with
   *          in the met area.
   * @param configId
   *          Reference to the extractor config. {@link ExtractorConfigReader}
   *          will load the configuration
   * @param overwrite
   *          Flag to indicate whether or not to overwrite a met file if present
   *          in the staging area.
   * @return The {@link Metadata} retrieved from the met area path if present or
   *         extracted using the met extractor config.
   * @throws FileNotFoundException
   * @throws InstantiationException
   * @throws IOException
   * @throws MetExtractionException
   */
  protected Metadata getStagingMetadata(String id, String configId,
      Boolean overwrite) throws InstantiationException,
      IOException, MetExtractionException {
    if (configId == null || configId.trim().length() == 0) {
      return this.readMetFile(id + CurationService.config.getMetExtension());
    } else {
      String relMetPath = id.startsWith("/") ? id : "/" + id;
      String pathToMetFile = CurationService.config.getMetAreaPath()
          + relMetPath + CurationService.config.getMetExtension();
      if (!overwrite && new File(pathToMetFile).exists()) {
        return this.readMetFile(id + CurationService.config.getMetExtension());
      } else {
        // Make sure the parent directory exists
        new File(pathToMetFile).getParentFile().mkdirs();
        Metadata metadata = this.runMetExtractor(id, ExtractorConfigReader
            .readFromDirectory(
                new File(CurationService.config
                .getMetExtrConfUploadPath()), configId));
        this.writeMetFile(id, metadata);
        return metadata;
      }
    }
  }

  /**
   *
   * @param id
   *          Relative path from staging root to the product
   * @param config
   *          Configuration to run this met extractor
   * @return
   * @throws MetExtractionException
   */
  protected Metadata runMetExtractor(String id, ExtractorConfig config)
      throws MetExtractionException {
    MetExtractor metExtractor = GenericMetadataObjectFactory
        .getMetExtractorFromClassName(config.getClassName());
    metExtractor.setConfigFile(config.getConfigFiles().get(0));
    return metExtractor.extractMetadata(CurationService.config
        .getStagingAreaPath()
        + "/" + id);
  }

  @GET
  @Path(CATALOG)
  @Produces("text/plain")
  public String getCatalogMetadata(@QueryParam("id") String id,
      @DefaultValue(FORMAT_HTML) @QueryParam("format") String format,
      @Context HttpServletRequest req, @Context HttpServletResponse res) {

      // Call file manager to get metadata
      Product prod;
      Metadata metadata;
      String productId = id.substring(id.lastIndexOf("/") + 1);

      try (FileManagerClient fmClient = CurationService.config.getFileManagerClient()) {
        prod = fmClient.getProductById(productId);
        metadata = this.getCatalogMetadata(prod);
      } catch (Exception e) {
        return "<div class=\"error\">" + e.getMessage() + "</div>";
      }

      if (FORMAT_HTML.equals(format)) {
        return this.getMetadataAsHTML(metadata);
      }
      return this.getMetadataAsJSON(metadata).toString();
  }

  @POST
  @Path(CATALOG)
  @Consumes("application/x-www-form-urlencoded")
  @Produces("text/plain")
  public String setCatalogMetadata(MultivaluedMap<String, String> formParams,
      @FormParam("id") String id) {

    Product prod;
    Metadata metadata = this.getMetadataFromMap(formParams);

    String productId = id.substring(id.lastIndexOf("/") + 1);

    try (FileManagerClient fmClient = CurationService.config.getFileManagerClient()) {
      prod = fmClient.getProductById(productId);
      this.updateCatalogMetadata(prod, metadata);
    } catch (Exception e) {
      LOG.log(Level.SEVERE, e.getMessage());
      return "<div class=\"error\">" + e.getMessage() + "</div>";
    }

    return this.getMetadataAsHTML(metadata);
  }

  @GET
  @Path(PRODUCT_TYPE)
  @Produces("text/plain")
  public String getProductTypeMetadata(@QueryParam("id") String id,
      @DefaultValue(FORMAT_HTML) @QueryParam("format") String format,
      @Context HttpServletRequest req, @Context HttpServletResponse res) {


    Metadata metadata;
    String[] idParts = id.split("/", 3);
    String policy = idParts[1];
    String productType = idParts[2];
    productType = productType.substring(0, productType.lastIndexOf("/"));
    try {
      metadata = getProductTypeMetadataForPolicy(policy, productType);
    } catch (Exception e) {
      return "<div class=\"error\">" + e.getMessage() + "</div>";
    }

    if (FORMAT_HTML.equals(format)) {
      return this.getMetadataAsHTML(metadata);
    }
    return this.getMetadataAsJSON(metadata).toString();
  }


  @POST
  @Path(PRODUCT_TYPE)
  @Consumes("application/x-www-form-urlencoded")
  @Produces("text/plain")
  public String setProductTypeMetadata(MultivaluedMap<String, String> formParams) {
    String[] idParts = formParams.getFirst("id").split("/");
    String policy = idParts[1];
    String productType = idParts[2];
    try {
    this.writeProductTypeMetadata(policy, productType, this
        .getMetadataFromMap(formParams));
    } catch (Exception e) {
      return "<div class=\"error\">" + e.getMessage() + "</div>";
    }
    return "";
  }

  @POST
  @Path(STAGING)
  @Consumes("application/x-www-form-urlencoded")
  @Produces("text/plain")
  public String setStagingMetadata(MultivaluedMap<String, String> formParams) {
    try {
      this.writeMetFile(formParams.getFirst("id"), this
          .getMetadataFromMap(formParams));
    } catch (Exception e) {
      return "<div class=\"error\">" + e.getMessage() + "</div>";
    }
    return "";
  }

  @GET
  @Path("staging/info")
  @Produces("text/plain")
  public String getMetadataInfo(@QueryParam("id") String id) {

      return "Staging met info";
  }


  private JSONObject getMetadataAsJSON(Metadata metadata) {
    return JSONObject.fromObject(metadata.getMap());
  }

  private Metadata getMetadataFromJSON(String metadataJSON) {
    JSONObject json = (JSONObject) JSONSerializer.toJSON(metadataJSON);
    Metadata metadata = new Metadata();

    Set<String> keys = json.keySet();
    for (String key : keys) {
      List values = (List) JSONSerializer.toJava((JSONArray) json.get(key));
      metadata.addMetadata(key, values);
    }

    return metadata;
  }

  private Metadata getMetadataFromMap(MultivaluedMap<String, String> formParams) {
    Metadata metadata = new Metadata();

    for (Map.Entry<String, List<String>> entry : formParams.entrySet()) {
      if (entry.getKey().startsWith("metadata.")) {
        String newKey = entry.getKey().substring(entry.getKey().indexOf('.') + 1);
        for (String value : entry.getValue()) {
          metadata.addMetadata(newKey, value);
        }
      }
    }

    return metadata;
  }

  protected String getMetadataAsHTML(Metadata metadata) {
    if (metadata == null) {
      return "<table></table>";
    }

    StringBuilder html = new StringBuilder();

    html.append("<table>\r\n");
    for (String key : (Set<String>) metadata.getMap().keySet()) {
      html.append(" <tr>\r\n");
      html.append("  <th>").append(key).append("</th>\r\n");
      html.append("  <td class=\"").append(key).append("\">");
      List<String> values = metadata.getAllMetadata(key);
      for (Iterator<String> i = values.iterator(); i.hasNext();) {
        html.append("<span>").append(i.next()).append("</span>");
        if (i.hasNext()) {
          html.append(", ");
        }
      }
      for (String value : (List<String>) metadata.getAllMetadata(key)) {
      }
      html.append("</td>\r\n");
      html.append(" </tr>\r\n");
    }
    html.append("</table>\r\n");

    return html.toString();
  }


  /**
   * Reads a {@link Metadata} object from a String representation of a .met
   * {@link File}.
   *
   * @param file
   *          The full path to the .met {@link File}.
   * @return The read-in CAS {@link Metadata} object.
   * @throws InstantiationException
   * @throws InstantiationException
   *           If there is an error instantiating the {@link Metadata} class.
   * @throws IOException
   * @throws FileNotFoundException
   * @throws FileNotFoundException
   *           If the .met {@link File} is not found.
   * @throws IOException
   *           If there is an IO problem opening the met file.
   */
  public Metadata readMetFile(String file) throws InstantiationException,
      IOException {
    SerializableMetadata metadata = new SerializableMetadata("UTF-8", false);
    metadata.loadMetadataFromXmlStream(new FileInputStream(config
        .getMetAreaPath()
        + "/" + file));

    return metadata;
  }

  /**
   * Retrieves the cataloged {@link Metadata} associated with a {@link Product}.
   *
   * @param product
   *          The {@link Product} to obtain cataloged {@link Metadata} for.
   * @return The cataloged {@link Metadata} for {@link Product}.
   * @throws CatalogException
   *           If there is an error talking to the CAS File Manager
   *           {@link Catalog}.
   */
  public Metadata getCatalogMetadata(Product product) throws CatalogException {
    try(    FileManagerClient fmClient = CurationService.config.getFileManagerClient()){
      return fmClient.getMetadata(fmClient.getProductById(product.getProductId()));
    } catch (IOException e){
      LOG.severe("Error occurred when fetching catalog metadata for product: " +
              product.getProductName() + " :" + e.getMessage());
      return null;
    }
  }

  /**
   * Writes a CAS {@link Metadata} {@link File} using the given identifier.
   *
   * @param id
   *          The identifier of the .met file to write.
   * @param metadata
   *          The {@link Metadata} object to persist and write to a {@link File}
   *          .
   * @throws FileNotFoundException
   *           If the .met {@link File} cannot be written.
   * @throws IOException
   *           If there is an IO exception writing the {@link File}.
   */
  public void writeMetFile(String id, Metadata metadata)
      throws IOException {
    SerializableMetadata serMet = new SerializableMetadata(metadata, "UTF-8",
        false);
    serMet.writeMetadataToXmlStream(new FileOutputStream(new File(config
        .getMetAreaPath(), id + config.getMetExtension())));
  }

  /**
   * Method to update the catalog metadata for a given product.
   * All current metadata fields will be preserved,
   * except those specified in the HTTP POST request as 'metadata.<field_name>=<field_value>'.
   *
   * @param id
   * 	identifier of CAS product - either 'id' or 'name' must be specified
   * @param name
   * 	name of CAS product - either 'id' or 'name' must be specified
   * @param formParams
   * 	HTTP (name, value) form parameters. The parameter names MUST start with "metadata."
   * @param replace
   * 	optional flag set to false to add the new metadata values to the existing values, for the given flags
   */
  @POST
  @Path(UPDATE)
  @Consumes("application/x-www-form-urlencoded")
  @Produces("text/plain")
  public String updateMetadata(MultivaluedMap<String, String> formParams,
		  @FormParam("id") String id,
		  @FormParam("name") String name,
		  @DefaultValue("true") @FormParam("replace") boolean replace,
		  @DefaultValue("false") @FormParam("remove") boolean remove) {

  	// new metadata from HTTP POST request
    Metadata newMetadata = this.getMetadataFromMap(formParams);
    // empty metadata
    Metadata metadata;
    try (FileManagerClient fmClient = CurationService.config.getFileManagerClient()) {
      // retrieve product from catalog
      Product product;
      if (StringUtils.hasText(id)) {
    	  id = id.substring(id.lastIndexOf("/") + 1);
    	  product = fmClient.getProductById(id);
      } else if (StringUtils.hasText(name)) {
    	  product = fmClient.getProductByName(name);
      } else {
    	  throw new Exception("Either the HTTP parameter 'id' or the HTTP parameter 'name' must be specified");
      }

      // retrieve existing metadata
      metadata = fmClient.getMetadata(product);

      // remove product references (as they will be added later)
      metadata.removeMetadata("reference_orig");
      metadata.removeMetadata("reference_data_store");
      metadata.removeMetadata("reference_fileSize");
      metadata.removeMetadata("reference_mimeType");

      // merge new and existing metadata
      metadata.addMetadata(newMetadata);

      // replace metadata values for keys specified in HTTP request (not others)
      if (replace) {
    	  for (String key : newMetadata.getAllKeys()) {
    		  metadata.replaceMetadata(key, newMetadata.getAllMetadata(key));
    	  }
      }

      // remove metadata tags
      if (remove) {
	      for (String key : newMetadata.getAllKeys()) {
	      	metadata.removeMetadata(key);
	      }
      }

      // insert old and new metadata
      fmClient.updateMetadata(product, metadata);

      // return product id to downstream processors
      return "id="+product.getProductId();

    } catch (Exception e) {
      LOG.log(Level.SEVERE, e.getMessage());
      // return error message
      throw new WebApplicationException(e, Response.Status.INTERNAL_SERVER_ERROR);
    }
  }

  /**
   * Updates the cataloged {@link Metadata} for a {@link Product} in the CAS
   * File Manager.
   *
   * @param product
   *          The {@link Product} to update {@link Metadata} for.
   * @throws CatalogException
   *           If any error occurs during the update.
   * @throws IOException
   * @throws FileNotFoundException
   */
  public void updateCatalogMetadata(Product product, Metadata newMetadata)
      throws CatalogException, IOException {
    InputStream is = new FileInputStream(CurationService.config.getFileMgrProps());
    try {
      System.getProperties().load(is);
    }
    finally{
      is.close();
    }
    Catalog catalog = this.getCatalog();

    Metadata oldMetadata = catalog.getMetadata(product);
    List<Reference> references = catalog.getProductReferences(product);
    Product newProduct = new Product(product.getProductName(), product
        .getProductType(), product.getProductStructure(), product
        .getTransferStatus(), product.getProductReferences());
    // Constructor is bugged and doesn't set transfer status
    newProduct.setTransferStatus(product.getTransferStatus());
    catalog.removeMetadata(oldMetadata, product);
    catalog.removeProduct(product);
    newProduct.setProductId(product.getProductId());
    catalog.addProduct(newProduct);
    newProduct.setProductReferences(references);
    catalog.addProductReferences(newProduct);
    catalog.addMetadata(newMetadata, newProduct);
  }

  /**
   * Method to delete a specific product from the catalog
   *
   * @param id
   * 	identifier of CAS product - either 'id' or 'name' must be specified
   * @param name
   * 	name of CAS product - either 'id' or 'name' must be specified
   * @return the product ID of the deleted product if deletion successful
   */
  @POST
  @Path(DELETE)
  @Consumes("application/x-www-form-urlencoded")
  @Produces("text/plain")
  public String deleteCatalogMetadata(
		  @FormParam("id") String id,
		  @FormParam("name") String name) {

	  try (FileManagerClient fmClient = CurationService.config.getFileManagerClient()) {
		  // retrieve product from catalog
		  Product product;
          if (StringUtils.hasText(id)) {
              id = id.substring(id.lastIndexOf("/") + 1);
              product = fmClient.getProductById(id);
          } else if (StringUtils.hasText(name)) {
              product = fmClient.getProductByName(name);
          } else {
              throw new Exception("Either the HTTP parameter 'id' or the HTTP parameter 'name' must be specified");
          }

		  // remove product from catalog
		  this.deleteCatalogProduct(product);

		  // return product id to downstream processors
		  return "id="+product.getProductId();

	  } catch (Exception e) {
		  LOG.log(Level.SEVERE, e.getMessage());
		  // return error message
		  throw new WebApplicationException(e, Response.Status.INTERNAL_SERVER_ERROR);

	  }
  }

  /**
   * Deletes a given product from the catalog
   *
   * @param product
   *          The {@link Product} to delete
   * @throws FileNotFoundException
   * @throws IOException
   * @throws CatalogException
   *           If any error occurs during this delete operation.
   */
  public void deleteCatalogProduct(Product product) throws CatalogException {
	try (FileManagerClient fmClient = CurationService.config.getFileManagerClient()){
	  fmClient.removeProduct(product);
    } catch (IOException e) {
      LOG.severe(String.format("Couldn't detele product - %s : %s", product.getProductName(), e.getMessage()));
      throw new CatalogException("Unable to delete product", e);
    }
  }

  private Metadata getProductTypeMetadataForPolicy(String policy,
      String productTypeName) throws MalformedURLException,
      InstantiationException, RepositoryManagerException {
    String rootPolicyPath = this.cleanse(CurationService.config
        .getPolicyUploadPath());
    String policyPath = new File(rootPolicyPath + policy).toURI().toURL()
        .toExternalForm();
    String[] policies = { policyPath };
    XMLRepositoryManager repMgr = new XMLRepositoryManager(Arrays
        .asList(policies));
    ProductType productType = repMgr.getProductTypeByName(productTypeName);

    return productType.getTypeMetadata();
  }

    private Metadata writeProductTypeMetadata(String policy,
            String productTypeName, Metadata metadata)
            throws MalformedURLException, InstantiationException, RepositoryManagerException, UnsupportedEncodingException,
            CurationException {
        String rootPolicyPath = this.cleanse(CurationService.config
                .getPolicyUploadPath());
        String policyPath = new File(rootPolicyPath + policy).toURI().toURL()
                .toExternalForm();
        String[] policies = {policyPath};
        XMLRepositoryManager repMgr = new XMLRepositoryManager(Arrays
                .asList(policies));

        ProductType productType = repMgr.getProductTypeByName(productTypeName);
        productType.setTypeMetadata(metadata);

        CurationXmlStructFactory.writeProductTypeXmlDocument(repMgr
                .getProductTypes(), rootPolicyPath + policy + "/product-types.xml");

        // refresh the config on the fm end
        try (FileManagerClient fmClient = CurationService.config.getFileManagerClient()) {
            fmClient.refreshConfigAndPolicy();
        } catch (IOException e) {
            LOG.severe(String.format("Unable to refresh config and policy: %s", e.getMessage()));
        }

        return productType.getTypeMetadata();
    }

  private String cleanse(String origPath) {
    String retStr = origPath;
    if (!retStr.endsWith("/")) {
      retStr += "/";
    }
    return retStr;
  }

  // Method to instantiate the CAS catalog, if not done already.
  private synchronized Catalog getCatalog() {

  	if (catalog==null) {
  		String catalogFactoryClass = this.context.getInitParameter(CATALOG_FACTORY_CLASS);
  		// preserve backward compatibility
  		if (!StringUtils.hasText(catalogFactoryClass)) {
          catalogFactoryClass = "org.apache.oodt.cas.filemgr.catalog.LuceneCatalogFactory";
        }
  		catalog = GenericFileManagerObjectFactory.getCatalogServiceFromFactory(catalogFactoryClass);
  	}

  	return catalog;
  }

  @DELETE
  @Path(PRODUCT_TYPE+"/remove")
  @Produces("text/plain")
  public boolean removeProductType(
		  @FormParam("policy") String policy,
		  @FormParam("id") String id) {
    XMLRepositoryManager xmlRepo = getRepo(policy);
    try {
      ProductType type = xmlRepo.getProductTypeById(id);
      xmlRepo.removeProductType(type);
      return true;
    } catch (RepositoryManagerException e) {
      LOG.log(Level.SEVERE, e.getMessage());
      return false;
    }
  }

  @GET
  @Path(PRODUCT_TYPE+"/parentmap")
  @Produces("text/plain")
  public String getParentTypeMap(
      @FormParam("policy") String policy) {
    XMLValidationLayer vLayer = getValidationLayer(policy);
    return JSONSerializer.toJSON(vLayer.getSubToSuperMap()).toString();
  }

  @POST
  @Path(PRODUCT_TYPE+"/parent/add")
  @Produces("text/plain")
  public boolean addParentForProductType(
      @FormParam("policy") String policy,
      @FormParam("id") String id,
      @FormParam("parentId") String parentId) {
    XMLValidationLayer vLayer = getValidationLayer(policy);
    XMLRepositoryManager xmlRepo = getRepo(policy);
    try {
      ProductType type = xmlRepo.getProductTypeById(id);
      vLayer.addParentForProductType(type, parentId);
      return true;
    } catch (RepositoryManagerException e) {
      LOG.log(Level.SEVERE, e.getMessage());
    }
    return false;
  }

  @DELETE
  @Path(PRODUCT_TYPE+"/parent/remove")
  @Produces("text/plain")
  public boolean removeParentForProductType(
      @FormParam("policy") String policy,
      @FormParam("id") String id)
          throws ValidationLayerException {
    XMLValidationLayer vLayer = getValidationLayer(policy);
    XMLRepositoryManager xmlRepo = getRepo(policy);
    try {
      ProductType type = xmlRepo.getProductTypeById(id);
      vLayer.removeParentForProductType(type);
      return true;
    } catch (RepositoryManagerException e) {
      LOG.log(Level.SEVERE, e.getMessage());
    }
    return false;
  }

  @POST
  @Path(PRODUCT_TYPE+"/elements/add")
  @Produces("text/plain")
  public boolean addElementsForProductType(
		  @FormParam("policy") String policy,
		  @FormParam("id") String id,
		  @FormParam("elementIds") String elementIds) {
    XMLValidationLayer vLayer = getValidationLayer(policy);
    XMLRepositoryManager xmlRepo = getRepo(policy);
    try {
      ProductType type = xmlRepo.getProductTypeById(id);
      for(String elementid: elementIds.split(",")) {
        Element element = vLayer.getElementById(elementid);
        if(element == null) {
          element = new Element(elementid, elementid, "", "", "Automatically added", "");
          vLayer.addElement(element);
        }
        vLayer.addElementToProductType(type, element);
      }
      return true;
    } catch (Exception e) {
      LOG.log(Level.SEVERE, e.getMessage());
    }
    return false;
  }

  @GET
  @Path(PRODUCT_TYPE+"/elements")
  @Produces("text/plain")
  public String getElementsForProductType(
		  @FormParam("policy") String policy,
		  @FormParam("id") String id,
		  @FormParam("direct") boolean direct) {
    XMLValidationLayer vLayer = getValidationLayer(policy);
    XMLRepositoryManager xmlRepo = getRepo(policy);
    try {
      ProductType type = xmlRepo.getProductTypeById(id);
      ArrayList<String> elementIds = new ArrayList<String>();
      for(Element el : vLayer.getElements(type, direct)) {
        elementIds.add(el.getElementId());
      }
      return JSONSerializer.toJSON(elementIds).toString();
    } catch (Exception e) {
      LOG.log(Level.SEVERE, e.getMessage());
    }
    return null;
  }

  @DELETE
  @Path(PRODUCT_TYPE+"/elements/remove/all")
  @Produces("text/plain")
  public boolean removeAllElementsForProductType(
		  @FormParam("policy") String policy,
		  @FormParam("id") String id) {
    XMLValidationLayer vLayer = getValidationLayer(policy);
    XMLRepositoryManager xmlRepo = getRepo(policy);
    try {
      ProductType type = xmlRepo.getProductTypeById(id);
      List<Element> elementList = vLayer.getElements(type);
      for(Element element: elementList) {
        vLayer.removeElementFromProductType(type, element);
      }
      this.removeUnusedElements(elementList, xmlRepo, vLayer);
      return true;
    } catch (Exception e) {
      LOG.log(Level.SEVERE, e.getMessage());
    }
    return false;
  }

  @DELETE
  @Path(PRODUCT_TYPE+"/elements/remove")
  @Produces("text/plain")
  public boolean removeElementsForProductType(
		  @FormParam("policy") String policy,
		  @FormParam("id") String id,
		  @FormParam("elementIds") String elementIds) {
    XMLValidationLayer vLayer = getValidationLayer(policy);
    XMLRepositoryManager xmlRepo = getRepo(policy);
    try {
      ProductType type = xmlRepo.getProductTypeById(id);
      ArrayList<Element> elements = new ArrayList<Element>();
      for(String elementId: elementIds.split(",")) {
        Element element = vLayer.getElementById(elementId);
        if(element != null) {
          vLayer.removeElementFromProductType(type, element);
          elements.add(element);
        }
      }
      this.removeUnusedElements(elements, xmlRepo, vLayer);
      return true;
    } catch (Exception e) {
      LOG.log(Level.SEVERE, e.getMessage());
    }
    return false;
  }

  @GET
  @Path(PRODUCT_TYPE+"/typeswithelement/{elementId}")
  @Produces("text/plain")
  public String getProductTypeIdsHavingElement(
		  @FormParam("policy") String policy,
		  @PathParam("elementId") String elementId) {
	  XMLValidationLayer vLayer = getValidationLayer(policy);
	  XMLRepositoryManager xmlRepo = getRepo(policy);
	  ArrayList<String> typeids = new ArrayList<String>();
      try {
    	  for(ProductType type : xmlRepo.getProductTypes()) {
    		  for(Element el : vLayer.getElements(type)) {
    			  if(el.getElementId().equals(elementId)) {
                    typeids.add(type.getProductTypeId());
                  }
    		  }
    	  }
      } catch (Exception e) {
          LOG.log(Level.SEVERE, e.getMessage());
      }
      return JSONSerializer.toJSON(typeids).toString();
  }


  /*
   * Private helper functions
   */
  private void removeUnusedElements(List<Element> elements,
		  XMLRepositoryManager xmlRepo, XMLValidationLayer vLayer)
				  throws ValidationLayerException, RepositoryManagerException {
      // Remove Elements that aren't used in any product type
      List<ProductType> ptypelist = xmlRepo.getProductTypes();
      ConcurrentHashMap<String, Boolean> usedElementIds = new ConcurrentHashMap<String, Boolean>();
      for(ProductType ptype: ptypelist) {
          List<Element> ptypeElements =
              vLayer.getElements(ptype);
          for(Element el: ptypeElements) {
              usedElementIds.put(el.getElementId(), true);
          }
      }
      for(Element el: elements) {
          if(!usedElementIds.containsKey(el.getElementId())) {
            vLayer.removeElement(el);
          }
      }
  }

	private XMLRepositoryManager getRepo(String policy) {
		XMLRepositoryManager xmlRepo = null;
		String url = "file://" + CurationService.config.getPolicyUploadPath() + "/" + policy;

		try {
			xmlRepo = new XMLRepositoryManager(Collections.singletonList(url));
		} catch (Exception e) {
			LOG.log(Level.SEVERE, e.getMessage());
		}

		return xmlRepo;
	}

	private XMLValidationLayer getValidationLayer(String policy) {
		XMLValidationLayer vLayer = null;
		String url = "file://" + CurationService.config.getPolicyUploadPath() + "/" + policy;

		try {
			vLayer = new XMLValidationLayer(Collections.singletonList(url));
		} catch (Exception e) {
			LOG.log(Level.SEVERE, e.getMessage());
		}
		return vLayer;
	}
}