diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/templating/StaticInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/templating/StaticInterceptor.java new file mode 100644 index 000000000..e35102ff3 --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/templating/StaticInterceptor.java @@ -0,0 +1,144 @@ +package com.predic8.membrane.core.interceptor.templating; + +import com.predic8.membrane.annot.MCAttribute; +import com.predic8.membrane.annot.MCElement; +import com.predic8.membrane.annot.MCTextContent; +import com.predic8.membrane.core.beautifier.JSONBeautifier; +import com.predic8.membrane.core.exchange.Exchange; +import com.predic8.membrane.core.http.Message; +import com.predic8.membrane.core.interceptor.AbstractInterceptor; +import com.predic8.membrane.core.interceptor.Outcome; +import com.predic8.membrane.core.util.TextUtil; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.StringReader; + +import static com.predic8.membrane.core.http.MimeType.*; +import static com.predic8.membrane.core.interceptor.Outcome.CONTINUE; +import static com.predic8.membrane.core.util.TextUtil.unifyIndent; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.apache.commons.text.StringEscapeUtils.escapeHtml4; + +@MCElement(name = "static", mixed = true) +public class StaticInterceptor extends AbstractInterceptor { + + protected String location; + + protected String textTemplate; + + protected String contentType = TEXT_PLAIN; + + protected Boolean pretty = false; + + protected final JSONBeautifier jsonBeautifier = new JSONBeautifier(); + + protected static final Logger log = LoggerFactory.getLogger("StaticInterceptor"); + + public StaticInterceptor() { + name = "Static"; + } + + @Override + public Outcome handleRequest(Exchange exc) throws Exception { + return handleInternal(exc.getRequest()); + } + + @Override + public Outcome handleResponse(Exchange exc) throws Exception { + return handleInternal(exc.getResponse()); + } + + private Outcome handleInternal(Message msg) { + msg.setBodyContent(getTemplateBytes()); + msg.getHeader().setContentType(getContentType()); + return CONTINUE; + } + + private byte @NotNull [] getTemplateBytes() { + if (!pretty) + return textTemplate.getBytes(UTF_8); + + return switch (contentType) { + case APPLICATION_JSON -> prettifyJson(textTemplate).getBytes(UTF_8); + case APPLICATION_XML, APPLICATION_SOAP, TEXT_HTML, TEXT_XML, TEXT_HTML_UTF8, TEXT_XML_UTF8 -> prettifyXML(textTemplate).getBytes(UTF_8); + default -> unifyIndent(textTemplate).getBytes(UTF_8); + }; + } + + private String prettifyXML(String text) { + try { + return TextUtil.formatXML(new StringReader(text)); + } catch (Exception e) { + log.warn("Failed to format XML", e); + return text; + } + } + + String prettifyJson(String text) { + try { + return jsonBeautifier.beautify(text); + } catch (IOException e) { + log.warn("Failed to format JSON", e); + return text; + } + } + + @Override + public void init() throws Exception { + if (this.getLocation() != null && (getTextTemplate() != null && !getTextTemplate().isBlank())) { + throw new IllegalStateException("On <" + getName() + ">, ./text() and ./@location cannot be set at the same time."); + } + } + + public String getLocation() { + return location; + } + + @MCAttribute + public void setLocation(String location){ + this.location = location; + } + + public String getTextTemplate() { + return textTemplate; + } + + @MCTextContent + public void setTextTemplate(String textTemplate) { + this.textTemplate = textTemplate; + } + + protected String getName() { + return getClass().getAnnotation(MCElement.class).name(); + } + + public String getContentType() { + return contentType; + } + + @MCAttribute + public void setContentType(String contentType) { + this.contentType = contentType; + } + + public Boolean getPretty() { + return pretty; + } + + @MCAttribute + public void setPretty(String pretty) { + this.pretty = Boolean.valueOf(pretty); + } + + private String formatAsHtml(String plaintext) { + return String.join("
", escapeHtml4(plaintext).split("\n")); + } + + @Override + public String getShortDescription() { + return formatAsHtml(textTemplate); + } +} diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/templating/TemplateInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/templating/TemplateInterceptor.java index cfcd399bd..a2002cbfb 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/templating/TemplateInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/templating/TemplateInterceptor.java @@ -14,27 +14,27 @@ package com.predic8.membrane.core.interceptor.templating; -import com.predic8.membrane.annot.*; -import com.predic8.membrane.core.beautifier.*; -import com.predic8.membrane.core.exceptions.*; -import com.predic8.membrane.core.exchange.*; -import com.predic8.membrane.core.http.*; -import com.predic8.membrane.core.interceptor.*; -import com.predic8.membrane.core.lang.*; -import com.predic8.membrane.core.resolver.*; -import groovy.text.*; -import org.apache.commons.io.*; -import org.apache.commons.lang3.*; -import org.slf4j.*; - -import java.io.*; -import java.util.*; +import com.predic8.membrane.annot.MCElement; +import com.predic8.membrane.core.exceptions.ProblemDetails; +import com.predic8.membrane.core.exchange.Exchange; +import com.predic8.membrane.core.http.Message; +import com.predic8.membrane.core.interceptor.Outcome; +import com.predic8.membrane.core.lang.ScriptingUtils; +import com.predic8.membrane.core.resolver.ResolverMap; +import groovy.text.StreamingTemplateEngine; +import groovy.text.Template; +import groovy.text.TemplateExecutionException; +import groovy.text.XmlTemplateEngine; +import org.apache.commons.io.FilenameUtils; + +import java.io.InputStreamReader; +import java.util.HashMap; import static com.predic8.membrane.core.http.MimeType.*; -import static com.predic8.membrane.core.interceptor.Interceptor.Flow.*; -import static com.predic8.membrane.core.interceptor.Outcome.*; -import static java.nio.charset.StandardCharsets.*; -import static org.apache.commons.text.StringEscapeUtils.*; +import static com.predic8.membrane.core.interceptor.Interceptor.Flow.REQUEST; +import static com.predic8.membrane.core.interceptor.Interceptor.Flow.RESPONSE; +import static com.predic8.membrane.core.interceptor.Outcome.CONTINUE; +import static java.nio.charset.StandardCharsets.UTF_8; /** * @description Renders the body content of a message from a template. The template can @@ -48,25 +48,12 @@ @MCElement(name="template", mixed = true) -public class TemplateInterceptor extends AbstractInterceptor{ - - /** - * @description Path of template file - */ - private String location; - - private String textTemplate; - - private Template template; - - private String contentType = TEXT_PLAIN; - - private Boolean pretty = false; - - private final JSONBeautifier jsonBeautifier = new JSONBeautifier(); +public class TemplateInterceptor extends StaticInterceptor { private boolean scriptAccessesJson; + protected Template template; + public TemplateInterceptor() { name = "Template"; } @@ -97,14 +84,6 @@ private Outcome handleInternal(Message msg, Exchange exc, Flow flow) { return CONTINUE; } - String prettifyJson(String text) { - try { - return jsonBeautifier.beautify(text); - } catch (IOException e) { - return text; - } - } - @SuppressWarnings("RedundantThrows") // Declaration of exception is needed. However, Groovy does not declare it. private String fillTemplate(Exchange exc, Message msg, Flow flow) throws TemplateExecutionException { @@ -155,32 +134,6 @@ public void init() throws Exception { throw new IllegalStateException("You have to set either ./@location or ./text()"); } - public String getLocation() { - return location; - } - - /** - * @description path of xml template file. - * @example template.xml - */ - @MCAttribute - public void setLocation(String location){ - this.location = location; - } - - public String getTextTemplate() { - return textTemplate; - } - - @MCTextContent - public void setTextTemplate(String textTemplate) throws IOException, ClassNotFoundException { - this.textTemplate = textTemplate; - - if(textTemplate != null && !StringUtils.isBlank(textTemplate)){ - template = new StreamingTemplateEngine().createTemplate(this.getTextTemplate()); - } - } - public Template getTemplate() { return template; } @@ -188,45 +141,4 @@ public Template getTemplate() { public void setTemplate(Template template) { this.template = template; } - - - private String getName() { - return getClass().getAnnotation(MCElement.class).name(); - } - - public String getContentType() { - return contentType; - } - - /** - * @description content type for body - * @example application/json - */ - @MCAttribute - public void setContentType(String contentType) { - this.contentType = contentType; - } - - public Boolean getPretty() { - return pretty; - } - - /** - * @description Format JSON documents. - * @example yes - * @default no - */ - @MCAttribute - public void setPretty(String pretty) { - this.pretty = Boolean.valueOf(pretty); - } - - private String formatAsHtml(String plaintext) { - return String.join("
", escapeHtml4(plaintext).split("\n")); - } - - @Override - public String getShortDescription() { - return formatAsHtml(textTemplate); - } } \ No newline at end of file diff --git a/core/src/main/java/com/predic8/membrane/core/transport/ssl/PEMSupport.java b/core/src/main/java/com/predic8/membrane/core/transport/ssl/PEMSupport.java index e2bf4fa74..21b74f207 100644 --- a/core/src/main/java/com/predic8/membrane/core/transport/ssl/PEMSupport.java +++ b/core/src/main/java/com/predic8/membrane/core/transport/ssl/PEMSupport.java @@ -36,6 +36,8 @@ import java.util.List; import java.util.regex.Pattern; +import static com.predic8.membrane.core.util.TextUtil.unifyIndent; + public abstract class PEMSupport { private static final Logger log = LoggerFactory.getLogger(PEMSupport.class.getName()); @@ -64,21 +66,8 @@ public PEMSupportImpl() { Security.addProvider(new BouncyCastleProvider()); } - private String cleanupPEM(String pemBlock) { - String lines[] = pemBlock.split("\r?\n"); - StringBuilder block = new StringBuilder(); - for (String line : lines) { - String l = line.replaceAll("^\\s+", ""); - if (l.length() > 0) { - block.append(l); - block.append("\n"); - } - } - return block.toString(); - } - public X509Certificate parseCertificate(String pemBlock) throws IOException { - PEMParser p2 = new PEMParser(new StringReader(cleanupPEM(pemBlock))); + PEMParser p2 = new PEMParser(new StringReader(unifyIndent(pemBlock))); Object o2 = p2.readObject(); if (o2 == null) throw new InvalidParameterException("Could not read certificate. Expected the certificate to begin with '-----BEGIN CERTIFICATE-----'."); @@ -94,7 +83,7 @@ public X509Certificate parseCertificate(String pemBlock) throws IOException { } public List parseCertificates(String pemBlock) throws IOException { List res = new ArrayList<>(); - PEMParser p2 = new PEMParser(new StringReader(cleanupPEM(pemBlock))); + PEMParser p2 = new PEMParser(new StringReader(unifyIndent(pemBlock))); JcaX509CertificateConverter certconv = new JcaX509CertificateConverter().setProvider("BC"); while(true) { Object o2 = p2.readObject(); @@ -115,7 +104,7 @@ public List parseCertificates(String pemBlock) throws IOExcepti } public Key getPrivateKey(String pemBlock) throws IOException { - PEMParser p = new PEMParser(new StringReader(cleanupPEM(pemBlock))); + PEMParser p = new PEMParser(new StringReader(unifyIndent(pemBlock))); Object o = p.readObject(); if (o == null) throw new InvalidParameterException("Could not read certificate. Expected the certificate to begin with '-----BEGIN CERTIFICATE-----'."); @@ -131,7 +120,7 @@ public Key getPrivateKey(String pemBlock) throws IOException { } public Object parseKey(String pemBlock) throws IOException { - PEMParser p = new PEMParser(new StringReader(cleanupPEM(pemBlock))); + PEMParser p = new PEMParser(new StringReader(unifyIndent(pemBlock))); Object o = p.readObject(); if (o == null) { log.error("Could not read PEM file. Check the contents of PEM file or configuration. Content is {}", pemBlock); diff --git a/core/src/main/java/com/predic8/membrane/core/util/TextUtil.java b/core/src/main/java/com/predic8/membrane/core/util/TextUtil.java index bf981bff9..45f064c12 100644 --- a/core/src/main/java/com/predic8/membrane/core/util/TextUtil.java +++ b/core/src/main/java/com/predic8/membrane/core/util/TextUtil.java @@ -181,4 +181,62 @@ public static String getLineFromMultilineString(String s,int lineNumber) { public static String escapeQuotes(String s) { return s.replace("\"", "\\\""); } + + /** + * Adjusts the indentation of each line in a multiline string to match the minimum indentation found. + * + * @param multilineString The input multiline string to process. + * @return A string with adjusted indentation. + */ + public static String unifyIndent(String multilineString) { + String[] lines = multilineString.split("\r?\n"); + return trimLines(lines, getMinIndent(lines)).toString().replaceFirst("\\s*$", ""); + } + + /** + * Trims excess indentation from each line in the input array, based on a specified minimum indent level. + * + * @param lines The array of lines to process. + * @param minIndent The minimum indent level to maintain. + * @return A StringBuilder containing lines with adjusted indentation. + */ + public static StringBuilder trimLines(String[] lines, int minIndent) { + StringBuilder result = new StringBuilder(); + for (String line : lines) { + if (!line.trim().isEmpty()) { + result.append(" ".repeat(Math.max(getCurrentIndent(line) - minIndent, 0))).append(line.trim()).append("\n"); + } else { + result.append("\n"); + } + } + return result; + } + + /** + * Calculates the current indentation level (number of leading spaces) of a given line. + * + * @param line The line to calculate the indentation for. + * @return The number of leading spaces in the line. + */ + public static int getCurrentIndent(String line) { + return line.length() - line.replaceFirst("^\\s+", "").length(); + } + + /** + * Determines the minimum indentation level (number of leading spaces) across all non-empty lines in an array of lines. + * + * @param lines The array of lines to analyze. + * @return The minimum indent level found among the lines. + */ + public static int getMinIndent(String[] lines) { + int minIndent = Integer.MAX_VALUE; + for (String line : lines) { + if (!line.trim().isEmpty()) { + int leadingSpaces = getCurrentIndent(line); + minIndent = Math.min(minIndent, leadingSpaces); + } + } + return minIndent; + } + } diff --git a/core/src/test/java/com/predic8/membrane/core/interceptor/templating/StaticInterceptorTest.java b/core/src/test/java/com/predic8/membrane/core/interceptor/templating/StaticInterceptorTest.java new file mode 100644 index 000000000..9edfbf074 --- /dev/null +++ b/core/src/test/java/com/predic8/membrane/core/interceptor/templating/StaticInterceptorTest.java @@ -0,0 +1,10 @@ +package com.predic8.membrane.core.interceptor.templating; + +import org.junit.jupiter.api.Test; + +import static com.predic8.membrane.core.interceptor.templating.StaticInterceptor.*; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class StaticInterceptorTest { + +} \ No newline at end of file diff --git a/core/src/test/java/com/predic8/membrane/core/util/TextUtilTest.java b/core/src/test/java/com/predic8/membrane/core/util/TextUtilTest.java index a5ca5036b..fa192966f 100644 --- a/core/src/test/java/com/predic8/membrane/core/util/TextUtilTest.java +++ b/core/src/test/java/com/predic8/membrane/core/util/TextUtilTest.java @@ -14,6 +14,7 @@ package com.predic8.membrane.core.util; import static com.predic8.membrane.core.util.TextUtil.*; +import static java.lang.Integer.MAX_VALUE; import static org.junit.jupiter.api.Assertions.*; import java.util.regex.Pattern; @@ -23,58 +24,58 @@ public class TextUtilTest { - @Test - public void testGlobToExpStarPrefixHost() { - Pattern pattern = Pattern.compile(globToRegExp("*.predic8.de")); - assertTrue(pattern.matcher("hgsjagdjhsa.predic8.de").matches()); - assertTrue(pattern.matcher("jhkj.predic8.de").matches()); - assertFalse(pattern.matcher("jhkj.predic8.com").matches()); - } - - @Test - public void testGlobToExpStarSuffixHost() { - Pattern pattern = Pattern.compile(globToRegExp("predic8.*")); - assertTrue(pattern.matcher("predic8.de").matches()); - assertTrue(pattern.matcher("predic8.com").matches()); - assertFalse(pattern.matcher("jhkj.predic8.de").matches()); - } - - @Test - public void testGlobToExpStarInfixHost() { - Pattern pattern = Pattern.compile(globToRegExp("www.*.de")); - assertTrue(pattern.matcher("www.predic8.de").matches()); - assertTrue(pattern.matcher("www.oio.de").matches()); - assertFalse(pattern.matcher("www.predic8.com").matches()); - assertFalse(pattern.matcher("www.predic8.co.uk").matches()); - assertFalse(pattern.matcher("services.predic8.de").matches()); - } - - @Test - public void testGlobToExpStarPrefixIp() { - Pattern pattern = Pattern.compile(globToRegExp("*.68.5.122")); - assertTrue(pattern.matcher("192.68.5.122").matches()); - assertFalse(pattern.matcher("192.68.5.123").matches()); - } - - @Test - public void testGlobToExpStarSuffixIp() { - Pattern pattern = Pattern.compile(globToRegExp("192.68.7.*")); - assertTrue(pattern.matcher("192.68.7.12").matches()); - assertTrue(pattern.matcher("192.68.7.4").matches()); - assertFalse(pattern.matcher("192.68.6.12").matches()); - } - - @Test - public void testGlobToExpStarInfixIp() { - Pattern pattern = Pattern.compile(globToRegExp("192.68.*.15")); - assertTrue(pattern.matcher("192.68.5.15").matches()); - assertTrue(pattern.matcher("192.68.24.15").matches()); - assertFalse(pattern.matcher("192.68.24.12").matches()); - } + @Test + void testGlobToExpStarPrefixHost() { + Pattern pattern = Pattern.compile(globToRegExp("*.predic8.de")); + assertTrue(pattern.matcher("hgsjagdjhsa.predic8.de").matches()); + assertTrue(pattern.matcher("jhkj.predic8.de").matches()); + assertFalse(pattern.matcher("jhkj.predic8.com").matches()); + } + + @Test + void testGlobToExpStarSuffixHost() { + Pattern pattern = Pattern.compile(globToRegExp("predic8.*")); + assertTrue(pattern.matcher("predic8.de").matches()); + assertTrue(pattern.matcher("predic8.com").matches()); + assertFalse(pattern.matcher("jhkj.predic8.de").matches()); + } + + @Test + void testGlobToExpStarInfixHost() { + Pattern pattern = Pattern.compile(globToRegExp("www.*.de")); + assertTrue(pattern.matcher("www.predic8.de").matches()); + assertTrue(pattern.matcher("www.oio.de").matches()); + assertFalse(pattern.matcher("www.predic8.com").matches()); + assertFalse(pattern.matcher("www.predic8.co.uk").matches()); + assertFalse(pattern.matcher("services.predic8.de").matches()); + } + + @Test + void testGlobToExpStarPrefixIp() { + Pattern pattern = Pattern.compile(globToRegExp("*.68.5.122")); + assertTrue(pattern.matcher("192.68.5.122").matches()); + assertFalse(pattern.matcher("192.68.5.123").matches()); + } + + @Test + void testGlobToExpStarSuffixIp() { + Pattern pattern = Pattern.compile(globToRegExp("192.68.7.*")); + assertTrue(pattern.matcher("192.68.7.12").matches()); + assertTrue(pattern.matcher("192.68.7.4").matches()); + assertFalse(pattern.matcher("192.68.6.12").matches()); + } + + @Test + void testGlobToExpStarInfixIp() { + Pattern pattern = Pattern.compile(globToRegExp("192.68.*.15")); + assertTrue(pattern.matcher("192.68.5.15").matches()); + assertTrue(pattern.matcher("192.68.24.15").matches()); + assertFalse(pattern.matcher("192.68.24.12").matches()); + } @Test void getLineFromMultilineStringTest() { - assertEquals("ccc ccc",TextUtil.getLineFromMultilineString(""" + assertEquals("ccc ccc", TextUtil.getLineFromMultilineString(""" aaa aaa bb bb ccc ccc @@ -82,15 +83,138 @@ void getLineFromMultilineStringTest() { """, 3)); } - @Test - void getLineFromMultilineStringOneLine() { - assertEquals("aaa aaa",TextUtil.getLineFromMultilineString(""" + @Test + void getLineFromMultilineStringOneLine() { + assertEquals("aaa aaa", TextUtil.getLineFromMultilineString(""" aaa aaa """, 1)); - } + } + + @Test + void escapeQuoteSimple() { + assertEquals("Test text with \\\" quotes", escapeQuotes("Test text with \" quotes")); + } + + + @Test + void testUnifyIndent() { + assertEquals(""" + line1 + line2 + line3""", unifyIndent(""" + line1 + line2 + line3""")); + + assertEquals(""" + line1 + line2 + line3""", unifyIndent(""" + line1 + line2 + line3""")); + + assertEquals(""" + line1 + line2 + line3""", unifyIndent(""" + line1 + line2 + line3""")); + + assertEquals(""" + line1 + + line3""", unifyIndent(""" + line1 + + line3""")); + + assertEquals(""" + + line1 + line2 + line3""", unifyIndent(""" + + line1 + line2 + line3 + """)); + + assertEquals("", unifyIndent(""" + + + """)); + + assertEquals(""" + line1 + + line2""", unifyIndent(""" + line1\r + + line2""")); + + assertEquals(""" + line1 + line2""", unifyIndent(""" + \tline1 + \tline2""")); + + assertEquals(""" + line1 + + line2""", unifyIndent(""" + line1\r + + line2""")); + + assertEquals(""" + line1 + + line2""", unifyIndent(""" + line1\r + \r + line2""")); + + } - @Test - public void escapeQuoteSimple() { - assertEquals("Test text with \\\" quotes", escapeQuotes("Test text with \" quotes")); - } + @Test + void testTrimLines() { + assertEquals("Line1\nLine2\nLine3\n", trimLines(new String[]{"Line1", "Line2", "Line3"}, 0).toString()); + assertEquals("Line1\n Line2\nLine3\n", trimLines(new String[]{" Line1", " Line2", " Line3"}, 4).toString()); + assertEquals(" Line1\nLine2\nLine3\n", trimLines(new String[]{" Line1", "\tLine2", " Line3"}, 2).toString()); + assertEquals("\n\n\n", trimLines(new String[]{" ", "\t", " "}, 2).toString()); + assertEquals("\n\n\n", trimLines(new String[]{"", "", ""}, 0).toString()); + assertEquals("Line1\nLine2\nLine3\n", trimLines(new String[]{" Line1", " Line2", " Line3"}, 6).toString()); + assertEquals("Line1\nLine2\n", trimLines(new String[]{" Line1\r", "\tLine2\r"}, 2).toString()); + assertEquals("Line1\nLine2\n", trimLines(new String[]{"\tLine1", "\tLine2"}, 1).toString()); + assertEquals("Line1\nLine2\nLine3\n", trimLines(new String[]{" Line1\r", " Line2", " Line3"}, 2).toString()); + assertEquals("Line1\n\nLine3\n", trimLines(new String[]{" Line1", "", " Line3"}, 2).toString()); + } + + @Test + void testGetCurrentIndent() { + assertEquals(0, getCurrentIndent("NoIndentation")); + assertEquals(4, getCurrentIndent(" FourSpaces")); + assertEquals(3, getCurrentIndent("\t\t\tThreeTabs")); + assertEquals(5, getCurrentIndent(" \t \tMixedSpacesAndTabs")); + assertEquals(0, getCurrentIndent("")); + assertEquals(6, getCurrentIndent(" ")); + assertEquals(3, getCurrentIndent(" \rLineWithCarriageReturn")); + assertEquals(1, getCurrentIndent("\tLineWithTab")); + assertEquals(1, getCurrentIndent("\rLineStartsWithCarriageReturn")); + } + + @Test + void testGetMinIndent() { + assertEquals(0, getMinIndent(new String[]{"Line1", "Line2", "Line3"})); + assertEquals(2, getMinIndent(new String[]{" Line1", " Line2", " Line3"})); + assertEquals(1, getMinIndent(new String[]{" Line1", "\tLine2", " Line3"})); + assertEquals(MAX_VALUE, getMinIndent(new String[]{" ", "\t", ""})); + assertEquals(0, getMinIndent(new String[]{" Line1", "", " Line2", "Line3"})); + assertEquals(MAX_VALUE, getMinIndent(new String[]{"", " ", "\t"})); + assertEquals(2, getMinIndent(new String[]{" Line1\r", " Line2"})); + assertEquals(1, getMinIndent(new String[]{"\tLine1", " Line2"})); + assertEquals(0, getMinIndent(new String[]{"Line1\r", "\tLine2", "Line3"})); + } } \ No newline at end of file