com.boundlessgeo.geoserver.api.controllers.MapController.java Source code

Java tutorial

Introduction

Here is the source code for com.boundlessgeo.geoserver.api.controllers.MapController.java

Source

/* (c) 2014-2015 Boundless, http://boundlessgeo.com
 * This code is licensed under the GPL 2.0 license.
 */
package com.boundlessgeo.geoserver.api.controllers;

import static org.geoserver.catalog.Predicates.equal;

import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.annotation.Nullable;
import javax.servlet.http.HttpServletRequest;

import com.boundlessgeo.geoserver.util.RecentObjectCache;
import com.boundlessgeo.geoserver.util.RecentObjectCache.Ref;

import org.geoserver.catalog.Catalog;
import org.geoserver.catalog.LayerGroupInfo;
import org.geoserver.catalog.LayerGroupInfo.Mode;
import org.geoserver.catalog.CatalogBuilder;
import org.geoserver.catalog.LayerInfo;
import org.geoserver.catalog.Predicates;
import org.geoserver.catalog.PublishedInfo;
import org.geoserver.catalog.ResourceInfo;
import org.geoserver.catalog.StoreInfo;
import org.geoserver.catalog.StyleInfo;
import org.geoserver.catalog.WorkspaceInfo;
import org.geoserver.catalog.util.CloseableIterator;
import org.geoserver.config.GeoServer;
import org.geoserver.platform.resource.Resource;
import org.geoserver.platform.resource.Resource.Type;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.referencing.CRS;
import org.geotools.referencing.crs.DefaultGeographicCRS;
import org.geotools.util.logging.Logging;
import org.opengis.filter.Filter;
import org.opengis.filter.sort.SortBy;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.NoSuchAuthorityCodeException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

import com.boundlessgeo.geoserver.api.exceptions.BadRequestException;
import com.boundlessgeo.geoserver.api.exceptions.NotFoundException;
import com.boundlessgeo.geoserver.json.JSONArr;
import com.boundlessgeo.geoserver.json.JSONObj;
import com.google.common.base.Function;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.vividsolutions.jts.geom.Envelope;

@Controller
@RequestMapping("/api/maps")
public class MapController extends ApiController {
    static Logger LOG = Logging.getLogger(MapController.class);

    @Autowired
    LayerController layerController;

    @Autowired
    public MapController(GeoServer geoServer, RecentObjectCache recentCache) {
        super(geoServer, recentCache);
    }

    /**
     * API Endpoint to create a new map.
     * @param wsName The workspace to create the map in
     * @param obj Map description
     * @param req HTTP Request
     * @return The description of the newly created map
     * @throws NoSuchAuthorityCodeException If the map SRS is invalid
     * @throws FactoryException
     */
    @RequestMapping(value = "/{wsName:.+}", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE)
    @ResponseStatus(HttpStatus.CREATED)
    public @ResponseBody JSONObj create(@PathVariable String wsName, @RequestBody JSONObj obj,
            HttpServletRequest req) throws NoSuchAuthorityCodeException, FactoryException {
        Catalog cat = catalog();
        WorkspaceInfo ws = findWorkspace(wsName, cat);

        String name = obj.str("name");

        if (name == null) {
            throw new BadRequestException("Map object requires name");
        }

        try {
            findMap(wsName, name, cat);
            throw new BadRequestException("Map named '" + name + "' already exists");
        } catch (NotFoundException e) {
            // good!
        }

        String title = obj.str("title");
        String description = obj.str("abstract");

        Date created = new Date();

        CoordinateReferenceSystem crs = CRS.decode("EPSG:4326");

        JSONObj proj = obj.object("proj");
        if (proj != null) {
            try {
                crs = IO.crs(proj);
            } catch (Exception e) {
                throw new BadRequestException("Error parsing proj: " + proj.toString());
            }
        } else {
            throw new BadRequestException("Map object requires projection");
        }

        ReferencedEnvelope bounds = null;
        boolean updateBounds = false;

        if (obj.has("bbox")) {
            Envelope envelope = IO.bounds(obj.object("bbox"));
            bounds = new ReferencedEnvelope(envelope, crs);
        } else {
            bounds = new ReferencedEnvelope(crs);
            updateBounds = true;
        }

        if (!obj.has("layers")) {
            throw new BadRequestException("Map object requires layers array");
        }

        LayerGroupInfo map = cat.getFactory().createLayerGroup();
        map.setName(name);
        map.setAbstract(description);
        map.setTitle(title);
        map.setMode(Mode.SINGLE);
        map.setWorkspace(ws);

        for (Object o : obj.array("layers")) {
            JSONObj l = (JSONObj) o;

            LayerInfo layer = findLayer(wsName, l.str("name"), cat);
            map.getLayers().add(layer);
            map.getStyles().add(null);

            if (updateBounds) {
                try {
                    updateBounds(bounds, layer);
                } catch (Exception e) {
                    throw new RuntimeException("Error calculating map bounds ", e);
                }
            }

        }

        map.setBounds(bounds);

        Metadata.created(map, created);
        Metadata.modified(map, created);
        Metadata.modified(ws, created);

        cat.add(map);
        cat.save(ws);

        recent.add(LayerGroupInfo.class, map);
        recent.add(WorkspaceInfo.class, ws);
        return mapDetails(new JSONObj(), map, wsName, req);
    }

