From b6e675de40107bd6b308f94b956bd8770172c6df Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Thu, 7 Feb 2019 14:54:22 +0100 Subject: [PATCH] Add writer with indent support This commit adds an `IndentingWriter` with a factory that supports different indenting option according to a content identifier (e.g. a language). Closes gh-812 Co-authored-by: Andy Wilkinson --- .../generator/io/IndentingWriter.java | 113 +++++++++++++++++ .../generator/io/IndentingWriterFactory.java | 115 ++++++++++++++++++ .../generator/io/SimpleIndentStrategy.java | 54 ++++++++ .../initializr/generator/io/package-info.java | 20 +++ .../io/IndentingWriterFactoryTests.java | 63 ++++++++++ .../generator/io/IndentingWriterTests.java | 91 ++++++++++++++ .../io/SimpleIndentStrategyTests.java | 70 +++++++++++ 7 files changed, 526 insertions(+) create mode 100644 initializr-generator/src/main/java/io/spring/initializr/generator/io/IndentingWriter.java create mode 100644 initializr-generator/src/main/java/io/spring/initializr/generator/io/IndentingWriterFactory.java create mode 100644 initializr-generator/src/main/java/io/spring/initializr/generator/io/SimpleIndentStrategy.java create mode 100644 initializr-generator/src/main/java/io/spring/initializr/generator/io/package-info.java create mode 100644 initializr-generator/src/test/java/io/spring/initializr/generator/io/IndentingWriterFactoryTests.java create mode 100644 initializr-generator/src/test/java/io/spring/initializr/generator/io/IndentingWriterTests.java create mode 100644 initializr-generator/src/test/java/io/spring/initializr/generator/io/SimpleIndentStrategyTests.java diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/io/IndentingWriter.java b/initializr-generator/src/main/java/io/spring/initializr/generator/io/IndentingWriter.java new file mode 100644 index 00000000..3c6f4bbf --- /dev/null +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/io/IndentingWriter.java @@ -0,0 +1,113 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.spring.initializr.generator.io; + +import java.io.IOException; +import java.io.Writer; +import java.util.function.Function; + +/** + * A {@link Writer} with support for indenting. + * + * @author Andy Wilkinson + */ +public class IndentingWriter extends Writer { + + private final Writer out; + + private final Function indentStrategy; + + private int level = 0; + + private String indent = ""; + + private boolean prependIndent = false; + + public IndentingWriter(Writer out) { + this(out, new SimpleIndentStrategy(" ")); + } + + public IndentingWriter(Writer out, Function indentStrategy) { + this.out = out; + this.indentStrategy = indentStrategy; + } + + public void print(String string) { + write(string.toCharArray(), 0, string.length()); + } + + public void println(String string) { + write(string.toCharArray(), 0, string.length()); + println(); + } + + public void println() { + String separator = System.lineSeparator(); + try { + this.out.write(separator.toCharArray(), 0, separator.length()); + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + this.prependIndent = true; + } + + public void indented(Runnable runnable) { + indent(); + runnable.run(); + outdent(); + } + + private void indent() { + this.level++; + refreshIndent(); + } + + private void outdent() { + this.level--; + refreshIndent(); + } + + private void refreshIndent() { + this.indent = this.indentStrategy.apply(this.level); + } + + @Override + public void write(char[] chars, int offset, int length) { + try { + if (this.prependIndent) { + this.out.write(this.indent.toCharArray(), 0, this.indent.length()); + this.prependIndent = false; + } + this.out.write(chars, offset, length); + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + + @Override + public void flush() throws IOException { + this.out.flush(); + } + + @Override + public void close() throws IOException { + this.out.close(); + } + +} diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/io/IndentingWriterFactory.java b/initializr-generator/src/main/java/io/spring/initializr/generator/io/IndentingWriterFactory.java new file mode 100644 index 00000000..555794a3 --- /dev/null +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/io/IndentingWriterFactory.java @@ -0,0 +1,115 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.spring.initializr.generator.io; + +import java.io.Writer; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * A factory for {@link IndentingWriter} that provides customizations according to the + * chosen content. + * + * @author Stephane Nicoll + * @see SimpleIndentStrategy + */ +public final class IndentingWriterFactory { + + private final Function defaultIndentingStrategy; + + private final Map> indentingStrategies; + + private IndentingWriterFactory(Builder builder) { + this.defaultIndentingStrategy = builder.defaultIndentingStrategy; + this.indentingStrategies = new HashMap<>(builder.indentingStrategies); + } + + /** + * Create an {@link IndentingWriter} for the specified content and output. + * @param contentId the identifier of the content + * @param out the output to use + * @return a configured {@link IndentingWriter} + */ + public IndentingWriter createIndentingWriter(String contentId, Writer out) { + Function indentingStrategy = this.indentingStrategies + .getOrDefault(contentId, this.defaultIndentingStrategy); + return new IndentingWriter(out, indentingStrategy); + } + + /** + * Create an {@link IndentingWriterFactory} with default settings. + * @return an {@link IndentingWriterFactory} with default settings + */ + public static IndentingWriterFactory withDefaultSettings() { + return create(new SimpleIndentStrategy(" ")); + } + + /** + * Create a {@link IndentingWriterFactory} with a single indenting strategy. + * @param defaultIndentingStrategy the default indenting strategy to use + * @return an {@link IndentingWriterFactory} + */ + public static IndentingWriterFactory create( + Function defaultIndentingStrategy) { + return new IndentingWriterFactory(new Builder(defaultIndentingStrategy)); + } + + /** + * Create a {@link IndentingWriterFactory}. + * @param defaultIndentingStrategy the default indenting strategy to use + * @param factory a consumer of the builder to apply further customizations + * @return an {@link IndentingWriterFactory} + */ + public static IndentingWriterFactory create( + Function defaultIndentingStrategy, + Consumer factory) { + Builder factoryBuilder = new Builder(defaultIndentingStrategy); + factory.accept(factoryBuilder); + return new IndentingWriterFactory(factoryBuilder); + } + + /** + * Settings customizer for {@link IndentingWriterFactory}. + */ + public static final class Builder { + + private final Function defaultIndentingStrategy; + + private final Map> indentingStrategies = new HashMap<>(); + + private Builder(Function defaultIndentingStrategy) { + this.defaultIndentingStrategy = defaultIndentingStrategy; + } + + /** + * Register an indenting strategy for the specified content. + * @param contentId the identifier of the content to configure + * @param indentingStrategy the indent strategy for that particular content + * @return this builder + * @see #createIndentingWriter(String, Writer) + */ + public Builder indentingStrategy(String contentId, + Function indentingStrategy) { + this.indentingStrategies.put(contentId, indentingStrategy); + return this; + } + + } + +} diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/io/SimpleIndentStrategy.java b/initializr-generator/src/main/java/io/spring/initializr/generator/io/SimpleIndentStrategy.java new file mode 100644 index 00000000..91aa5f89 --- /dev/null +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/io/SimpleIndentStrategy.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.spring.initializr.generator.io; + +import java.util.function.Function; + +import org.springframework.util.Assert; + +/** + * A simple indenting strategy that uses a configurable {@code indent} value. + * + * @author Stephane Nicoll + */ +public class SimpleIndentStrategy implements Function { + + private final String indent; + + /** + * Create a new instance with the indent style to apply. + * @param indent the indent to apply for a single level + */ + public SimpleIndentStrategy(String indent) { + Assert.notNull(indent, "Indent must be provided"); + this.indent = indent; + } + + @Override + public String apply(Integer level) { + if (level < 0) { + throw new IllegalArgumentException( + "Indent level must not be negative, got" + level); + } + StringBuilder indentBuilder = new StringBuilder(); + for (int i = 0; i < level; i++) { + indentBuilder.append(this.indent); + } + return indentBuilder.toString(); + } + +} diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/io/package-info.java b/initializr-generator/src/main/java/io/spring/initializr/generator/io/package-info.java new file mode 100644 index 00000000..422dfde8 --- /dev/null +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/io/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Support for writing project assets. + */ +package io.spring.initializr.generator.io; diff --git a/initializr-generator/src/test/java/io/spring/initializr/generator/io/IndentingWriterFactoryTests.java b/initializr-generator/src/test/java/io/spring/initializr/generator/io/IndentingWriterFactoryTests.java new file mode 100644 index 00000000..44983fa3 --- /dev/null +++ b/initializr-generator/src/test/java/io/spring/initializr/generator/io/IndentingWriterFactoryTests.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.spring.initializr.generator.io; + +import java.io.StringWriter; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link IndentingWriterFactory}. + * + * @author Stephane Nicoll + */ +class IndentingWriterFactoryTests { + + private static final SimpleIndentStrategy SPACE_STRATEGY = new SimpleIndentStrategy( + " "); + + private static final SimpleIndentStrategy TAB_STRATEGY = new SimpleIndentStrategy( + "\t"); + + private final StringWriter out = new StringWriter(); + + @Test + void createWithSingleIndentStrategy() { + IndentingWriter writer = IndentingWriterFactory.create((SPACE_STRATEGY)) + .createIndentingWriter("test", this.out); + assertThat(writer).hasFieldOrPropertyWithValue("indentStrategy", SPACE_STRATEGY); + } + + @Test + void createWithSpecializedIndentStrategy() { + SimpleIndentStrategy twoSpacesStrategy = new SimpleIndentStrategy(" "); + IndentingWriterFactory indentingWriterFactory = IndentingWriterFactory + .create(SPACE_STRATEGY, (factory) -> { + factory.indentingStrategy("java", TAB_STRATEGY); + factory.indentingStrategy("pom", twoSpacesStrategy); + }); + assertThat(indentingWriterFactory.createIndentingWriter("java", this.out)) + .hasFieldOrPropertyWithValue("indentStrategy", TAB_STRATEGY); + assertThat(indentingWriterFactory.createIndentingWriter("pom", this.out)) + .hasFieldOrPropertyWithValue("indentStrategy", twoSpacesStrategy); + assertThat(indentingWriterFactory.createIndentingWriter("c", this.out)) + .hasFieldOrPropertyWithValue("indentStrategy", SPACE_STRATEGY); + } + +} diff --git a/initializr-generator/src/test/java/io/spring/initializr/generator/io/IndentingWriterTests.java b/initializr-generator/src/test/java/io/spring/initializr/generator/io/IndentingWriterTests.java new file mode 100644 index 00000000..ec3234c3 --- /dev/null +++ b/initializr-generator/src/test/java/io/spring/initializr/generator/io/IndentingWriterTests.java @@ -0,0 +1,91 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.spring.initializr.generator.io; + +import java.io.StringWriter; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link IndentingWriter}. + * + * @author Andy Wilkinson + */ +class IndentingWriterTests { + + private final StringWriter stringWriter = new StringWriter(); + + private final IndentingWriter indentingWriter = new IndentingWriter( + this.stringWriter); + + @Test + void linesAreNotIndentedByDefault() { + this.indentingWriter.println("a"); + this.indentingWriter.println("b"); + this.indentingWriter.println("c"); + assertThat(this.stringWriter.toString()).isEqualTo("a\nb\nc\n"); + } + + @Test + void linesCanBeIndented() { + this.indentingWriter.println("a"); + this.indentingWriter.indented(() -> this.indentingWriter.println("b")); + this.indentingWriter.println("c"); + assertThat(this.stringWriter.toString()).isEqualTo("a\n b\nc\n"); + } + + @Test + void blankLinesAreNotIndented() { + this.indentingWriter.println("a"); + this.indentingWriter.indented(() -> { + this.indentingWriter.println("b"); + this.indentingWriter.println(); + }); + this.indentingWriter.println("c"); + assertThat(this.stringWriter.toString()).isEqualTo("a\n b\n\nc\n"); + } + + @Test + void useOfPrintDoesNotAddIndent() { + this.indentingWriter.println("a"); + this.indentingWriter.indented(() -> { + this.indentingWriter.print("b"); + this.indentingWriter.print("b"); + this.indentingWriter.println("b"); + }); + this.indentingWriter.println("c"); + assertThat(this.stringWriter.toString()).isEqualTo("a\n bbb\nc\n"); + } + + @Test + void customIndentStrategyIsUsed() { + IndentingWriter customIndentingWriter = new IndentingWriter(this.stringWriter, + new SimpleIndentStrategy("\t")); + customIndentingWriter.println("a"); + customIndentingWriter.indented(() -> { + customIndentingWriter.println("b"); + customIndentingWriter.indented(() -> { + customIndentingWriter.print("c"); + customIndentingWriter.println("e"); + }); + }); + assertThat(this.stringWriter.toString()).isEqualTo("a\n\tb\n\t\tce\n"); + } + +} diff --git a/initializr-generator/src/test/java/io/spring/initializr/generator/io/SimpleIndentStrategyTests.java b/initializr-generator/src/test/java/io/spring/initializr/generator/io/SimpleIndentStrategyTests.java new file mode 100644 index 00000000..df0450a8 --- /dev/null +++ b/initializr-generator/src/test/java/io/spring/initializr/generator/io/SimpleIndentStrategyTests.java @@ -0,0 +1,70 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.spring.initializr.generator.io; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link SimpleIndentStrategy}. + * + * @author Stephane Nicoll + */ +class SimpleIndentStrategyTests { + + @Test + void noLevelIsAllowed() { + assertThat(new SimpleIndentStrategy(" ").apply(0)).isEqualTo(""); + } + + @Test + void singleLevelIndentSpace() { + assertThat(new SimpleIndentStrategy(" ").apply(1)).isEqualTo(" "); + } + + @Test + void singleLevelIndentTab() { + assertThat(new SimpleIndentStrategy("\t").apply(1)).isEqualTo("\t"); + } + + @Test + void multiLevelIndentSpace() { + assertThat(new SimpleIndentStrategy(" ").apply(3)).isEqualTo(" "); + } + + @Test + void multiLevelIndentTab() { + assertThat(new SimpleIndentStrategy("\t").apply(3)).isEqualTo("\t\t\t"); + } + + @Test + void mustHaveIndent() { + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> new SimpleIndentStrategy(null)); + } + + @Test + void indentLevelMustNotBeNegative() { + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> new SimpleIndentStrategy(" ").apply(-1)) + .withMessageContaining("Indent level must not be negative") + .withMessageContaining("-1"); + } + +}