/*
 * 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.knox.gateway.service.admin;

import com.fasterxml.jackson.annotation.JsonProperty;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.knox.gateway.i18n.GatewaySpiMessages;
import org.apache.knox.gateway.i18n.messages.MessagesFactory;
import org.apache.knox.gateway.service.admin.beans.BeanConverter;
import org.apache.knox.gateway.service.admin.beans.Topology;
import org.apache.knox.gateway.services.ServiceType;
import org.apache.knox.gateway.services.GatewayServices;
import org.apache.knox.gateway.config.GatewayConfig;
import org.apache.knox.gateway.services.topology.TopologyService;

import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
import static javax.ws.rs.core.MediaType.APPLICATION_XML;
import static javax.ws.rs.core.MediaType.TEXT_PLAIN;

import static javax.ws.rs.core.Response.ok;
import static javax.ws.rs.core.Response.created;
import static javax.ws.rs.core.Response.notModified;
import static javax.ws.rs.core.Response.status;


@Path("/api/v1")
public class TopologiesResource {

  private static final String XML_EXT  = ".xml";
  private static final String JSON_EXT = ".json";
  private static final String YAML_EXT = ".yml";

  private static final String TOPOLOGIES_API_PATH    = "topologies";
  private static final String SINGLE_TOPOLOGY_API_PATH = TOPOLOGIES_API_PATH + "/{id}";
  private static final String PROVIDERCONFIG_API_PATH = "providerconfig";
  private static final String SINGLE_PROVIDERCONFIG_API_PATH = PROVIDERCONFIG_API_PATH + "/{name}";
  private static final String DESCRIPTORS_API_PATH    = "descriptors";
  private static final String SINGLE_DESCRIPTOR_API_PATH = DESCRIPTORS_API_PATH + "/{name}";

  private static final int     RESOURCE_NAME_LENGTH_MAX = 100;
  private static final Pattern RESOURCE_NAME_PATTERN    = Pattern.compile("^[\\w-/.]+$");

  private static GatewaySpiMessages log = MessagesFactory.get(GatewaySpiMessages.class);

  private static final Map<MediaType, String> mediaTypeFileExtensions = new HashMap<>();
  static {
    mediaTypeFileExtensions.put(MediaType.APPLICATION_XML_TYPE, XML_EXT);
    mediaTypeFileExtensions.put(MediaType.APPLICATION_JSON_TYPE, JSON_EXT);
    mediaTypeFileExtensions.put(MediaType.TEXT_PLAIN_TYPE, YAML_EXT);
  }

  @Context
  private HttpServletRequest request;

  @GET
  @Produces({APPLICATION_JSON, APPLICATION_XML})
  @Path(SINGLE_TOPOLOGY_API_PATH)
  public Topology getTopology(@PathParam("id") String id) {
    GatewayServices services =
              (GatewayServices) request.getServletContext().getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
    if (services != null) {
      TopologyService ts = services.getService(ServiceType.TOPOLOGY_SERVICE);

      GatewayConfig config =
                (GatewayConfig) request.getServletContext().getAttribute(GatewayConfig.GATEWAY_CONFIG_ATTRIBUTE);
      for (org.apache.knox.gateway.topology.Topology t : ts.getTopologies()) {
        if (t.getName().equals(id)) {
          // we need to convert first so that the original topology does not get
          // overwritten in TopologyService (i.e. URI does not change from 'file://...' to
          // 'https://...'
          final Topology convertedTopology = BeanConverter.getTopology(t);
          try {
            convertedTopology.setUri(new URI( buildURI(t, config, request) ));
          } catch (URISyntaxException se) {
            convertedTopology.setUri(null);
          }

          // For any read-only override topology, mark it as generated to discourage modification.
          final List<String> ambariManagedTopos = config.getReadOnlyOverrideTopologyNames();
          if (ambariManagedTopos.contains(convertedTopology.getName())) {
            convertedTopology.setGenerated(true);
          }

          return convertedTopology;
        }
      }
    }
    return null;
  }

  @GET
  @Produces({APPLICATION_JSON, APPLICATION_XML})
  @Path(TOPOLOGIES_API_PATH)
  public SimpleTopologyWrapper getTopologies() {
    GatewayServices services =
              (GatewayServices) request.getServletContext().getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
    GatewayConfig config =
        (GatewayConfig) request.getServletContext().getAttribute(GatewayConfig.GATEWAY_CONFIG_ATTRIBUTE);

    TopologyService ts = services.getService(ServiceType.TOPOLOGY_SERVICE);

    ArrayList<SimpleTopology> st = new ArrayList<>();
    for (org.apache.knox.gateway.topology.Topology t : ts.getTopologies()) {
      st.add(getSimpleTopology(t, config));
    }
    st.sort(new TopologyComparator());

    SimpleTopologyWrapper stw = new SimpleTopologyWrapper();
    stw.topologies.addAll(st);

    return stw;

  }

  @PUT
  @Consumes({APPLICATION_JSON, APPLICATION_XML})
  @Path(SINGLE_TOPOLOGY_API_PATH)
  public Topology uploadTopology(@PathParam("id") String id, Topology t) {
    Topology result = null;

    try {
      id = URLDecoder.decode(id, StandardCharsets.UTF_8.name());
    } catch (Exception e) {
      // Ignore
    }

    if (!isValidResourceName(id)) {
      log.invalidResourceName(id);
      throw new BadRequestException("Invalid topology name: " + id);
    }

    GatewayServices gs =
                (GatewayServices) request.getServletContext().getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);

    t.setName(id);
    TopologyService ts = gs.getService(ServiceType.TOPOLOGY_SERVICE);

    // Check for existing topology with the same name, to see if it had been generated
    boolean existingGenerated = false;
    for (org.apache.knox.gateway.topology.Topology existingTopology : ts.getTopologies()) {
      if(existingTopology.getName().equals(id)) {
        existingGenerated = existingTopology.isGenerated();
        break;
      }
    }

    // If a topology with the same ID exists, which had been generated, then DO NOT overwrite it because it will be
    // out of sync with the source descriptor. Otherwise, deploy the updated version.
    if (!existingGenerated) {
      ts.deployTopology(BeanConverter.getTopology(t));
      result = getTopology(id);
    } else {
      log.disallowedOverwritingGeneratedTopology(id);
    }

    return result;
  }

  @DELETE
  @Produces(APPLICATION_JSON)
  @Path(SINGLE_TOPOLOGY_API_PATH)
  public Response deleteTopology(@PathParam("id") String id) {
    boolean deleted = false;
    if(!"admin".equals(id)) {
      GatewayServices services = (GatewayServices) request.getServletContext()
          .getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);

      TopologyService ts = services.getService(ServiceType.TOPOLOGY_SERVICE);

      for (org.apache.knox.gateway.topology.Topology t : ts.getTopologies()) {
        if(t.getName().equals(id)) {
          ts.deleteTopology(t);
          deleted = true;
        }
      }
    }else{
      deleted = false;
    }
    return ok().entity("{ \"deleted\" : " + deleted + " }").build();
  }

  @GET
  @Produces({APPLICATION_JSON})
  @Path(PROVIDERCONFIG_API_PATH)
  public HrefListing getProviderConfigurations() {
    HrefListing listing = new HrefListing();
    listing.setHref(buildHref(request));

    GatewayServices services =
            (GatewayServices) request.getServletContext().getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);

    List<HrefListItem> configs = new ArrayList<>();
    TopologyService ts = services.getService(ServiceType.TOPOLOGY_SERVICE);
    // Get all the simple descriptor file names
    for (File providerConfig : ts.getProviderConfigurations()){
      String id = FilenameUtils.getBaseName(providerConfig.getName());
      configs.add(new HrefListItem(buildHref(id, request), providerConfig.getName()));
    }

    listing.setItems(configs);
    return listing;
  }

  @GET
  @Produces({APPLICATION_XML, APPLICATION_JSON, TEXT_PLAIN})
  @Path(SINGLE_PROVIDERCONFIG_API_PATH)
  public Response getProviderConfiguration(@PathParam("name") String name) {
    Response response;

    GatewayServices services =
            (GatewayServices) request.getServletContext().getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);

    TopologyService ts = services.getService(ServiceType.TOPOLOGY_SERVICE);

    File providerConfigFile = null;

    for (File pc : ts.getProviderConfigurations()){
      // If the file name matches the specified id
      if (FilenameUtils.getBaseName(pc.getName()).equals(name)) {
        providerConfigFile = pc;
        break;
      }
    }

    if (providerConfigFile != null) {
      byte[] content;
      try {
        content = FileUtils.readFileToByteArray(providerConfigFile);
        response = ok().entity(content).build();
      } catch (IOException e) {
        log.failedToReadConfigurationFile(providerConfigFile.getAbsolutePath(), e);
        response = Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
      }

    } else {
      response = Response.status(Response.Status.NOT_FOUND).build();
    }
    return response;
  }

  @DELETE
  @Produces(APPLICATION_JSON)
  @Path(SINGLE_PROVIDERCONFIG_API_PATH)
  public Response deleteProviderConfiguration(@PathParam("name") String name, @QueryParam("force") String force) {
    Response response;
    GatewayServices services =
            (GatewayServices) request.getServletContext().getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);

    TopologyService ts = services.getService(ServiceType.TOPOLOGY_SERVICE);
    if (ts.deleteProviderConfiguration(name, Boolean.valueOf(force))) {
      response = ok().entity("{ \"deleted\" : \"provider config " + name + "\" }").build();
    } else {
      response = notModified().build();
    }
    return response;
  }


  @DELETE
  @Produces(APPLICATION_JSON)
  @Path(SINGLE_DESCRIPTOR_API_PATH)
  public Response deleteSimpleDescriptor(@PathParam("name") String name) {
    Response response = null;
    if(!"admin".equals(name)) {
      GatewayServices services =
              (GatewayServices) request.getServletContext().getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);

      TopologyService ts = services.getService(ServiceType.TOPOLOGY_SERVICE);
      if (ts.deleteDescriptor(name)) {
        response = ok().entity("{ \"deleted\" : \"descriptor " + name + "\" }").build();
      }
    }

    if (response == null) {
      response = notModified().build();
    }

    return response;
  }


  @PUT
  @Consumes({APPLICATION_XML, APPLICATION_JSON, TEXT_PLAIN})
  @Path(SINGLE_PROVIDERCONFIG_API_PATH)
  public Response uploadProviderConfiguration(@PathParam("name") String name, @Context HttpHeaders headers, String content) {
    Response response = null;

    try {
      name = URLDecoder.decode(name, StandardCharsets.UTF_8.name());
    } catch (Exception e) {
      // Ignore
    }

    if (!isValidResourceName(name)) {
      log.invalidResourceName(name);
      throw new BadRequestException("Invalid provider configuration name: " + name);
    }

    GatewayServices gs =
            (GatewayServices) request.getServletContext().getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);

    TopologyService ts = gs.getService(ServiceType.TOPOLOGY_SERVICE);

    File existing = getExistingConfigFile(ts.getProviderConfigurations(), name);
    boolean isUpdate = (existing != null);

    // If it's an update, then use the matching existing filename; otherwise, use the media type to determine the file
    // extension.
    String filename = isUpdate ? existing.getName() : getFileNameForResource(name, headers);

    if (ts.deployProviderConfiguration(filename, content)) {
      try {
        if (isUpdate) {
          response = Response.noContent().build();
        } else {
          response = created(new URI(buildHref(request))).build();
        }
      } catch (URISyntaxException e) {
        log.invalidResourceURI(e.getInput(), e.getReason(), e);
        response = status(Response.Status.BAD_REQUEST).entity("{ \"error\" : \"Failed to deploy provider configuration " + name + "\" }").build();
      }
    }

    return response;
  }

  @PUT
  @Consumes({APPLICATION_JSON, TEXT_PLAIN})
  @Path(SINGLE_DESCRIPTOR_API_PATH)
  public Response uploadSimpleDescriptor(@PathParam("name") String name,
                                         @Context HttpHeaders headers,
                                         String content) {
    Response response = null;

    try {
      name = URLDecoder.decode(name, StandardCharsets.UTF_8.name());
    } catch (Exception e) {
      // Ignore
    }

    if (!isValidResourceName(name)) {
      log.invalidResourceName(name);
      throw new BadRequestException("Invalid descriptor name: " + name);
    }

    GatewayServices gs =
            (GatewayServices) request.getServletContext().getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);

    TopologyService ts = gs.getService(ServiceType.TOPOLOGY_SERVICE);

    File existing = getExistingConfigFile(ts.getDescriptors(), name);
    boolean isUpdate = (existing != null);

    // If it's an update, then use the matching existing filename; otherwise, use the media type to determine the file
    // extension.
    String filename = isUpdate ? existing.getName() : getFileNameForResource(name, headers);

    if (ts.deployDescriptor(filename, content)) {
      try {
        if (isUpdate) {
          response = Response.noContent().build();
        } else {
          response = created(new URI(buildHref(request))).build();
        }
      } catch (URISyntaxException e) {
        log.invalidResourceURI(e.getInput(), e.getReason(), e);
        response = status(Response.Status.BAD_REQUEST).entity("{ \"error\" : \"Failed to deploy descriptor " + name + "\" }").build();
      }
    }

    return response;
  }


  @GET
  @Produces({APPLICATION_JSON})
  @Path(DESCRIPTORS_API_PATH)
  public HrefListing getSimpleDescriptors() {
    HrefListing listing = new HrefListing();
    listing.setHref(buildHref(request));

    GatewayServices services =
            (GatewayServices) request.getServletContext().getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);

    List<HrefListItem> descriptors = new ArrayList<>();
    TopologyService ts = services.getService(ServiceType.TOPOLOGY_SERVICE);
    for (File descriptor : ts.getDescriptors()){
      String id = FilenameUtils.getBaseName(descriptor.getName());
      descriptors.add(new HrefListItem(buildHref(id, request), descriptor.getName()));
    }

    listing.setItems(descriptors);
    return listing;
  }


  @GET
  @Produces({APPLICATION_JSON, TEXT_PLAIN})
  @Path(SINGLE_DESCRIPTOR_API_PATH)
  public Response getSimpleDescriptor(@PathParam("name") String name) {
    Response response;

    GatewayServices services =
            (GatewayServices) request.getServletContext().getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);

    TopologyService ts = services.getService(ServiceType.TOPOLOGY_SERVICE);

    File descriptorFile = null;

    for (File sd : ts.getDescriptors()){
      // If the file name matches the specified id
      if (FilenameUtils.getBaseName(sd.getName()).equals(name)) {
        descriptorFile = sd;
        break;
      }
    }

    if (descriptorFile != null) {
      String mediaType = APPLICATION_JSON;

      byte[] content;
      try {
        if ("yml".equals(FilenameUtils.getExtension(descriptorFile.getName()))) {
          mediaType = TEXT_PLAIN;
        }
        content = FileUtils.readFileToByteArray(descriptorFile);
        response = ok().type(mediaType).entity(content).build();
      } catch (IOException e) {
        log.failedToReadConfigurationFile(descriptorFile.getAbsolutePath(), e);
        response = Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
      }
    } else {
      response = Response.status(Response.Status.NOT_FOUND).build();
    }

    return response;
  }


  private String getFileNameForResource(String resourceName, HttpHeaders headers) {
    String filename;
    String extension = FilenameUtils.getExtension(resourceName);
    if (extension != null && !extension.isEmpty()) {
      filename = resourceName;
    } else {
      extension = getExtensionForMediaType(headers.getMediaType());
      filename = (extension != null) ? (resourceName + extension) : (resourceName + JSON_EXT);
    }
    return filename;
  }

  private String getExtensionForMediaType(MediaType type) {
    String extension = null;

    for (Map.Entry<MediaType, String> entry : mediaTypeFileExtensions.entrySet()) {
      if (type.isCompatible(entry.getKey())) {
        extension = entry.getValue();
        break;
      }
    }

    return extension;
  }

  private File getExistingConfigFile(Collection<File> existing, String candidateName) {
    File result = null;
    for (File exists : existing) {
      if (FilenameUtils.getBaseName(exists.getName()).equals(candidateName)) {
        result = exists;
        break;
      }
    }
    return result;
  }

  private static boolean isValidResourceName(final String name) {
    return name != null && name.length() <= RESOURCE_NAME_LENGTH_MAX &&
        RESOURCE_NAME_PATTERN.matcher(name).matches();
  }


  private static class TopologyComparator implements Comparator<SimpleTopology> {
    @Override
    public int compare(SimpleTopology t1, SimpleTopology t2) {
      return t1.getName().compareTo(t2.getName());
    }
  }


  String buildURI(org.apache.knox.gateway.topology.Topology topology, GatewayConfig config, HttpServletRequest req){
    String uri = buildXForwardBaseURL(req);

    // Strip extra context
    uri = uri.replace(req.getContextPath(), "");

    // Add the gateway path
    String gatewayPath;
    if(config.getGatewayPath() != null){
      gatewayPath = config.getGatewayPath();
    }else{
      gatewayPath = "gateway";
    }
    return uri + "/" + gatewayPath + "/" + topology.getName();
  }

  String buildHref(HttpServletRequest req) {
    return buildHref((String)null, req);
  }

  String buildHref(String id, HttpServletRequest req) {
    StringBuilder href = new StringBuilder(buildXForwardBaseURL(req));
    // Make sure that the pathInfo doesn't have any '/' chars at the end.
    String pathInfo = req.getPathInfo();
    while(pathInfo.endsWith("/")) {
      pathInfo = pathInfo.substring(0, pathInfo.length() - 1);
    }

    href.append(pathInfo);

    if (id != null) {
      href.append('/').append(id);
    }

    return href.toString();
  }

   String buildHref(org.apache.knox.gateway.topology.Topology t, HttpServletRequest req) {
     return buildHref(t.getName(), req);
  }

  private SimpleTopology getSimpleTopology(org.apache.knox.gateway.topology.Topology t, GatewayConfig config) {
    String uri = buildURI(t, config, request);
    String href = buildHref(t, request);
    return new SimpleTopology(t, uri, href);
  }

  private String buildXForwardBaseURL(HttpServletRequest req){
    final String X_Forwarded = "X-Forwarded-";
    final String X_Forwarded_Context = X_Forwarded + "Context";
    final String X_Forwarded_Proto = X_Forwarded + "Proto";
    final String X_Forwarded_Host = X_Forwarded + "Host";
    final String X_Forwarded_Port = X_Forwarded + "Port";
    final String X_Forwarded_Server = X_Forwarded + "Server";

    StringBuilder baseURL = new StringBuilder();

    // Get Protocol
    if(req.getHeader(X_Forwarded_Proto) != null){
      baseURL.append(req.getHeader(X_Forwarded_Proto)).append("://");
    } else {
      baseURL.append(req.getProtocol()).append("://");
    }

    // Handle Server/Host and Port Here
    if (req.getHeader(X_Forwarded_Host) != null && req.getHeader(X_Forwarded_Port) != null){
      // Double check to see if host has port
      if(req.getHeader(X_Forwarded_Host).contains(req.getHeader(X_Forwarded_Port))){
        baseURL.append(req.getHeader(X_Forwarded_Host));
      } else {
        // If there's no port, add the host and port together;
        baseURL.append(req.getHeader(X_Forwarded_Host)).append(':').append(req.getHeader(X_Forwarded_Port));
      }
    } else if(req.getHeader(X_Forwarded_Server) != null && req.getHeader(X_Forwarded_Port) != null){
      // Tack on the server and port if they're available. Try host if server not available
      baseURL.append(req.getHeader(X_Forwarded_Server)).append(':').append(req.getHeader(X_Forwarded_Port));
    } else if(req.getHeader(X_Forwarded_Port) != null) {
      // if we at least have a port, we can use it.
      baseURL.append(req.getServerName()).append(':').append(req.getHeader(X_Forwarded_Port));
    } else {
      // Resort to request members
      baseURL.append(req.getServerName()).append(':').append(req.getLocalPort());
    }

    // Handle Server context
    if( req.getHeader(X_Forwarded_Context) != null ) {
      baseURL.append(req.getHeader( X_Forwarded_Context ));
    } else {
      baseURL.append(req.getContextPath());
    }

    return baseURL.toString();
  }


  static class HrefListing {
    @JsonProperty
    String href;

    @JsonProperty
    List<HrefListItem> items;

    HrefListing() {}

    public void setHref(String href) {
      this.href = href;
    }

    public String getHref() {
      return href;
    }

    public void setItems(List<HrefListItem> items) {
      this.items = items;
    }

    public List<HrefListItem> getItems() {
      return items;
    }
  }

  static class HrefListItem {
    @JsonProperty
    String href;

    @JsonProperty
    String name;

    HrefListItem() {}

    HrefListItem(String href, String name) {
      this.href = href;
      this.name = name;
    }

    public void setHref(String href) {
      this.href = href;
    }

    public String getHref() {
      return href;
    }

    public void setName(String name) {
      this.name = name;
    }
    public String getName() {
      return name;
    }
  }


  @XmlAccessorType(XmlAccessType.NONE)
  public static class SimpleTopology {

    @XmlElement
    private String name;
    @XmlElement
    private String timestamp;
    @XmlElement
    private String defaultServicePath;
    @XmlElement
    private String uri;
    @XmlElement
    private String href;

    public SimpleTopology() {}

    public SimpleTopology(org.apache.knox.gateway.topology.Topology t, String uri, String href) {
      this.name = t.getName();
      this.timestamp = Long.toString(t.getTimestamp());
      this.defaultServicePath = t.getDefaultServicePath();
      this.uri = uri;
      this.href = href;
    }

    public String getName() {
      return name;
    }

    public void setName(String n) {
      name = n;
    }

    public String getTimestamp() {
      return timestamp;
    }

    public void setDefaultService(String defaultServicePath) {
      this.defaultServicePath = defaultServicePath;
    }

    public String getDefaultService() {
      return defaultServicePath;
    }

    public void setTimestamp(String timestamp) {
      this.timestamp = timestamp;
    }

    public String getUri() {
      return uri;
    }

    public void setUri(String uri) {
      this.uri = uri;
    }

    public String getHref() {
      return href;
    }

    public void setHref(String href) {
      this.href = href;
    }
  }

  @XmlAccessorType(XmlAccessType.FIELD)
  public static class SimpleTopologyWrapper{

    @XmlElement(name="topology")
    @XmlElementWrapper(name="topologies")
    private List<SimpleTopology> topologies = new ArrayList<>();

    public List<SimpleTopology> getTopologies(){
      return topologies;
    }

    public void setTopologies(List<SimpleTopology> ts){
      this.topologies = ts;
    }

  }
}