    void updateBounds(ReferencedEnvelope bounds, LayerInfo layer) throws Exception {
        ResourceInfo r = layer.getResource();
        if (r.boundingBox() != null
                && CRS.equalsIgnoreMetadata(bounds.getCoordinateReferenceSystem(), r.getCRS())) {
            bounds.include(r.boundingBox());
        } else {
            bounds.include(r.getLatLonBoundingBox().transform(bounds.getCoordinateReferenceSystem(), true));
        }
    }

    /**
     * API endpoint to delete a map from the catalog
     * @param wsName The workspace name
     * @param name The map name
     * @return A list of the remaining maps in the workspace
     */
    @RequestMapping(value = "/{wsName}/{name:.+}", method = RequestMethod.DELETE)
    @ResponseStatus(value = HttpStatus.NO_CONTENT)
    public void delete(@PathVariable String wsName, @PathVariable String name) {
        Catalog cat = catalog();

        WorkspaceInfo ws = findWorkspace(wsName, cat);
        LayerGroupInfo map = findMap(wsName, name, cat);
        cat.remove(map);

        recent.remove(LayerGroupInfo.class, map);
        recent.add(WorkspaceInfo.class, ws);
    }

    /**
     * API endpoint to get details on a specific map
     * @param wsName The workspace name
     * @param name The map name
     * @param req The HTTP request
     * @return The map, encoded as a JSON object
     */
    @RequestMapping(value = "/{wsName}/{name:.+}", method = RequestMethod.GET)
    public @ResponseBody JSONObj get(@PathVariable String wsName, @PathVariable String name,
            HttpServletRequest req) {
        Catalog cat = catalog();
        LayerGroupInfo map = findMap(wsName, name, cat);
        return mapDetails(new JSONObj(), map, wsName, req);
    }

    /**
     * API endpoint to update an existing map
     * @param wsName The workspace of the map
     * @param name The name of the map
     * @param obj Partial description of the map, containing all changes
     * @param req HTTP request
     * @return The description of the updated map
     * @throws Exception
     */
    @RequestMapping(value = "/{wsName}/{name:.+}", method = RequestMethod.PATCH)
    public @ResponseBody JSONObj patch(@PathVariable String wsName, @PathVariable String name,
            @RequestBody JSONObj obj, HttpServletRequest req) throws Exception {
        return put(wsName, name, obj, req);
    }

