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 00000000000..21726300b43 Binary files /dev/null and b/doc/en/user/source/extensions/mapml/images/mapml_global_menu.png differ 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 00000000000..5f57ae6c4c4 Binary files /dev/null and b/doc/en/user/source/extensions/mapml/images/mapml_global_verbose_output.png differ 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 + } }