diff --git a/use-core/src/main/java/org/tzi/use/util/CollectingUserOutput.java b/use-core/src/main/java/org/tzi/use/util/CollectingUserOutput.java new file mode 100644 index 000000000..ccd86012b --- /dev/null +++ b/use-core/src/main/java/org/tzi/use/util/CollectingUserOutput.java @@ -0,0 +1,53 @@ +/* + * USE - UML based specification environment + * Copyright (C) 1999-2026 University of Bremen & University of Applied Sciences Hamburg + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package org.tzi.use.util; + +import java.util.EnumMap; +import java.util.Map; + +/** + * A {@link UserOutput} that collects all output in memory. Useful for tests and + * for capturing output before forwarding it somewhere else. + * + * @author Cansin Yildiz + * @author Claude + */ +public class CollectingUserOutput implements UserOutput { + + private final StringBuilder all = new StringBuilder(); + private final Map byLevel = new EnumMap<>(OutputLevel.class); + + @Override + public void print(OutputLevel level, String message) { + all.append(message); + byLevel.computeIfAbsent(level, l -> new StringBuilder()).append(message); + } + + /** All collected output across every level, in the order it was produced. */ + public String getOutput() { + return all.toString(); + } + + /** The collected output for a single {@code level}. */ + public String getOutput(OutputLevel level) { + StringBuilder sb = byLevel.get(level); + return sb == null ? "" : sb.toString(); + } +} diff --git a/use-core/src/main/java/org/tzi/use/util/NullUserOutput.java b/use-core/src/main/java/org/tzi/use/util/NullUserOutput.java new file mode 100644 index 000000000..ba52590be --- /dev/null +++ b/use-core/src/main/java/org/tzi/use/util/NullUserOutput.java @@ -0,0 +1,34 @@ +/* + * USE - UML based specification environment + * Copyright (C) 1999-2026 University of Bremen & University of Applied Sciences Hamburg + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package org.tzi.use.util; + +/** + * A {@link UserOutput} that discards all output. + * + * @author Cansin Yildiz + * @author Claude + */ +public final class NullUserOutput implements UserOutput { + + @Override + public void print(OutputLevel level, String message) { + // discard + } +} diff --git a/use-core/src/main/java/org/tzi/use/util/OutputLevel.java b/use-core/src/main/java/org/tzi/use/util/OutputLevel.java new file mode 100644 index 000000000..fbfe85637 --- /dev/null +++ b/use-core/src/main/java/org/tzi/use/util/OutputLevel.java @@ -0,0 +1,44 @@ +/* + * USE - UML based specification environment + * Copyright (C) 1999-2026 University of Bremen & University of Applied Sciences Hamburg + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package org.tzi.use.util; + +/** + * The level of a piece of {@link UserOutput} meant for the USE user. + * + *

This is intentionally separate from developer-facing logging (see issue + * #60): it describes what kind of user output is produced, so that + * different output targets can render or route it accordingly (e.g. errors and + * warnings to an error stream, warnings highlighted, ...).

+ * + * @author Cansin Yildiz + * @author Claude + */ +public enum OutputLevel { + /** Regular output that is the result the user asked for. */ + NORMAL, + /** Additional, informational output. */ + INFO, + /** A warning the user should be aware of. */ + WARN, + /** An error reported to the user. */ + ERROR, + /** Fine-grained trace output for following what USE is doing. */ + TRACE +} diff --git a/use-core/src/main/java/org/tzi/use/util/StreamUserOutput.java b/use-core/src/main/java/org/tzi/use/util/StreamUserOutput.java new file mode 100644 index 000000000..a97374e4f --- /dev/null +++ b/use-core/src/main/java/org/tzi/use/util/StreamUserOutput.java @@ -0,0 +1,60 @@ +/* + * USE - UML based specification environment + * Copyright (C) 1999-2026 University of Bremen & University of Applied Sciences Hamburg + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package org.tzi.use.util; + +import java.io.PrintStream; +import java.util.Objects; + +/** + * A {@link UserOutput} that writes to {@link PrintStream}s. Warnings and errors + * are written to the error stream, everything else to the normal stream. + * + * @author Cansin Yildiz + * @author Claude + */ +public class StreamUserOutput implements UserOutput { + + private final PrintStream normalStream; + private final PrintStream errorStream; + + /** + * @param normalStream stream for {@code NORMAL}, {@code INFO} and {@code TRACE} output + * @param errorStream stream for {@code WARN} and {@code ERROR} output + */ + public StreamUserOutput(PrintStream normalStream, PrintStream errorStream) { + this.normalStream = Objects.requireNonNull(normalStream); + this.errorStream = Objects.requireNonNull(errorStream); + } + + /** Writes all levels to the same stream. */ + public StreamUserOutput(PrintStream stream) { + this(stream, stream); + } + + @Override + public void print(OutputLevel level, String message) { + streamFor(level).print(message); + } + + private PrintStream streamFor(OutputLevel level) { + return (level == OutputLevel.WARN || level == OutputLevel.ERROR) + ? errorStream : normalStream; + } +} diff --git a/use-core/src/main/java/org/tzi/use/util/UserOutput.java b/use-core/src/main/java/org/tzi/use/util/UserOutput.java new file mode 100644 index 000000000..8ee64b0fa --- /dev/null +++ b/use-core/src/main/java/org/tzi/use/util/UserOutput.java @@ -0,0 +1,96 @@ +/* + * USE - UML based specification environment + * Copyright (C) 1999-2026 University of Bremen & University of Applied Sciences Hamburg + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package org.tzi.use.util; + +/** + * Abstraction for output that is meant for the USE user, as opposed to + * developer-facing logging. + * + *

This is the foundation for unifying USE's output handling (issue #60). + * Instead of statically writing to {@code System.out}/{@code System.err} or the + * static {@link Log}, code that produces user output should be given a + * {@code UserOutput} instance. This separates user output from developer + * logging, allows multiple output targets (e.g. shell and log window) and + * allows decorated output (e.g. warnings in a different color).

+ * + *

Implementations only have to provide {@link #print(OutputLevel, String)}; + * the {@code println} and per-level convenience methods are derived from it.

+ * + * @author Cansin Yildiz + * @author Claude + */ +public interface UserOutput { + + /** + * Outputs {@code message} at the given {@code level}. This is the single + * method implementations must provide. + */ + void print(OutputLevel level, String message); + + /** Outputs {@code message} followed by a line separator at {@code level}. */ + default void println(OutputLevel level, String message) { + print(level, message + System.lineSeparator()); + } + + /** Outputs an empty line at {@code level}. */ + default void println(OutputLevel level) { + print(level, System.lineSeparator()); + } + + default void printNormal(String message) { print(OutputLevel.NORMAL, message); } + default void printlnNormal(String message) { println(OutputLevel.NORMAL, message); } + default void printlnNormal() { println(OutputLevel.NORMAL); } + + default void printInfo(String message) { print(OutputLevel.INFO, message); } + default void printlnInfo(String message) { println(OutputLevel.INFO, message); } + + default void printWarn(String message) { print(OutputLevel.WARN, message); } + default void printlnWarn(String message) { println(OutputLevel.WARN, message); } + + default void printError(String message) { print(OutputLevel.ERROR, message); } + default void printlnError(String message) { println(OutputLevel.ERROR, message); } + + default void printTrace(String message) { print(OutputLevel.TRACE, message); } + default void printlnTrace(String message) { println(OutputLevel.TRACE, message); } + + /** + * A {@code UserOutput} that discards everything. Useful where no user output + * is desired. + */ + static UserOutput getSilentOutput() { + return new NullUserOutput(); + } + + /** + * The default {@code UserOutput}, writing normal output to {@code System.out} + * and warnings/errors to {@code System.err}. + */ + static UserOutput getDefaultOutput() { + return new StreamUserOutput(System.out, System.err); + } + + /** + * A {@code UserOutput} that collects everything in memory. Useful for tests + * and for capturing output. + */ + static CollectingUserOutput createCollectingOutput() { + return new CollectingUserOutput(); + } +} diff --git a/use-core/src/test/java/org/tzi/use/util/UserOutputTest.java b/use-core/src/test/java/org/tzi/use/util/UserOutputTest.java new file mode 100644 index 000000000..9e2d3b08c --- /dev/null +++ b/use-core/src/test/java/org/tzi/use/util/UserOutputTest.java @@ -0,0 +1,112 @@ +/* + * USE - UML based specification environment + * Copyright (C) 1999-2026 University of Bremen & University of Applied Sciences Hamburg + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package org.tzi.use.util; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; + +/** + * Tests for the {@link UserOutput} abstraction (issue #60). + * + * @author Cansin Yildiz + * @author Claude + */ +public class UserOutputTest { + + private static final String NL = System.lineSeparator(); + + @Test + public void collectingCapturesAllOutputInOrder() { + CollectingUserOutput out = UserOutput.createCollectingOutput(); + out.printlnNormal("hello"); + out.printlnError("oops"); + assertEquals("hello" + NL + "oops" + NL, out.getOutput()); + } + + @Test + public void collectingSeparatesOutputByLevel() { + CollectingUserOutput out = new CollectingUserOutput(); + out.printlnNormal("n"); + out.printlnWarn("w"); + out.printlnError("e"); + assertEquals("n" + NL, out.getOutput(OutputLevel.NORMAL)); + assertEquals("w" + NL, out.getOutput(OutputLevel.WARN)); + assertEquals("e" + NL, out.getOutput(OutputLevel.ERROR)); + assertEquals("", out.getOutput(OutputLevel.INFO)); + } + + @Test + public void convenienceMethodsUseTheMatchingLevel() { + CollectingUserOutput out = new CollectingUserOutput(); + out.printInfo("i"); + out.printTrace("t"); + assertEquals("i", out.getOutput(OutputLevel.INFO)); + assertEquals("t", out.getOutput(OutputLevel.TRACE)); + } + + @Test + public void streamRoutesNormalToOutAndWarningsErrorsToErr() { + ByteArrayOutputStream outBuf = new ByteArrayOutputStream(); + ByteArrayOutputStream errBuf = new ByteArrayOutputStream(); + UserOutput out = new StreamUserOutput( + new PrintStream(outBuf, true, StandardCharsets.UTF_8), + new PrintStream(errBuf, true, StandardCharsets.UTF_8)); + + out.printNormal("N"); + out.printInfo("I"); + out.printTrace("T"); + out.printWarn("W"); + out.printError("E"); + + assertEquals("NIT", outBuf.toString(StandardCharsets.UTF_8)); + assertEquals("WE", errBuf.toString(StandardCharsets.UTF_8)); + } + + @Test + public void printlnAppendsLineSeparator() { + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + UserOutput out = new StreamUserOutput(new PrintStream(buf, true, StandardCharsets.UTF_8)); + out.printlnNormal("x"); + assertEquals("x" + NL, buf.toString(StandardCharsets.UTF_8)); + } + + @Test + public void silentOutputDiscardsEverythingWithoutError() { + UserOutput out = UserOutput.getSilentOutput(); + assertDoesNotThrow(() -> { + out.printlnNormal("ignored"); + out.printlnError("ignored"); + }); + } + + @Test + public void factoriesReturnUsableInstances() { + assertNotNull(UserOutput.getDefaultOutput()); + assertNotNull(UserOutput.getSilentOutput()); + assertNotNull(UserOutput.createCollectingOutput()); + } +}