    /**
     * API endpoint to update an existing map
     * @param wsName The workspace of the map
     * @param name The name of the map
     * @param obj Partial description of the map, containing all changes
     * @param req HTTP request
     * @return The description of the updated map
     * @throws Exception
     */
    @RequestMapping(value = "/{wsName}/{name:.+}", method = RequestMethod.PUT)
    public @ResponseBody JSONObj put(@PathVariable String wsName, @PathVariable String name,
            @RequestBody JSONObj obj, HttpServletRequest req) throws Exception {
        Catalog cat = geoServer.getCatalog();

        LayerGroupInfo map = findMap(wsName, name, cat);
        WorkspaceInfo ws = map.getWorkspace();

        if (obj.has("name")) {
            map.setName(obj.str("name"));
        }
        if (obj.has("title")) {
            map.setTitle(obj.str("title"));
        }
        if (obj.has("description")) {
            map.setAbstract(obj.str("description"));
        }

        if (obj.has("proj")) {
            CoordinateReferenceSystem crs = CRS.decode("EPSG:4326");

            String srs = obj.str("proj");
            try {
                crs = CRS.decode(srs);
            } catch (FactoryException e) {
                throw new BadRequestException("Unknown spatial reference identifier: " + srs);
            }
            if (obj.has("bbox")) {
                Envelope envelope = IO.bounds(obj.object("bbox"));
                ReferencedEnvelope bounds = new ReferencedEnvelope(envelope, crs);

                map.setBounds(bounds);
            } else {
                new CatalogBuilder(cat).calculateLayerGroupBounds(map, crs);
            }
        }
        if (obj.has("layers")) {
            List<LayerInfo> layers = new ArrayList<LayerInfo>();
            for (Iterator<Object> i = obj.array("layers").iterator(); i.hasNext();) {
                JSONObj l = (JSONObj) i.next();
                String n = l.str("workspace") + ":" + l.str("name");
                LayerInfo layer = cat.getLayerByName(n);
                layers.add(layer);
            }
            map.layers().clear();
            map.layers().addAll(layers);
        }
        if (obj.has("timeout")) {
            map.getMetadata().put("timeout", (Serializable) obj.get("timeout"));
        }
        // update configuration history
        String user = SecurityContextHolder.getContext().getAuthentication().getName();
        map.getMetadata().put("user", user);

        Date modified = new Date();
        Metadata.modified(map, modified);
        Metadata.modified(ws, modified);

        if (obj.has("change")) {
            map.getMetadata().put("change", obj.str("change"));
        } else {
            map.getMetadata().put("change", "modified " + obj.keys());
        }
        cat.save(map);
        cat.save(ws);

        recent.add(LayerGroupInfo.class, map);
        recent.add(WorkspaceInfo.class, ws);

        return mapDetails(new JSONObj(), map, wsName, req);
    }

    /**
     * API Endpoint to calculate the bounds of a map, given an (optional) projection
     * @param wsName Workspace name
     * @param name Map name
     * @param obj JSON object, containing the projection to calculate the bounds for.
     * @param req Servlet Request
     * @return JSON object returning the calculated bounds
     * @throws Exception If there is an error in the request.
     */
    @RequestMapping(value = "/{wsName}/{name:.+}/bounds", method = RequestMethod.PUT)
    public @ResponseBody JSONObj bounds(@PathVariable String wsName, @PathVariable String name,
            @RequestBody JSONObj obj, HttpServletRequest req) throws Exception {

        Catalog cat = geoServer.getCatalog();

        LayerGroupInfo map = findMap(wsName, name, cat);
        WorkspaceInfo ws = map.getWorkspace();

        CoordinateReferenceSystem crs = CRS.decode("EPSG:4326");
        if (obj.has("proj")) {
            String srs = obj.str("proj");
            try {
                crs = CRS.decode(srs);
            } catch (FactoryException e) {
                throw new BadRequestException("Unknown spatial reference identifier: " + srs);
            }
        } else if (map.getBounds() != null) {
            crs = map.getBounds().getCoordinateReferenceSystem();
        }
        new CatalogBuilder(cat).calculateLayerGroupBounds(map, crs);
        JSONObj result = new JSONObj();
        IO.proj(result.putObject("proj"), crs, CRS.toSRS(crs));
        IO.bbox(result.putObject("bbox"), map);
        return result;
    }

