From 1de3553a18456dd95ed8e431e45def05978987c0 Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Fri, 30 May 2025 17:11:08 -0400 Subject: [PATCH] GEOS-11850 Pretty-print text/mapml responses based on GeoServer global verbose setting --- .../mapml/images/mapml_global_menu.png | Bin 0 -> 6410 bytes .../images/mapml_global_verbose_output.png | Bin 0 -> 26791 bytes .../source/extensions/mapml/installation.rst | 7 ++ .../org/geoserver/mapml/MapMLEncoder.java | 112 ++++++++++++++++- .../MapMLGetFeatureInfoOutputFormat.java | 13 +- .../mapml/MapMLGetFeatureOutputFormat.java | 14 +-- .../geoserver/mapml/MapMLMapOutputFormat.java | 4 +- .../mapml/MapMLMessageConverter.java | 19 ++- .../MapMLGetFeatureOutputFormatTest.java | 81 ++++++++++++ .../mapml/MapMLMessageConverterTest.java | 115 ++++++++++++++++++ .../org/geoserver/mapml/MapMLWMSTest.java | 104 ++++++++++++++++ 11 files changed, 432 insertions(+), 37 deletions(-) create mode 100644 doc/en/user/source/extensions/mapml/images/mapml_global_menu.png create mode 100644 doc/en/user/source/extensions/mapml/images/mapml_global_verbose_output.png create mode 100644 src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLMessageConverterTest.java diff --git a/doc/en/user/source/extensions/mapml/images/mapml_global_menu.png b/doc/en/user/source/extensions/mapml/images/mapml_global_menu.png new file mode 100644 index 0000000000000000000000000000000000000000..21726300b43ca3c75749f348bac1e34baf12fe83 GIT binary patch literal 6410 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 literal 0 HcmV?d00001 diff --git a/doc/en/user/source/extensions/mapml/images/mapml_global_verbose_output.png b/doc/en/user/source/extensions/mapml/images/mapml_global_verbose_output.png new file mode 100644 index 0000000000000000000000000000000000000000..5f57ae6c4c40d11c18aec1bd50b21a5c88d1fe93 GIT binary patch literal 26791 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 R{#=MaLq%J;Sn<`{{{@h~bNK)O literal 0 HcmV?d00001 diff --git a/doc/en/user/source/extensions/mapml/installation.rst b/doc/en/user/source/extensions/mapml/installation.rst index 512f4fa9420..ae2ecdd750f 100644 --- a/doc/en/user/source/extensions/mapml/installation.rst +++ b/doc/en/user/source/extensions/mapml/installation.rst @@ -80,7 +80,14 @@ For example, the UTM14WGS84Quad specified in the above selector has the followin .. figure:: images/mapml_utm_gridset.png +Global Settings +--------------- + +.. figure:: images/mapml_global_menu.png + +The Global settings menu (above) contains a Service Response Settings section (below) which contains the Verbose XML output (pretty print) checkbox. The MapML extension respects or uses this setting when serializing text/mapml (media type) responses. Be aware that caching on both the client and server may prevent this setting from becoming immediately obvious in devtools responses. Refreshing the browser cache can request a new version of the response, but if the response is cached on the server, for example as a vector tile, it may not be possible to obtain a pretty printed version of the data, short of deleting the tile cache, which may be undesirable. +.. figure:: images/mapml_global_verbose_output.png Styles ------ diff --git a/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLEncoder.java b/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLEncoder.java index c23bfae64a0..a9c1d85ea15 100644 --- a/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLEncoder.java +++ b/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLEncoder.java @@ -51,10 +51,23 @@ private Marshaller createMarshaller() throws JAXBException { * @param output OutputStream */ public void encode(Mapml mapml, OutputStream output) { + encode(mapml, output, false); + } + + /** + * Use Marshaller to encode MapML object onto an output stream with optional pretty-printing + * + * @param mapml MapML object + * @param output OutputStream + * @param prettyPrint true to enable pretty-printing with 2-space indents, false for dense markup + */ + public void encode(Mapml mapml, OutputStream output, boolean prettyPrint) { try { - XMLStreamWriter writer = new Wrapper(XMLOutputFactory.newInstance().createXMLStreamWriter(output)); - createMarshaller().marshal(mapml, writer); - writer.flush(); + XMLOutputFactory factory = XMLOutputFactory.newInstance(); + Wrapper wrapper = new Wrapper(factory.createXMLStreamWriter(output)); + wrapper.setIndenting(prettyPrint); + createMarshaller().marshal(mapml, wrapper); + wrapper.flush(); } catch (JAXBException | XMLStreamException e) { throw new ServiceException(e); } @@ -81,6 +94,13 @@ static class Wrapper implements XMLStreamWriter { private final XMLStreamWriter writer; private static final String NS_PREFIX = ""; + public static final String MAPML_INDENT_PROPERTY = "mapml.indent"; + private static final String INDENT = " "; + + private boolean indenting = false; + private int depth = 0; + private boolean needsIndent = false; + private boolean lastWasStartElement = false; /** * Constructor @@ -91,39 +111,90 @@ static class Wrapper implements XMLStreamWriter { this.writer = writer; } + /** Writes indentation if pretty-printing is enabled */ + private void writeIndent() throws XMLStreamException { + if (indenting && needsIndent) { + writer.writeCharacters("\n"); + for (int i = 0; i < depth; i++) { + writer.writeCharacters(INDENT); + } + needsIndent = false; + } + } + @Override public void writeStartElement(String localName) throws XMLStreamException { + writeIndent(); writer.writeStartElement(localName); + depth++; + needsIndent = true; + lastWasStartElement = true; } @Override public void writeStartElement(String namespaceURI, String localName) throws XMLStreamException { + writeIndent(); writer.writeStartElement(namespaceURI, localName); + depth++; + needsIndent = true; + lastWasStartElement = true; } @Override public void writeStartElement(String prefix, String localName, String namespaceURI) throws XMLStreamException { + writeIndent(); writer.writeStartElement(NS_PREFIX, localName, namespaceURI); + depth++; + needsIndent = true; + lastWasStartElement = true; } @Override public void writeEmptyElement(String namespaceURI, String localName) throws XMLStreamException { - writer.writeEmptyElement(namespaceURI, localName); + // Force HTML-compatible empty elements with explicit end tags + writeIndent(); + writer.writeStartElement(namespaceURI, localName); + writer.writeEndElement(); + needsIndent = true; } @Override public void writeEmptyElement(String prefix, String localName, String namespaceURI) throws XMLStreamException { - writer.writeEmptyElement(prefix, localName, namespaceURI); + // Force HTML-compatible empty elements with explicit end tags + writeIndent(); + writer.writeStartElement(NS_PREFIX, localName, namespaceURI); + writer.writeEndElement(); + needsIndent = true; } @Override public void writeEmptyElement(String localName) throws XMLStreamException { - writer.writeEmptyElement(localName); + // Force HTML-compatible empty elements with explicit end tags + writeIndent(); + writer.writeStartElement(localName); + writer.writeEndElement(); + needsIndent = true; } @Override public void writeEndElement() throws XMLStreamException { + depth--; + // For empty elements (start immediately followed by end), keep on same line + if (needsIndent && !lastWasStartElement) { + writeIndent(); + } else if (lastWasStartElement) { + // Note: empty string typically doesn't add content but signals "non-empty" to writer + writer.writeCharacters(""); + } writer.writeEndElement(); + + // Add two newlines after the root closing tag + if (depth == 0 && indenting) { + writer.writeCharacters("\n\n"); + } + + needsIndent = true; + lastWasStartElement = false; } @Override @@ -169,7 +240,9 @@ public void writeDefaultNamespace(String namespaceURI) throws XMLStreamException @Override public void writeComment(String data) throws XMLStreamException { + writeIndent(); writer.writeComment(data); + needsIndent = true; } @Override @@ -214,11 +287,17 @@ public void writeStartDocument(String encoding, String version) throws XMLStream @Override public void writeCharacters(String text) throws XMLStreamException { + // Don't indent before text content as it would alter the actual content + needsIndent = false; + lastWasStartElement = false; writer.writeCharacters(text); } @Override public void writeCharacters(char[] text, int start, int len) throws XMLStreamException { + // Don't indent before text content as it would alter the actual content + needsIndent = false; + lastWasStartElement = false; writer.writeCharacters(text, start, len); } @@ -249,7 +328,28 @@ public NamespaceContext getNamespaceContext() { @Override public Object getProperty(String name) throws IllegalArgumentException { + if (MAPML_INDENT_PROPERTY.equals(name)) { + return indenting; + } return writer.getProperty(name); } + + /** + * Sets the indenting property for pretty-printing + * + * @param indent true to enable pretty-printing with 2-space indents, false for dense markup + */ + public void setIndenting(boolean indent) { + this.indenting = indent; + } + + /** + * Gets the current indenting state + * + * @return true if pretty-printing is enabled + */ + public boolean isIndenting() { + return indenting; + } } } diff --git a/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLGetFeatureInfoOutputFormat.java b/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLGetFeatureInfoOutputFormat.java index e71edc0038f..f4048098487 100644 --- a/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLGetFeatureInfoOutputFormat.java +++ b/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLGetFeatureInfoOutputFormat.java @@ -7,7 +7,6 @@ import java.io.IOException; import java.io.OutputStream; -import java.io.OutputStreamWriter; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -16,8 +15,6 @@ import java.util.function.Function; import java.util.logging.Level; import java.util.logging.Logger; -import javax.xml.transform.Result; -import javax.xml.transform.stream.StreamResult; import net.opengis.wfs.FeatureCollectionType; import org.geoserver.catalog.Catalog; import org.geoserver.catalog.FeatureTypeInfo; @@ -47,7 +44,6 @@ import org.geotools.feature.FeatureCollection; import org.geotools.util.logging.Logging; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.oxm.jaxb.Jaxb2Marshaller; /** * @author Chris Hodgson @@ -57,7 +53,7 @@ public class MapMLGetFeatureInfoOutputFormat extends GetFeatureInfoOutputFormat private static final Logger LOGGER = Logging.getLogger("org.geoserver.mapml"); @Autowired - private Jaxb2Marshaller mapmlMarshaller; + private MapMLEncoder mapMLEncoder; private WMS wms; @@ -157,10 +153,9 @@ public void write(FeatureCollectionType results, GetFeatureInfoRequest request, } } - OutputStreamWriter osw = new OutputStreamWriter(out, wms.getCharSet()); - Result result = new StreamResult(osw); - mapmlMarshaller.marshal(mapml, result); - osw.flush(); + // write to output based on global verbose setting + boolean verbose = wms.getGeoServer().getGlobal().getSettings().isVerbose(); + mapMLEncoder.encode(mapml, out, verbose); } @Override diff --git a/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLGetFeatureOutputFormat.java b/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLGetFeatureOutputFormat.java index 97319345561..952ca7e081a 100644 --- a/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLGetFeatureOutputFormat.java +++ b/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLGetFeatureOutputFormat.java @@ -7,14 +7,11 @@ import java.io.IOException; import java.io.OutputStream; -import java.io.OutputStreamWriter; import java.net.URI; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.logging.Logger; -import javax.xml.transform.Result; -import javax.xml.transform.stream.StreamResult; import net.opengis.wfs.impl.GetFeatureTypeImpl; import net.opengis.wfs.impl.QueryTypeImpl; import org.geoserver.catalog.LayerInfo; @@ -32,7 +29,6 @@ import org.geotools.referencing.CRS; import org.geotools.util.logging.Logging; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.oxm.jaxb.Jaxb2Marshaller; /** * @author Chris Hodgson @@ -42,7 +38,7 @@ public class MapMLGetFeatureOutputFormat extends WFSGetFeatureOutputFormat { private static final Logger LOGGER = Logging.getLogger(MapMLGetFeatureOutputFormat.class); @Autowired - private Jaxb2Marshaller mapmlMarshaller; + private MapMLEncoder mapMLEncoder; private String base; private String path; @@ -97,11 +93,9 @@ protected void write(FeatureCollectionResponse featureCollectionResponse, Output null, null); - // write to output - OutputStreamWriter osw = new OutputStreamWriter(out, gs.getSettings().getCharset()); - Result result = new StreamResult(osw); - mapmlMarshaller.marshal(mapml, result); - osw.flush(); + // write to output based on global verbose setting + boolean verbose = gs.getGlobal().getSettings().isVerbose(); + mapMLEncoder.encode(mapml, out, verbose); } /** diff --git a/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLMapOutputFormat.java b/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLMapOutputFormat.java index 0a21ac91630..3cc779fb61a 100644 --- a/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLMapOutputFormat.java +++ b/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLMapOutputFormat.java @@ -73,7 +73,9 @@ public WebMap produceMap(WMSMapContent mapContent) throws ServiceException, IOEx mapMLDocument = mapMLDocumentBuilder.getMapMLDocument(); } ByteArrayOutputStream bos = new ByteArrayOutputStream(); - encoder.encode(mapMLDocument, bos); + // write to output based on global verbose setting + boolean verbose = geoServer.getGlobal().getSettings().isVerbose(); + encoder.encode(mapMLDocument, bos, verbose); return new RawMap(mapContent, bos, MapMLConstants.MAPML_MIME_TYPE); } diff --git a/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLMessageConverter.java b/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLMessageConverter.java index 2487213580f..eb1b7281bf1 100644 --- a/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLMessageConverter.java +++ b/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLMessageConverter.java @@ -5,16 +5,12 @@ package org.geoserver.mapml; import java.io.IOException; -import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; -import javax.xml.transform.Result; -import javax.xml.transform.stream.StreamResult; import org.geoserver.rest.converters.BaseMessageConverter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpOutputMessage; import org.springframework.http.MediaType; import org.springframework.lang.Nullable; -import org.springframework.oxm.jaxb.Jaxb2Marshaller; /** * @author Chris Hodgson @@ -23,7 +19,7 @@ public class MapMLMessageConverter extends BaseMessageConverter { @Autowired - private Jaxb2Marshaller mapmlMarshaller; + private MapMLEncoder mapMLEncoder; /** */ public MapMLMessageConverter() { @@ -47,7 +43,7 @@ public boolean canRead(Class clazz, @Nullable MediaType mediaType) { */ @Override public boolean canWrite(Class clazz, @Nullable MediaType mediaType) { - return canWrite(mediaType) && mapmlMarshaller.supports(clazz); + return canWrite(mediaType) && org.geoserver.mapml.xml.Mapml.class.isAssignableFrom(clazz); } /** @@ -69,11 +65,12 @@ protected boolean supports(Class clazz) { @Override protected void writeInternal(Object o, HttpOutputMessage outputMessage) throws UnsupportedEncodingException, IOException { - try (OutputStreamWriter osw = new OutputStreamWriter( - outputMessage.getBody(), geoServer.getSettings().getCharset())) { - Result result = new StreamResult(osw); - mapmlMarshaller.marshal(o, result); - osw.flush(); + if (o instanceof org.geoserver.mapml.xml.Mapml) { + // write to output based on global verbose setting + boolean verbose = geoServer.getGlobal().getSettings().isVerbose(); + mapMLEncoder.encode((org.geoserver.mapml.xml.Mapml) o, outputMessage.getBody(), verbose); + } else { + throw new IllegalArgumentException("Can only write Mapml objects, got: " + o.getClass()); } } } diff --git a/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLGetFeatureOutputFormatTest.java b/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLGetFeatureOutputFormatTest.java index 5bd6e13f25e..573c1e7f1eb 100644 --- a/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLGetFeatureOutputFormatTest.java +++ b/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLGetFeatureOutputFormatTest.java @@ -9,6 +9,7 @@ import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -25,6 +26,8 @@ import org.geoserver.catalog.FeatureTypeInfo; import org.geoserver.catalog.MetadataMap; import org.geoserver.catalog.ResourceInfo; +import org.geoserver.config.GeoServer; +import org.geoserver.config.GeoServerInfo; import org.geoserver.data.test.MockData; import org.geoserver.data.test.SystemTestData; import org.geoserver.mapml.tcrs.TiledCRSConstants; @@ -498,4 +501,82 @@ protected org.w3c.dom.Document getMapML(final String path, HashMap query) throws Exception { + MockHttpServletRequest request = createRequest(path, query); + request.setMethod("GET"); + request.setContent(new byte[] {}); + return dispatch(request, "UTF-8").getContentAsString(); + } + + @Test + public void testVerboseSettingPrettyPrint() throws Exception { + // Test that output is pretty-printed when global verbose setting is true + GeoServer gs = getGeoServer(); + GeoServerInfo info = gs.getGlobal(); + // default is unchecked / false + info.getSettings().setVerbose(true); + gs.save(info); + + HashMap vars = new HashMap<>(); + vars.put("service", "wfs"); + vars.put("version", "1.0.0"); + vars.put("request", "GetFeature"); + vars.put("typename", MockData.STREAMS.getLocalPart()); + vars.put("outputFormat", "MAPML"); + + String response = getMapMLAsString("wfs", vars); + + // Check for pretty-printing indicators + assertTrue("Response should contain newlines for pretty-printing", response.contains("\n")); + assertTrue("Response should use two spaces before elements for indenting", response.contains(" <")); + + // Verify XML structure is preserved + org.w3c.dom.Document doc = dom(new ByteArrayInputStream(response.getBytes()), true); + assertEquals("mapml-", doc.getDocumentElement().getNodeName()); + + // reset the default false + info.getSettings().setVerbose(false); + gs.save(info); + } + + @Test + public void testVerboseSettingDenseOutput() throws Exception { + // Test that output is dense when global verbose setting is false + GeoServer gs = getGeoServer(); + GeoServerInfo info = gs.getGlobal(); + // the default is actually false + info.getSettings().setVerbose(false); + gs.save(info); + + HashMap vars = new HashMap<>(); + vars.put("service", "wfs"); + vars.put("version", "1.0.0"); + vars.put("request", "GetFeature"); + vars.put("typename", MockData.STREAMS.getLocalPart()); + vars.put("outputFormat", "MAPML"); + + String response = getMapMLAsString("wfs", vars); + + // Check that output is more compact (fewer newlines and no indentation) + // Should still have some structure but less whitespace + assertFalse( + "Dense output should have minimal indentation spaces", + response.contains(" <") && response.contains(" <")); + + // Verify XML structure is still valid + org.w3c.dom.Document doc = dom(new ByteArrayInputStream(response.getBytes()), true); + assertEquals("mapml-", doc.getDocumentElement().getNodeName()); + + // the default is actually false + info.getSettings().setVerbose(false); + gs.save(info); + } } diff --git a/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLMessageConverterTest.java b/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLMessageConverterTest.java new file mode 100644 index 00000000000..71cdef05893 --- /dev/null +++ b/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLMessageConverterTest.java @@ -0,0 +1,115 @@ +/* (c) 2025 Open Source Geospatial Foundation - all rights reserved + * This code is licensed under the GPL 2.0 license, available at the root + * application directory. + */ +package org.geoserver.mapml; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import org.geoserver.config.GeoServer; +import org.geoserver.config.GeoServerInfo; +import org.geoserver.config.SettingsInfo; +import org.geoserver.mapml.xml.BodyContent; +import org.geoserver.mapml.xml.HeadContent; +import org.geoserver.mapml.xml.Mapml; +import org.junit.Before; +import org.junit.Test; +import org.springframework.http.HttpOutputMessage; + +public class MapMLMessageConverterTest { + + private MapMLMessageConverter converter; + private MapMLEncoder mockEncoder; + private GeoServer mockGeoServer; + private GeoServerInfo mockGeoServerInfo; + private SettingsInfo mockSettings; + private HttpOutputMessage mockOutputMessage; + private ByteArrayOutputStream outputStream; + + @Before + public void setUp() throws Exception { + converter = new MapMLMessageConverter(); + + // Mock the dependencies + mockEncoder = mock(MapMLEncoder.class); + mockGeoServer = mock(GeoServer.class); + mockGeoServerInfo = mock(GeoServerInfo.class); + mockSettings = mock(SettingsInfo.class); + mockOutputMessage = mock(HttpOutputMessage.class); + outputStream = new ByteArrayOutputStream(); + + // Set up the mock chain + when(mockGeoServer.getGlobal()).thenReturn(mockGeoServerInfo); + when(mockGeoServerInfo.getSettings()).thenReturn(mockSettings); + when(mockOutputMessage.getBody()).thenReturn(outputStream); + + // Inject mocks using reflection + java.lang.reflect.Field encoderField = MapMLMessageConverter.class.getDeclaredField("mapMLEncoder"); + encoderField.setAccessible(true); + encoderField.set(converter, mockEncoder); + + java.lang.reflect.Field geoServerField = + converter.getClass().getSuperclass().getDeclaredField("geoServer"); + geoServerField.setAccessible(true); + geoServerField.set(converter, mockGeoServer); + } + + @Test + public void testCanWriteMapmlClass() { + assertTrue( + "Should be able to write Mapml objects", + converter.canWrite(Mapml.class, MapMLConstants.MAPML_MEDIA_TYPE)); + assertFalse( + "Should not be able to write other objects", + converter.canWrite(String.class, MapMLConstants.MAPML_MEDIA_TYPE)); + } + + @Test + public void testWriteInternalWithVerboseTrue() throws UnsupportedEncodingException, IOException { + // Set verbose = true + when(mockSettings.isVerbose()).thenReturn(true); + + Mapml mapml = createTestMapml(); + + converter.writeInternal(mapml, mockOutputMessage); + + // Verify that encode was called with verbose = true + org.mockito.Mockito.verify(mockEncoder).encode(mapml, outputStream, true); + } + + @Test + public void testWriteInternalWithVerboseFalse() throws UnsupportedEncodingException, IOException { + // Set verbose = false + when(mockSettings.isVerbose()).thenReturn(false); + + Mapml mapml = createTestMapml(); + + converter.writeInternal(mapml, mockOutputMessage); + + // Verify that encode was called with verbose = false + org.mockito.Mockito.verify(mockEncoder).encode(mapml, outputStream, false); + } + + @Test + public void testWriteInternalWithNonMapmlObject() { + assertThrows(IllegalArgumentException.class, () -> { + converter.writeInternal("not a mapml object", mockOutputMessage); + }); + } + + private Mapml createTestMapml() { + Mapml mapml = new Mapml(); + HeadContent head = new HeadContent(); + head.setTitle("Test"); + mapml.setHead(head); + mapml.setBody(new BodyContent()); + return mapml; + } +} diff --git a/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLWMSTest.java b/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLWMSTest.java index a779a56c6dd..8cd354c12f3 100644 --- a/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLWMSTest.java +++ b/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLWMSTest.java @@ -57,6 +57,7 @@ import org.geoserver.catalog.StyleInfo; import org.geoserver.config.GeoServer; import org.geoserver.config.GeoServerDataDirectory; +import org.geoserver.config.GeoServerInfo; import org.geoserver.data.test.MockData; import org.geoserver.data.test.SystemTestData; import org.geoserver.gwc.GWC; @@ -2251,4 +2252,107 @@ public void describeTo(Description description) { description.appendText("Bounds matches " + expected + " with tolerance " + tolerance); } } + + @Test + public void testMapMLWMSVerboseSettingPrettyPrint() throws Exception { + // Test that WMS GetMap MapML output is pretty-printed when global verbose setting is true + GeoServer gs = getGeoServer(); + GeoServerInfo info = gs.getGlobal(); + info.getSettings().setVerbose(true); + gs.save(info); + + String layerId = getLayerId(MockData.BASIC_POLYGONS); + MockRequestResponse requestResponse = getMockRequestResponse(layerId, null, null, "EPSG:4326", null); + + String response = requestResponse.response.getContentAsString(); + + // Check for pretty-printing indicators + assertTrue("WMS MapML response should contain newlines for pretty-printing", response.contains("\n")); + assertTrue("WMS MapML response should contain indentation spaces", response.contains(" <")); + + // Verify XML structure is preserved + org.w3c.dom.Document doc = dom(new ByteArrayInputStream(response.getBytes()), true); + assertEquals("mapml-", doc.getDocumentElement().getNodeName()); + + // reset to default + info.getSettings().setVerbose(false); + gs.save(info); + } + + @Test + public void testMapMLWMSVerboseSettingDenseOutput() throws Exception { + // Test that WMS GetMap MapML output is dense when global verbose setting is false + GeoServer gs = getGeoServer(); + GeoServerInfo info = gs.getGlobal(); + info.getSettings().setVerbose(false); + gs.save(info); + + String layerId = getLayerId(MockData.BASIC_POLYGONS); + MockRequestResponse requestResponse = getMockRequestResponse(layerId, null, null, "EPSG:4326", null); + + String response = requestResponse.response.getContentAsString(); + + // Check that output is more compact (minimal indentation) + assertFalse( + "Dense WMS MapML output should have minimal indentation spaces", + response.contains(" <") && response.contains(" <")); + + // Verify XML structure is still valid + org.w3c.dom.Document doc = dom(new ByteArrayInputStream(response.getBytes()), true); + assertEquals("mapml-", doc.getDocumentElement().getNodeName()); + + // reset to default not necessary - default is false + } + + @Test + public void testMapMLGetFeatureInfoVerboseSettingPrettyPrint() throws Exception { + // Test that WMS GetFeatureInfo MapML output is pretty-printed when global verbose setting is true + GeoServer gs = getGeoServer(); + GeoServerInfo info = gs.getGlobal(); + info.getSettings().setVerbose(true); + gs.save(info); + + String response = getAsString("wms?LAYERS=" + getLayerId(MockData.FORESTS) + "&STYLES=&FORMAT=image%2Fpng" + + "&SERVICE=WMS&VERSION=1.1.1&REQUEST=GetFeatureInfo&SRS=EPSG%3A4326&BBOX=-0.002,-0.002,0.002,0.002" + + "&WIDTH=20&HEIGHT=20&INFO_FORMAT=text/mapml&QUERY_LAYERS=" + getLayerId(MockData.FORESTS) + + "&X=10&Y=10"); + + // Check for pretty-printing indicators + assertTrue( + "GetFeatureInfo MapML response should contain newlines for pretty-printing", response.contains("\n")); + assertTrue("GetFeatureInfo MapML response should contain indentation spaces", response.contains(" ")); + + // Verify XML structure is preserved + org.w3c.dom.Document doc = dom(new ByteArrayInputStream(response.getBytes()), true); + assertEquals("mapml-", doc.getDocumentElement().getNodeName()); + + // reset to default + info.getSettings().setVerbose(false); + gs.save(info); + } + + @Test + public void testMapMLGetFeatureInfoVerboseSettingDenseOutput() throws Exception { + // Test that WMS GetFeatureInfo MapML output is dense when global verbose setting is false + GeoServer gs = getGeoServer(); + GeoServerInfo info = gs.getGlobal(); + info.getSettings().setVerbose(false); + gs.save(info); + + String response = getAsString("wms?LAYERS=" + getLayerId(MockData.FORESTS) + "&STYLES=&FORMAT=image%2Fpng" + + "&SERVICE=WMS&VERSION=1.1.1&REQUEST=GetFeatureInfo&SRS=EPSG%3A4326&BBOX=-0.002,-0.002,0.002,0.002" + + "&WIDTH=20&HEIGHT=20&INFO_FORMAT=text/mapml&QUERY_LAYERS=" + getLayerId(MockData.FORESTS) + + "&X=10&Y=10"); + + // Check that output is more compact (minimal indentation) + assertFalse( + "Dense GetFeatureInfo MapML output should have minimal indentation spaces", + response.contains(" <") && response.contains(" <")); + + // Verify XML structure is still valid + org.w3c.dom.Document doc = dom(new ByteArrayInputStream(response.getBytes()), true); + assertEquals("mapml-", doc.getDocumentElement().getNodeName()); + + // reset to default not necessary - default is false + } }