Skip to content

Commit

Permalink
OSEO product creation via ZIP file
Browse files Browse the repository at this point in the history
  • Loading branch information
aaime committed Jun 28, 2017
1 parent 943d4f7 commit 49dbc2e
Show file tree
Hide file tree
Showing 7 changed files with 366 additions and 89 deletions.
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -57,8 +57,7 @@ public class OSEOTestSupport extends GeoServerSystemTestSupport {
try { try {
OS_SCHEMA = factory OS_SCHEMA = factory
.newSchema(OSEOTestSupport.class.getResource("/schemas/OpenSearch.xsd")); .newSchema(OSEOTestSupport.class.getResource("/schemas/OpenSearch.xsd"));
ATOM_SCHEMA = factory ATOM_SCHEMA = null; // factory .newSchema(OSEOTestSupport.class.getResource("/schemas/searchResults.xsd"));
.newSchema(OSEOTestSupport.class.getResource("/schemas/searchResults.xsd"));
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException("Could not parse the OpenSearch schemas", e); throw new RuntimeException("Could not parse the OpenSearch schemas", e);
} }
Expand Down
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -414,7 +414,9 @@ paths:
* product.json: the list of searchable attributes and eventual OGC links * product.json: the list of searchable attributes and eventual OGC links
* description.html: the HTML description for the product * description.html: the HTML description for the product
* metadata.xml: the O&M metadata for the collection * metadata.xml: the O&M metadata for the collection
* thumbnail.png: the product thumbnail (ignored at the time of writing) * thumbnail.png: the product thumbnail (can also have jpeg or jpg extension)
* owsLinks.json: the list of OWS links, in the same JSON format as the associated resource
* granules.json: the list of granules, in the same JSON format as the associated resource
The JSON format is the same as the one returned by a GET on an existing product, the "*Href" properties should be omitted The JSON format is the same as the one returned by a GET on an existing product, the "*Href" properties should be omitted
consumes: consumes:
- application/json - application/json
Expand Down
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -4,24 +4,35 @@
*/ */
package org.geoserver.opensearch.rest; package org.geoserver.opensearch.rest;


import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader; import java.io.StringReader;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipInputStream;


import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.ParserConfigurationException;


import org.apache.commons.io.IOUtils;
import org.geoserver.opensearch.eo.ListComplexFeatureCollection; import org.geoserver.opensearch.eo.ListComplexFeatureCollection;
import org.geoserver.opensearch.eo.OpenSearchAccessProvider; import org.geoserver.opensearch.eo.OpenSearchAccessProvider;
import org.geoserver.opensearch.eo.response.LinkFeatureComparator; import org.geoserver.opensearch.eo.response.LinkFeatureComparator;
import org.geoserver.opensearch.eo.store.OpenSearchAccess; import org.geoserver.opensearch.eo.store.OpenSearchAccess;
import org.geoserver.opensearch.eo.store.OpenSearchAccess.ProductClass; import org.geoserver.opensearch.eo.store.OpenSearchAccess.ProductClass;
import org.geoserver.opensearch.rest.CollectionsController.CollectionPart;
import org.geoserver.opensearch.rest.CollectionsController.IOConsumer; import org.geoserver.opensearch.rest.CollectionsController.IOConsumer;
import org.geoserver.rest.ResourceNotFoundException; import org.geoserver.rest.ResourceNotFoundException;
import org.geoserver.rest.RestBaseController; import org.geoserver.rest.RestBaseController;
Expand All @@ -44,6 +55,8 @@
import org.geotools.feature.collection.BaseSimpleFeatureCollection; import org.geotools.feature.collection.BaseSimpleFeatureCollection;
import org.geotools.feature.simple.SimpleFeatureBuilder; import org.geotools.feature.simple.SimpleFeatureBuilder;
import org.geotools.feature.simple.SimpleFeatureTypeBuilder; import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
import org.geotools.geojson.feature.FeatureJSON;
import org.geotools.util.logging.Logging;
import org.opengis.feature.Attribute; import org.opengis.feature.Attribute;
import org.opengis.feature.Feature; import org.opengis.feature.Feature;
import org.opengis.feature.FeatureFactory; import org.opengis.feature.FeatureFactory;
Expand All @@ -55,6 +68,8 @@
import org.opengis.feature.type.Name; import org.opengis.feature.type.Name;
import org.opengis.feature.type.PropertyDescriptor; import org.opengis.feature.type.PropertyDescriptor;
import org.opengis.filter.FilterFactory2; import org.opengis.filter.FilterFactory2;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.w3c.dom.Document; import org.w3c.dom.Document;
import org.xml.sax.InputSource; import org.xml.sax.InputSource;
Expand All @@ -66,6 +81,18 @@
* @author Andrea Aime - GeoSolutions * @author Andrea Aime - GeoSolutions
*/ */
public abstract class AbstractOpenSearchController extends RestBaseController { public abstract class AbstractOpenSearchController extends RestBaseController {

interface ZipPart {

/**
* Returns true if the part matches the provided name
* @param name
* @return
*/
public boolean matches(String name);
}

static final Logger LOGGER = Logging.getLogger(CollectionsController.class);


static final String SOURCE_NAME = "SourceName"; static final String SOURCE_NAME = "SourceName";


Expand All @@ -75,8 +102,11 @@ public abstract class AbstractOpenSearchController extends RestBaseController {


protected OpenSearchAccessProvider accessProvider; protected OpenSearchAccessProvider accessProvider;


public AbstractOpenSearchController(OpenSearchAccessProvider accessProvider) { protected OseoJSONConverter jsonConverter;

public AbstractOpenSearchController(OpenSearchAccessProvider accessProvider, OseoJSONConverter jsonConverter) {
this.accessProvider = accessProvider; this.accessProvider = accessProvider;
this.jsonConverter = jsonConverter;
} }


protected OpenSearchAccess getOpenSearchAccess() throws IOException { protected OpenSearchAccess getOpenSearchAccess() throws IOException {
Expand Down Expand Up @@ -417,5 +447,76 @@ protected ListFeatureCollection beansToLinksCollection(OgcLinks links) throws IO
} }
return linksCollection; return linksCollection;
} }

protected <T extends ZipPart> Map<T, byte[]> parsePartsFromZip(InputStream body, T[] parts)
throws IOException {
// check the zip contents and map to the expected parts
Map<T, byte[]> result = new HashMap<>();
try {
ZipInputStream zis = new ZipInputStream(body);
ZipEntry entry = null;

while ((entry = zis.getNextEntry()) != null) {
String name = entry.getName();
T part = null;
for (T zp : parts) {
if (zp.matches(name)) {
part = zp;
break;
}
}
if (part != null) {
result.put(part, IOUtils.toByteArray(zis));
} else {
LOGGER.warning("Ignoring un-recognized entry in zip file:" + name);
}
}
} catch (ZipException e) {
throw new RestException(e.getMessage(), HttpStatus.BAD_REQUEST);
}
return result;
}

@SuppressWarnings("unchecked")
protected <T> T parseJSON(Class<T> clazz, byte[] rawData) throws IOException {
T links = (T) jsonConverter.read(clazz, new HttpInputMessage() {

@Override
public HttpHeaders getHeaders() {
return new HttpHeaders();
}

@Override
public InputStream getBody() throws IOException {
return new ByteArrayInputStream(rawData);
}
});
return links;
}

protected SimpleFeature parseGeoJSONFeature(String fileReference, final byte[] payload) {
try {
SimpleFeature jsonFeature = new FeatureJSON().readFeature(new ByteArrayInputStream(payload));
return jsonFeature;
} catch (IOException e) {
throw new RestException(
fileReference + " contains invalid GeoJSON: " + e.getMessage(),
HttpStatus.BAD_REQUEST, e);
}
}

protected SimpleFeatureCollection parseGeoJSONFeatureCollection(String fileReference, final byte[] payload) {
try {
SimpleFeatureCollection fc = (SimpleFeatureCollection) new FeatureJSON()
.readFeatureCollection(new ByteArrayInputStream(payload));
return fc;
} catch (IOException e) {
throw new RestException(
fileReference + " contains invalid GeoJSON: " + e.getMessage(),
HttpStatus.BAD_REQUEST, e);
}
}




} }
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -80,12 +80,10 @@
@RequestMapping(path = RestBaseController.ROOT_PATH + "/oseo/collections") @RequestMapping(path = RestBaseController.ROOT_PATH + "/oseo/collections")
public class CollectionsController extends AbstractOpenSearchController { public class CollectionsController extends AbstractOpenSearchController {


static final Logger LOGGER = Logging.getLogger(CollectionsController.class);

/** /**
* Parses components of * List of parts making up a zipfile for a collection
*/ */
enum CollectionPart { enum CollectionPart implements ZipPart {
Collection("collection.json"), Description("description.html"), Metadata( Collection("collection.json"), Description("description.html"), Metadata(
"metadata.xml"), Thumbnail("thumbnail\\.[png|jpeg|jpg]"), OwsLinks("owsLinks.json"); "metadata.xml"), Thumbnail("thumbnail\\.[png|jpeg|jpg]"), OwsLinks("owsLinks.json");


Expand All @@ -95,6 +93,7 @@ enum CollectionPart {
this.pattern = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE); this.pattern = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE);
} }


@Override
public boolean matches(String name) { public boolean matches(String name) {
return pattern.matcher(name).matches(); return pattern.matcher(name).matches();
} }
Expand All @@ -110,13 +109,11 @@ public interface IOConsumer<T> {


static final Name COLLECTION_ID = new NameImpl(OpenSearchAccess.EO_NAMESPACE, "identifier"); static final Name COLLECTION_ID = new NameImpl(OpenSearchAccess.EO_NAMESPACE, "identifier");


OseoJSONConverter jsonConverter;

public CollectionsController(OpenSearchAccessProvider accessProvider, OseoJSONConverter jsonConverter) { public CollectionsController(OpenSearchAccessProvider accessProvider, OseoJSONConverter jsonConverter) {
super(accessProvider); super(accessProvider, jsonConverter);
this.jsonConverter = jsonConverter;
} }



@GetMapping(produces = { MediaType.APPLICATION_JSON_VALUE }) @GetMapping(produces = { MediaType.APPLICATION_JSON_VALUE })
@ResponseBody @ResponseBody
public CollectionReferences getCollections(HttpServletRequest request, public CollectionReferences getCollections(HttpServletRequest request,
Expand Down Expand Up @@ -159,6 +156,12 @@ public ResponseEntity<String> postCollectionJson(HttpServletRequest request,
runTransactionOnCollectionStore(fs -> fs.addFeatures(singleton(collectionFeature))); runTransactionOnCollectionStore(fs -> fs.addFeatures(singleton(collectionFeature)));


// if got here, all is fine // if got here, all is fine
return returnCreatedCollectionReference(request, eoId);
}


private ResponseEntity<String> returnCreatedCollectionReference(HttpServletRequest request,
String eoId) throws URISyntaxException {
String baseURL = ResponseUtils.baseURL(request); String baseURL = ResponseUtils.baseURL(request);
String newCollectionLocation = ResponseUtils.buildURL(baseURL, String newCollectionLocation = ResponseUtils.buildURL(baseURL,
"/rest/oseo/collections/" + eoId, null, URLType.RESOURCE); "/rest/oseo/collections/" + eoId, null, URLType.RESOURCE);
Expand All @@ -171,23 +174,15 @@ public ResponseEntity<String> postCollectionJson(HttpServletRequest request,
public ResponseEntity<String> postCollectionZip(HttpServletRequest request, InputStream body) public ResponseEntity<String> postCollectionZip(HttpServletRequest request, InputStream body)
throws IOException, URISyntaxException { throws IOException, URISyntaxException {


Map<CollectionPart, byte[]> parts = parseCollectionPartsFromZip(body); Map<CollectionPart, byte[]> parts = parsePartsFromZip(body, CollectionPart.values());


// process the collection part // process the collection part
final byte[] collectionPayload = parts.get(CollectionPart.Collection); final byte[] collectionPayload = parts.get(CollectionPart.Collection);
if (collectionPayload == null) { if (collectionPayload == null) {
throw new RestException("collection.json file is missing from the zip", throw new RestException("collection.json file is missing from the zip",
HttpStatus.BAD_REQUEST); HttpStatus.BAD_REQUEST);
} }
SimpleFeature jsonFeature; SimpleFeature jsonFeature = parseGeoJSONFeature("collection.json", collectionPayload);
try {
jsonFeature = new FeatureJSON()
.readFeature(new ByteArrayInputStream(collectionPayload));
} catch (IOException e) {
throw new RestException(
"collection.json file contains invalid GeoJSON: " + e.getMessage(),
HttpStatus.BAD_REQUEST, e);
}
String eoId = checkCollectionIdentifier(jsonFeature); String eoId = checkCollectionIdentifier(jsonFeature);
Feature collectionFeature = simpleToComplex(jsonFeature, getCollectionSchema(), Feature collectionFeature = simpleToComplex(jsonFeature, getCollectionSchema(),
COLLECTION_HREFS); COLLECTION_HREFS);
Expand All @@ -198,18 +193,7 @@ public ResponseEntity<String> postCollectionZip(HttpServletRequest request, Inpu
byte[] rawLinks = parts.get(CollectionPart.OwsLinks); byte[] rawLinks = parts.get(CollectionPart.OwsLinks);
SimpleFeatureCollection linksCollection; SimpleFeatureCollection linksCollection;
if(rawLinks != null) { if(rawLinks != null) {
OgcLinks links = (OgcLinks) jsonConverter.read(OgcLinks.class, new HttpInputMessage() { OgcLinks links = parseJSON(OgcLinks.class, rawLinks);

@Override
public HttpHeaders getHeaders() {
return new HttpHeaders();
}

@Override
public InputStream getBody() throws IOException {
return new ByteArrayInputStream(rawLinks);
}
});
linksCollection = beansToLinksCollection(links); linksCollection = beansToLinksCollection(links);
} else { } else {
linksCollection = null; linksCollection = null;
Expand Down Expand Up @@ -240,42 +224,7 @@ public InputStream getBody() throws IOException {


}); });


// if got here, all is fine return returnCreatedCollectionReference(request, eoId);
String baseURL = ResponseUtils.baseURL(request);
String newCollectionLocation = ResponseUtils.buildURL(baseURL,
"/rest/oseo/collections/" + eoId, null, URLType.RESOURCE);
HttpHeaders headers = new HttpHeaders();
headers.setLocation(new URI(newCollectionLocation));
return new ResponseEntity<>(eoId, headers, HttpStatus.CREATED);
}

private Map<CollectionPart, byte[]> parseCollectionPartsFromZip(InputStream body)
throws IOException {
// check the zip contents and map to the expected parts
Map<CollectionPart, byte[]> parts = new HashMap<>();
try {
ZipInputStream zis = new ZipInputStream(body);
ZipEntry entry = null;

while ((entry = zis.getNextEntry()) != null) {
String name = entry.getName();
CollectionPart part = null;
for (CollectionPart zp : CollectionPart.values()) {
if (zp.matches(name)) {
part = zp;
break;
}
}
if (part != null) {
parts.put(part, IOUtils.toByteArray(zis));
} else {
LOGGER.warning("Ignoring un-recognized entry in zip file:" + name);
}
}
} catch (ZipException e) {
throw new RestException(e.getMessage(), HttpStatus.BAD_REQUEST);
}
return parts;
} }


@GetMapping(path = "{collection}", produces = { MediaType.APPLICATION_JSON_VALUE }) @GetMapping(path = "{collection}", produces = { MediaType.APPLICATION_JSON_VALUE })
Expand Down

0 comments on commit 49dbc2e

Please sign in to comment.