    @RequestMapping(value = "/{wsName}/{name}/copy", method = RequestMethod.PUT)
    public @ResponseBody JSONObj copy(@PathVariable String wsName, @PathVariable String name,
            @RequestBody JSONObj obj, HttpServletRequest req)
            throws NoSuchAuthorityCodeException, FactoryException {
        Catalog cat = catalog();

        //update name
        String nameCopy = obj.str("name");
        JSONObj map = get(wsName, name, req).put("name", nameCopy);

        //Default to copying layers
        if (!obj.has("copylayers") || obj.bool("copylayers")) {
            JSONArr layers = new JSONArr();
            Map<String, Integer> renames = new HashMap<String, Integer>();

            //get mappings for renames
            if (obj.has("layers")) {
                layers = obj.array("layers");
                for (int i = 0; i < layers.size(); i++) {
                    JSONObj layer = layers.object(i);
                    renames.put(layer.object("layer").str("name"), i);
                }
            }

            JSONArr oldLayers = map.array("layers");
            JSONArr newLayers = new JSONArr();

            for (JSONObj layer : oldLayers.objects()) {
                String layerName = layer.str("name");
                if (renames.containsKey(layerName)) {
                    newLayers.add(layerController.create(wsName, layers.object(renames.get(layerName)), req));
                    renames.remove(layerName);
                } else {
                    //default to <layerName>-<map>
                    JSONObj newLayer = new JSONObj().put("name", layerName + "-" + nameCopy.substring(0, 3))
                            .put("layer", layer);
                    newLayers.add(layerController.create(wsName, newLayer, req));
                }
            }
            //Print a warning if they provided layes that do not belong to the map.
            if (renames.size() > 0) {
                for (Integer i : renames.values()) {
                    LOG.log(Level.FINE, wsName + "." + name + " unrecognized layers:"
                            + layers.object(renames.get(i)).str("name"));
                }

            }
            map.put("layers", newLayers);
        }

        return create(wsName, map, req);
    }

    /**
     * API endpoint to list maps in a workspace
     * @param wsName The workspace
     * @param page Page of the list
     * @param count Number of items per page
     * @param sort Sort order (asc or desc)
     * @param textFilter Search filter to limit results
     * @return List of items for the page, encoded as a JSON array
     */
    @RequestMapping(value = "/{wsName:.+}", method = RequestMethod.GET)
    public @ResponseBody JSONObj list(@PathVariable String wsName,
            @RequestParam(value = "page", required = false) Integer page,
            @RequestParam(value = "count", required = false, defaultValue = "" + DEFAULT_PAGESIZE) Integer count,
            @RequestParam(value = "sort", required = false) String sort,
            @RequestParam(value = "filter", required = false) String textFilter) {

        Catalog cat = geoServer.getCatalog();

        if ("default".equals(wsName)) {
            WorkspaceInfo def = cat.getDefaultWorkspace();
            if (def != null) {
                wsName = def.getName();
            }
        }

        Filter filter = equal("workspace.name", wsName);
        if (textFilter != null) {
            filter = Predicates.and(filter, Predicates.fullTextSearch(textFilter));
        }

        SortBy sortBy = parseSort(sort);

        Integer total = cat.count(LayerGroupInfo.class, filter);

        JSONObj obj = new JSONObj();

        JSONArr arr = obj.putArray("maps");
        try (CloseableIterator<LayerGroupInfo> it = cat.list(LayerGroupInfo.class, filter, offset(page, count),
                count, sortBy);) {
            while (it.hasNext()) {
                LayerGroupInfo map = it.next();
                if (checkMap(map)) {
                    map(arr.addObject(), map, wsName);
                } else {
                    //If a layer group is not shown, also remove it from the total count
                    total--;
                }
            }
        }
        obj.put("total", total);
        obj.put("page", page != null ? page : 0);
        obj.put("count", Math.min(total, count != null ? count : total));
        return obj;
    }

    private JSONArr mapLayerList(LayerGroupInfo map, HttpServletRequest req) {
        JSONArr arr = new JSONArr();
        for (PublishedInfo l : Lists.reverse(map.getLayers())) {
            layer(arr.addObject(), l, req);
        }
        return arr;
    }

