/* * (c) 2017 Open Source Geospatial Foundation - all rights reserved */ package org.geoserver.ows; import com.google.common.base.CaseFormat; import com.google.common.base.Converter; import com.vividsolutions.jts.geom.Coordinate; import net.sf.json.util.JSONStringer; import org.apache.commons.lang.ClassUtils; import org.eclipse.emf.ecore.EObject; import org.geoserver.catalog.CoverageInfo; import org.geoserver.catalog.FeatureTypeInfo; import org.geoserver.catalog.Info; import org.geoserver.ows.kvp.FormatOptionsKvpParser; import org.geoserver.ows.util.ClassProperties; import org.geoserver.ows.util.OwsUtils; import org.geoserver.platform.Operation; import org.geoserver.platform.Service; import org.geoserver.platform.ServiceException; import org.geoserver.wms.GetFeatureInfoRequest; import org.geoserver.wms.GetMapRequest; import org.geoserver.wms.WMS; import org.geotools.data.FeatureSource; import org.geotools.filter.text.cql2.CQL; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.referencing.CRS; import org.geotools.util.Converters; import org.geotools.util.logging.Logging; import org.opengis.coverage.grid.GridCoverageReader; import org.opengis.filter.Filter; import org.opengis.geometry.Envelope; import org.opengis.referencing.operation.MathTransform; import java.lang.reflect.Modifier; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Map; import java.util.Optional; import java.util.function.Supplier; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Stream; import static org.geotools.referencing.crs.DefaultGeographicCRS.WGS84; /** * Dispatcher callback that adds an option to "simulate" requests handled by the * OWS dispatcher. * <p> * In simulation mode the resulting request object is sent back as the response * to the request. * </p> */ public class SimulateCallback implements DispatcherCallback { public static final String KVP = "simulate"; public static final String OPT_DEPTH = "depth"; static Converter<String,String> KEY_CONVERTER = CaseFormat.UPPER_CAMEL.converterTo(CaseFormat.LOWER_UNDERSCORE); static Logger LOG = Logging.getLogger(SimulateCallback.class); @Override public Request init(Request request) { return request; } @Override public Service serviceDispatched(Request request, Service service) throws ServiceException { return null; } @Override public Operation operationDispatched(Request request, Operation operation) { String sim = Optional.ofNullable(request.getRawKvp().get(KVP)).map(String.class::cast).orElse(null); if (sim == null) { return operation; } Map<String,Object> simOpts; if (sim.contains(";")) { try { simOpts = (Map<String, Object>) new FormatOptionsKvpParser().parse(sim.toString()); } catch (Exception e) { throw new RuntimeException("Illegal syntax for simulate options: simulate=<key>:<val>[;<key>:<val>]*", e); } } else { simOpts = Collections.emptyMap(); } throw new HttpErrorCodeException(202, toJSON(operation, simOpts)).setContentType("application/json"); } @Override public Object operationExecuted(Request request, Operation operation, Object result) { return result; } @Override public Response responseDispatched(Request request, Operation operation, Object result, Response response) { return response; } @Override public void finished(Request request) { } String toJSON(Operation op, Map<String,Object> opts) { int depth = Converters.convert(opts.getOrDefault(OPT_DEPTH, 3), Integer.class); JSONStringer out = new JSONStringer(); out.object(); Service srv = op.getService(); out.key("service").object() .key("name").value(srv.getId()) .key("version").value(srv.getVersion()) .endObject(); out.key("operation").object() .key("name").value(op.getId()); Object req = Arrays.stream(op.getParameters()).findFirst().orElse(null); if (req != null) { out.key("request"); traverse(req, 0, depth, out); } out.endObject(); // operation out.endObject(); return out.toString(); } void traverse(Object obj, int depth, int maxDepth, JSONStringer out) { if (obj == null) { out.value(null); return; } if (obj instanceof Collection) { handleCollection((Collection)obj, depth, maxDepth, out); return; } if (obj instanceof Map) { handleMap((Map)obj, depth, maxDepth, out); return; } if (obj instanceof Envelope) { Envelope e = (Envelope) obj; out.object(); out.key("x1").value(e.getMinimum(0)); out.key("y1").value(e.getMaximum(0)); if (e.getDimension() > 1) { out.key("x2").value(e.getMinimum(1)); out.key("y2").value(e.getMaximum(1)); } out.endObject(); return; } if (obj instanceof com.vividsolutions.jts.geom.Envelope) { com.vividsolutions.jts.geom.Envelope e = (com.vividsolutions.jts.geom.Envelope) obj; out.object() .key("x1").value(e.getMinX()) .key("y1").value(e.getMinY()) .key("x2").value(e.getMaxX()) .key("y2").value(e.getMaxY()) .endObject(); return; } if (obj instanceof Filter) { out.value(CQL.toCQL((Filter)obj)); return; } if (!Modifier.isPublic(obj.getClass().getModifiers())) { out.value(obj.toString()); return; } String className = obj.getClass().getName(); if (className.startsWith("java.") || className.startsWith("org.geotools.") || className.startsWith("org.opengis.")) { if (OwsUtils.has(obj, "name")) { out.value(OwsUtils.get(obj, "name")); } else { out.value(obj.toString()); } return; } out.object(); propsOf(obj) .filter(p -> !isMetadata(p.name)) // skip class metadata, etc... .filter(p -> // skip geotools data objects !(FeatureSource.class.isAssignableFrom(p.type) || GridCoverageReader.class.isAssignableFrom(p.type)) ) .filter(p -> // skip geoserver catalog objects !(FeatureTypeInfo.class.isAssignableFrom(p.type) || CoverageInfo.class.isAssignableFrom(p.type)) ) .filter(p -> !isEmpty(p.value())) .forEach(p -> { out.key(toKey(p)); Object value = p.value(); if (isPrimitive(value) || depth >= maxDepth) { out.value(value); } else if (value instanceof Info) { out.value(((Info)value).getId()); } else if (value instanceof Collection) { handleCollection((Collection)value, depth, maxDepth, out); } else if (value instanceof Map) { handleMap((Map)value, depth, maxDepth, out); } else { traverse(value, depth+1, maxDepth, out); } }); if (obj instanceof GetFeatureInfoRequest) { // special case for GetFeatureInto to provide lon/lat corresponding to x/y try { GetFeatureInfoRequest info = (GetFeatureInfoRequest) obj; GetMapRequest map = info.getGetMapRequest(); Coordinate c = WMS.pixelToWorld(info.getXPixel(), info.getYPixel(), new ReferencedEnvelope(map.getBbox(), map.getCrs()), map.getWidth(), map.getHeight()); double[] p = new double[]{c.getOrdinate(0), c.getOrdinate(1)}; MathTransform tx = CRS.findMathTransform(map.getCrs(), WGS84, true); tx.transform(p, 0, p, 0, 1); out.key("lon").value(p[0]); out.key("lat").value(p[1]); } catch(Exception e) { LOG.log(Level.WARNING, "Error calculating lon/lat for GetFeatureInfo i/j", e); } } if (obj instanceof GetMapRequest) { try { GetMapRequest map = (GetMapRequest) obj; ReferencedEnvelope llbox = new ReferencedEnvelope(map.getBbox(), map.getCrs()).transform(WGS84, true); out.key("bbox_wgs84").object(); out.key("west").value(llbox.getMinX()); out.key("east").value(llbox.getMaxX()); out.key("south").value(llbox.getMinY()); out.key("north").value(llbox.getMaxY()); out.endObject(); } catch(Exception e) { LOG.log(Level.WARNING, "Error calculating lon/lat bbox for GetMap bbox", e); } } out.endObject(); } void handleCollection(Collection value, int depth, int maxDepth, JSONStringer out) { out.array(); for (Object o : ((Collection)value)) { traverse(o, depth+1, maxDepth, out); } out.endArray(); } void handleMap(Map value, int depth, int maxDepth, JSONStringer out) { out.object(); for (Object k : value.keySet()) { out.key(k != null ? k.toString() : "null"); traverse(value.get(k), depth+1, maxDepth, out); } out.endObject(); } boolean isMetadata(String f) { if ("Class".equalsIgnoreCase(f)) return true; if ("DeclaringClass".equalsIgnoreCase(f)) return true; if ("EStructuralFeature".equalsIgnoreCase(f)) return true; return false; } boolean isEmpty(Object value) { // skip empty properties if(value == null || (value instanceof Collection && ((Collection) value).isEmpty()) || (value instanceof Map && ((Map) value).isEmpty())) { return true; } return false; } boolean isPrimitive(Object value) { if (value instanceof String) { return true; } if (value instanceof Number) { return true; } Class clazz = value.getClass(); return clazz.isPrimitive() || ClassUtils.wrapperToPrimitive(clazz) != null; } static Pattern UPPER_REGEX = Pattern.compile("([A-Z])([A-Z]+)"); String toKey(Property p) { String key = p.name; Matcher m = UPPER_REGEX.matcher(key); while(m.find()) { key = m.replaceFirst(m.group(1) + m.group(2).toLowerCase()); m = UPPER_REGEX.matcher(key); } return KEY_CONVERTER.convert(key); } final class Property { final String name; final Class type; private final Supplier<Object> value; Object v; Property(String name, Class type, Supplier<Object> value) { this.name = name; this.type = type; this.value = value; } Object value() { if (v == null) { v = value.get(); } return v; } } Stream<Property> propsOf(Object obj) { if (obj instanceof EObject) { EObject eobj = (EObject) obj; return eobj.eClass().getEAllStructuralFeatures().stream() .map(f -> new Property(f.getName(), f.getEType().getInstanceClass(), () -> eobj.eGet(f))); } else { ClassProperties classProps = OwsUtils.getClassProperties(obj.getClass()); return classProps.properties().stream() .map(p -> new Property(p, classProps.getter(p, null).getReturnType(), () -> { Object value; try { value = OwsUtils.get(obj, p); } catch(Exception e) { //value = Throwables.getStackTraceAsString(e); value = e.getMessage(); } return value; })); } } }