    @RequestMapping(value = "/{wsName}/{name}/layers", method = RequestMethod.GET)
    public @ResponseBody JSONArr mapLayerListGet(@PathVariable String wsName, @PathVariable String name,
            HttpServletRequest req) {
        LayerGroupInfo m = findMap(wsName, name, catalog());
        return mapLayerList(m, req);
    }

    @RequestMapping(value = "/{wsName}/{name}/layers", method = RequestMethod.PUT)
    public @ResponseBody JSONArr mapLayerListPut(@PathVariable String wsName, @PathVariable String name,
            @RequestBody JSONArr layers, HttpServletRequest req) {
        Catalog cat = geoServer.getCatalog();
        LayerGroupInfo m = findMap(wsName, name, cat);

        // original
        List<MapLayer> mapLayers = MapLayer.list(m);
        Map<String, MapLayer> lookup = Maps.uniqueIndex(mapLayers, new Function<MapLayer, String>() {
            @Nullable
            public String apply(@Nullable MapLayer input) {
                return input.layer.getName();
            }
        });
        // modified
        List<PublishedInfo> reLayers = new ArrayList<PublishedInfo>();
        Map<String, PublishedInfo> check = Maps.uniqueIndex(reLayers, new Function<PublishedInfo, String>() {
            @Nullable
            public String apply(@Nullable PublishedInfo input) {
                return input.getName();
            }
        });
        List<StyleInfo> reStyles = new ArrayList<StyleInfo>();
        for (JSONObj l : Lists.reverse(Lists.newArrayList(layers.objects()))) {
            String layerName = l.str("name");
            String layerWorkspace = l.str("workspace");
            MapLayer mapLayer = lookup.get(layerName);
            if (mapLayer == null) {
                LayerInfo layer = findLayer(layerWorkspace, layerName, cat);
                if (layer != null) {
                    mapLayer = new MapLayer(layer, layer.getDefaultStyle());
                }
            }
            if (mapLayer == null) {
                throw new NotFoundException("No such layer: " + l.toString());
            }
            if (check.containsKey(layerName)) {
                throw new BadRequestException("Duplicate layer: " + l.toString());
            }
            reLayers.add(mapLayer.layer);
            reStyles.add(mapLayer.style);
        }
        m.getLayers().clear();
        m.getLayers().addAll(reLayers);
        m.getStyles().clear();
        m.getStyles().addAll(reStyles);

        WorkspaceInfo ws = m.getWorkspace();

        Date modified = new Date();
        Metadata.modified(m, modified);
        Metadata.modified(ws, modified);

        cat.save(m);
        cat.save(ws);

        recent.add(LayerGroupInfo.class, m);
        recent.add(WorkspaceInfo.class, ws);
        return mapLayerList(m, req);
    }

    @RequestMapping(value = "/{wsName}/{name}/layers", method = RequestMethod.POST)
    public @ResponseBody JSONArr mapLayerListPost(@PathVariable String wsName, @PathVariable String name,
            @RequestBody JSONArr layers, HttpServletRequest req) {
        Catalog cat = geoServer.getCatalog();
        LayerGroupInfo m = findMap(wsName, name, cat);
        WorkspaceInfo ws = m.getWorkspace();

        List<PublishedInfo> appendLayers = new ArrayList<PublishedInfo>();
        Map<String, PublishedInfo> check = Maps.uniqueIndex(appendLayers, new Function<PublishedInfo, String>() {
            @Nullable
            public String apply(@Nullable PublishedInfo input) {
                return input.getName();
            }
        });
        List<StyleInfo> appendStyles = new ArrayList<StyleInfo>();
        for (JSONObj l : Lists.reverse(Lists.newArrayList(layers.objects()))) {
            String layerName = l.str("name");
            String layerWorkspace = l.str("workspace");
            if (check.containsKey(layerName)) {
                throw new BadRequestException("Duplicate layer: " + l.toString());
            }
            LayerInfo layer = findLayer(layerWorkspace, layerName, cat);
            if (layer == null) {
                throw new NotFoundException("No such layer: " + l.toString());
            }
            appendLayers.add(layer);
            appendStyles.add(layer.getDefaultStyle());
        }
        m.getLayers().addAll(appendLayers);
        m.getStyles().addAll(appendStyles);

        Date modified = new Date();
        Metadata.modified(m, modified);
        Metadata.modified(ws, modified);

        cat.save(m);
        cat.save(ws);

        recent.add(LayerGroupInfo.class, m);
        recent.add(WorkspaceInfo.class, ws);
        return mapLayerList(m, req);
    }

    @RequestMapping(value = "/{wsName}/{mpName}/layers/{name:.+}", method = RequestMethod.GET)
    public @ResponseBody JSONObj mapLayerGet(@PathVariable String wsName, @PathVariable String mpName,
            @PathVariable String name, HttpServletRequest req) {
        LayerGroupInfo map = findMap(wsName, mpName, catalog());
        PublishedInfo layer = findMapLayer(map, name);

        JSONObj obj = layer(new JSONObj(), layer, req);
        obj.putObject("map").put("name", mpName).put("url", IO.url(req, "/maps/%s/%s", wsName, mpName));
        return obj;
    }

    @RequestMapping(value = "/recent", method = RequestMethod.GET)
    public @ResponseBody JSONArr listRecentMaps() {
        JSONArr arr = new JSONArr();
        Catalog cat = geoServer.getCatalog();

        for (Ref ref : recent.list(LayerGroupInfo.class)) {
            LayerGroupInfo map = cat.getLayerGroup(ref.id);
            if (map != null && checkMap(map)) {
                JSONObj obj = arr.addObject();
                map(obj, map, map.getWorkspace().getName());
            }
        }
        return arr;
    }

    @RequestMapping(value = "/{wsName}/{mapName}/layers/{name:.+}", method = RequestMethod.DELETE)
    public @ResponseBody JSONObj mapLayerDelete(@PathVariable String wsName, @PathVariable String mapName,
            @PathVariable String name, HttpServletRequest req) {
        Catalog cat = geoServer.getCatalog();

        LayerGroupInfo map = findMap(wsName, mapName, cat);
        WorkspaceInfo ws = map.getWorkspace();

        PublishedInfo layer = findMapLayer(map, name);
        int index = map.layers().indexOf(layer);
        boolean removed = map.getLayers().remove(layer);
        if (removed) {
            map.getStyles().remove(index);

            cat.save(map);
            recent.add(LayerGroupInfo.class, map);

            JSONObj delete = new JSONObj().put("name", layer.getName()).put("removed", removed);
            return delete;
        }
        String message = String.format("Unable to remove map layer %s/$s/%s", map.getWorkspace().getName(),
                map.getName(), name);
        throw new IllegalStateException(message);
    }

    /**
     * Confirm layer group matches composer definition of a Map.
     * @param map
     * @return true if layergroup can be handled by composer
     */
    private boolean checkMap(LayerGroupInfo map) {
        if (!(map.getMode() == Mode.SINGLE || map.getMode() == Mode.NAMED)) {
            return false;
        }
        for (int i = 0; i < map.styles().size(); i++) {
            LayerInfo style = map.layers().get(i);
            List<PublishedInfo> layer = map.getLayers();
            if (layer instanceof LayerInfo && style != ((LayerInfo) layer).getDefaultStyle()) {
                return false;
            } else if (layer instanceof LayerGroupInfo && style != ((LayerGroupInfo) layer).getRootLayerStyle()) {
                return false;
            }
        }
        return true;
    }

    /** Quick map description suitable for display in a list */
    JSONObj map(JSONObj obj, LayerGroupInfo map, String wsName) {
        obj.put("name", map.getName()).put("workspace", wsName).put("title", map.getTitle()).put("description",
                map.getAbstract());
        ReferencedEnvelope bounds = map.getBounds();
        IO.proj(obj.putObject("proj"), bounds.getCoordinateReferenceSystem(), null);
        IO.bounds(obj.putObject("bbox"), bounds);
        IO.bounds(obj.putObject("projectionExtent"), CRS.getEnvelope(bounds.getCoordinateReferenceSystem()));
        obj.put("layer_count", map.getLayers().size());

        if (map.getMetadata().containsKey("timeout")) {
            obj.put("timeout", map.getMetadata().get("timeout"));
        }
        IO.metadata(obj, map);
        if (!obj.has("modified")) {
            Resource r = dataDir().config(map);
            if (r.getType() != Type.UNDEFINED) {
                IO.date(obj.putObject("modified"), new Date(r.lastmodified()));
            }
        }

        return obj;
    }

    /** Complete map description suitable for editing. */
    JSONObj mapDetails(JSONObj obj, LayerGroupInfo map, String wsName, HttpServletRequest req) {
        map(obj, map, wsName);

        List<PublishedInfo> published = Lists.reverse(map.getLayers());
        JSONArr layers = obj.putArray("layers");
        for (PublishedInfo l : published) {
            layer(layers.addObject(), l, req);
        }
        return obj;
    }

    private JSONObj layer(JSONObj obj, PublishedInfo l, HttpServletRequest req) {
        if (l instanceof LayerInfo) {
            LayerInfo info = (LayerInfo) l;

            IO.layerDetails(obj, info, req);

            ResourceInfo r = info.getResource();
            String wsName = r.getNamespace().getPrefix();
            //            obj.put("workspace", wsName);
            //            obj.put("name", info.getName());
            obj.put("url", IO.url(req, "/layers/%s/%s", wsName, r.getName()));
            //            obj.put("title", IO.title(info));
            //            obj.put("description", IO.description(info));
            //            obj.put("type",IO.Type.of(info.getResource()).toString());
            StoreInfo store = r.getStore();
            obj.putObject("resource").put("name", r.getNativeName()).put("workspace", wsName)
                    .put("store", store.getName())
                    .put("url", IO.url(req, "/stores/%s/%s/%s", wsName, store.getName(), r.getNativeName()));

        } else if (l instanceof LayerGroupInfo) {
            LayerGroupInfo group = (LayerGroupInfo) l;

            IO.layerDetails(obj, group, req);
            //            String wsName = group.getWorkspace().getName();
            //            obj.put("workspace", wsName);
            //            obj.put("name", group.getName());
            //            obj.put("url", IO.url(req,"/layers/%s/%s",wsName,group.getName()) );
            //            obj.put("title", group.getTitle());
            //            obj.put("description", group.getAbstract());
            //            obj.put("type", "map");
            //            obj.put("group", group.getMode().name());
            //            obj.put("layer_count", group.getLayers().size());
        }
        return obj;
    }

    private PublishedInfo findMapLayer(LayerGroupInfo map, String name) {
        List<PublishedInfo> layers = Lists.reverse(map.getLayers());
        if (name.matches("\\d+")) {
            try {
                int index = Integer.parseInt(name);
                return layers.get(index);
            } catch (NumberFormatException ignore) {
            } catch (IndexOutOfBoundsException ignore) {
            }
        } else {
            for (PublishedInfo l : layers) {
                if (name.equals(l.getName())) {
                    return l;
                }
            }
        }
        String message = String.format("Unable to locate map layer %s/$s/%s", map.getWorkspace().getName(),
                map.getName(), name);
        throw new NotFoundException(message);
    }

    static class MapLayer {
        PublishedInfo layer;
        StyleInfo style;

        public MapLayer(PublishedInfo layer, StyleInfo style) {
            this.layer = layer;
            this.style = style;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + ((layer == null) ? 0 : layer.getId().hashCode());
            result = prime * result + ((style == null) ? 0 : style.getId().hashCode());
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            MapLayer other = (MapLayer) obj;
            if (layer == null) {
                if (other.layer != null)
                    return false;
            } else if (!layer.getId().equals(other.layer.getId()))
                return false;
            if (style == null) {
                if (other.style != null)
                    return false;
            } else if (!style.getId().equals(other.style.getId()))
                return false;
            return true;
        }

        static List<MapLayer> list(LayerGroupInfo map) {
            List<StyleInfo> styles = map.getStyles();
            List<PublishedInfo> layers = map.getLayers();
            List<MapLayer> l = new ArrayList<MapLayer>();
            for (int i = 0; i < map.getLayers().size(); i++) {
                l.add(new MapLayer(layers.get(i), styles.get(i)));
            }
            return l;
        }
    }
}