
de.larssh.utils.text.Lines Maven / Gradle / Ivy
// Generated by delombok at Fri Jul 03 00:25:34 CEST 2020
package de.larssh.utils.text;
import static com.google.common.collect.Iterators.peekingIterator;
import static java.util.stream.Collectors.toList;
import java.io.BufferedReader;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.function.BiFunction;
import java.util.function.BiPredicate;
import java.util.function.Function;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import com.google.common.collect.AbstractIterator;
import com.google.common.collect.PeekingIterator;
import de.larssh.utils.collection.Maps;
import edu.umd.cs.findbugs.annotations.Nullable;
/**
* This class contains helper methods for line based processing.
*
*
* Usage example 1: The following shows how to merge lines to logical log
* lines.
*
*
* // Some lines of example log files
* final List<String> fileLines = lines("[20:57:30] Thread 0: Start request\n"
* + "[20:57:31] Thread 0: Request ID: Example 1\n"
* + "[20:57:31] Thread 0: Request failed: SomeException\n"
* + " on line 12\n"
* + " on line 34\n"
* + "[20:57:32] Thread 0: Stop request");
*
* // Calculating logical log lines based on consecutive lines
* final Stream<List<String>> logicalLogLines = consecutive(
* fileLines,
* (lines, line) -> !line.startsWith("["));
*
* // Output logical log lines with three dashes in between each of them
* System.out.println(logicalLogLines //
* .map(lines -> lines.stream().collect(joining("\n")))
* .collect(joining("\n---\n")));
*
*
*
* Console output for usage example 1:
*
*
* [20:57:30] Thread 1: Start request
* ---
* [20:57:31] Thread 1: Request ID: Example 1
* ---
* [20:57:31] Thread 1: Request failed: SomeException
* on line 12
* on line 34
* ---
* [20:57:32] Thread 1: Stop request
*
*
*
* Usage example 2: The following shows how to group log lines based on
* their thread name.
*
*
* // Some lines of example log files
* final List<String> fileLines = lines(
* "[20:57:30] Thread 1: Start request\n"
* + "[20:57:30] Thread 2: Stop request\n"
* + "[20:57:30] Thread 1: Request ID: Example 2.1\n"
* + "[20:57:31] Thread 2: Start request\n"
* + "[20:57:31] Thread 1: Request processed\n"
* + "[20:57:31] Thread 2: Request ID: Example 2.2\n"
* + "[20:57:32] Thread 1: Stop request\n"
* + "[20:57:32] Thread 2: Request processed");
*
* // Grouping log lines based on thread names
* final Stream<Entry<String, List<String>>> groupedLogLines = grouped(
* fileLines,
* line -> line.substring(11, 19),
* (lines, line) -> {
* if (line.endsWith("Start request")) {
* return GroupedLineType.START;
* }
* if (line.endsWith("Stop request")) {
* return GroupedLineType.END;
* }
* return GroupedLineType.MIDDLE;
* });
*
* // Output grouped lines with three dashes between each group
* System.out.println(groupedLogLines
* .map(entry -> entry.getKey() + ":\n" + entry.getValue().stream().collect(joining("\n")))
* .collect(joining("\n---\n")));
*
*
*
* Console output for usage example 2:
*
*
* Thread 2:
* [20:57:30] Thread 2: Stop request
* ---
* Thread 1:
* [20:57:30] Thread 1: Start request
* [20:57:30] Thread 1: Request ID: Example 2.1
* [20:57:31] Thread 1: Request processed
* [20:57:32] Thread 1: Stop request
* ---
* Thread 2:
* [20:57:31] Thread 2: Start request
* [20:57:31] Thread 2: Request ID: Example 2.2
* [20:57:32] Thread 2: Request processed
*
*
*
* Based on your your needs you might need to combine the methods
* {@code lines(...)}, {@code consecutive(...)} and {@code grouped(...)}.
*/
public final class Lines {
/**
* Creates lists of consecutive lines based on {@code lines}. An input line is
* added to an output list whenever {@code isNextConsecutive} returns
* {@code true}. On {@code false} a new list gets created.
*
*
* Check out usage example 1 at {@link Lines}.
*
*
* Tip: {@code lines} must not necessarily consist of elements of type
* {@link String}.
*
*
* Note: This method requires {@code com.google.guava:guava} to be part
* of your dependencies!
*
* @param type of line (most probably {@link String})
* @param lines the lines
* @param isNextConsecutive when returning {@code true} the input line is added
* to an output list, else a new list gets created. The
* first argument is the previous list of lines,
* whereas the second argument is the current line.
* @return stream of lists containing consecutive lines
*/
public static Stream> consecutive(final Iterable lines, final BiPredicate, T> isNextConsecutive) {
return consecutive(StreamSupport.stream(lines.spliterator(), false), isNextConsecutive);
}
/**
* Creates lists of consecutive lines based on {@code lines}. An input line is
* added to an output list whenever {@code isNextConsecutive} returns
* {@code true}. On {@code false} a new list gets created.
*
*
* Check out usage example 1 at {@link Lines}.
*
*
* Tip: {@code lines} must not necessarily consist of elements of type
* {@link String}.
*
*
* Note: This method requires {@code com.google.guava:guava} to be part
* of your dependencies!
*
* @param type of line (most probably {@link String})
* @param lines the lines
* @param isNextConsecutive when returning {@code true} the input line is added
* to an output list, else a new list gets created. The
* first argument is the previous list of lines,
* whereas the second argument is the current line.
* @return stream of lists containing consecutive lines
*/
@SuppressWarnings("checkstyle:IllegalToken")
public static Stream> consecutive(final Iterator lines, final BiPredicate, T> isNextConsecutive) {
final PeekingIterator iterator = peekingIterator(lines);
return StreamSupport.stream(Spliterators.spliteratorUnknownSize(new AbstractIterator>() {
/** {@inheritDoc} */
@Nullable
@Override
protected List computeNext() {
if (!iterator.hasNext()) {
return endOfData();
}
final List lines = new ArrayList<>();
do {
lines.add(iterator.next());
} while (iterator.hasNext() && isNextConsecutive.test(lines, iterator.peek()));
return lines;
}
}, Spliterator.NONNULL | Spliterator.ORDERED), false);
}
/**
* Creates lists of consecutive lines based on {@code lines}. An input line is
* added to an output list whenever {@code isNextConsecutive} returns
* {@code true}. On {@code false} a new list gets created.
*
*
* Check out usage example 1 at {@link Lines}.
*
*
* Tip: {@code lines} must not necessarily consist of elements of type
* {@link String}.
*
*
* Note: This method requires {@code com.google.guava:guava} to be part
* of your dependencies!
*
* @param type of line (most probably {@link String})
* @param lines the lines
* @param isNextConsecutive when returning {@code true} the input line is added
* to an output list, else a new list gets created. The
* first argument is the previous list of lines,
* whereas the second argument is the current line.
* @return stream of lists containing consecutive lines
*/
public static Stream> consecutive(final Stream lines, final BiPredicate, T> isNextConsecutive) {
return consecutive(lines.iterator(), isNextConsecutive);
}
/**
* Groups lines based on a key and line types. While {@code getGroupKey}
* calculates the key that is used for grouping {@code getLineType} calculates
* the line type, which specifies the way groups of lines are created and
* closed.
*
*
* Check out usage example 2 at {@link Lines}.
*
*
* Tip: {@code lines} must not necessarily consist of elements of type
* {@link String}.
*
*
* Note: This method requires {@code com.google.guava:guava} to be part
* of your dependencies!
*
* @param type of the group key
* @param type of line
* @param lines the lines
* @param getGroupKey calculates the lines group key
* @param getLineType calculates the lines type to specify the way groups of
* lines are created and closed. The first argument is the
* previous list of lines, whereas the second argument is the
* current line.
* @return stream of entries with the group key as key and the grouped lines as
* value
*/
public static Stream>> grouped(final Iterable lines, final Function getGroupKey, final BiFunction, V, GroupedLineType> getLineType) {
return grouped(StreamSupport.stream(lines.spliterator(), false), getGroupKey, getLineType);
}
/**
* Groups lines based on a key and line types. While {@code getGroupKey}
* calculates the key that is used for grouping {@code getLineType} calculates
* the line type, which specifies the way groups of lines are created and
* closed.
*
*
* Check out usage example 2 at {@link Lines}.
*
*
* Tip: {@code lines} must not necessarily consist of elements of type
* {@link String}.
*
*
* Note: This method requires {@code com.google.guava:guava} to be part
* of your dependencies!
*
* @param type of the group key
* @param type of line
* @param lines the lines
* @param getGroupKey calculates the lines group key
* @param getLineType calculates the lines type to specify the way groups of
* lines are created and closed. The first argument is the
* previous list of lines, whereas the second argument is the
* current line.
* @return stream of entries with the group key as key and the grouped lines as
* value
*/
@SuppressWarnings({"checkstyle:AnonInnerLength", "checkstyle:IllegalToken"})
public static Stream>> grouped(final Iterator lines, final Function getGroupKey, final BiFunction, V, GroupedLineType> getLineType) {
return StreamSupport.stream(Spliterators.spliteratorUnknownSize(new AbstractIterator>>() {
/**
* Buffer of not-closed groups of lines
*/
private final Map> groups = new LinkedHashMap<>();
/** {@inheritDoc} */
@Nullable
@Override
protected Entry> computeNext() {
while (lines.hasNext()) {
final V line = lines.next();
final K groupKey = getGroupKey.apply(line);
List groupOfCurrentLine = groups.computeIfAbsent(groupKey, key -> new ArrayList<>());
final GroupedLineType lineType = getLineType.apply(groupOfCurrentLine, line);
if (lineType == GroupedLineType.END) {
groupOfCurrentLine.add(line);
return Maps.entry(groupKey, groups.remove(groupKey));
}
if (lineType == GroupedLineType.START && !groupOfCurrentLine.isEmpty()) {
groupOfCurrentLine = new ArrayList<>();
groupOfCurrentLine.add(line);
return Maps.entry(groupKey, groups.put(groupKey, groupOfCurrentLine));
}
groupOfCurrentLine.add(line);
}
if (groups.isEmpty()) {
return endOfData();
}
final K groupKey = groups.keySet().iterator().next();
return Maps.entry(groupKey, groups.remove(groupKey));
}
}, Spliterator.NONNULL | Spliterator.ORDERED), false);
}
/**
* Groups lines based on a key and line types. While {@code getGroupKey}
* calculates the key that is used for grouping {@code getLineType} calculates
* the line type, which specifies the way groups of lines are created and
* closed.
*
*
* Check out usage example 2 at {@link Lines}.
*
*
* Tip: {@code lines} must not necessarily consist of elements of type
* {@link String}.
*
*
* Note: This method requires {@code com.google.guava:guava} to be part
* of your dependencies!
*
* @param type of the group key
* @param type of line
* @param lines the lines
* @param getGroupKey calculates the lines group key
* @param getLineType calculates the lines type to specify the way groups of
* lines are created and closed. The first argument is the
* previous list of lines, whereas the second argument is the
* current line.
* @return stream of entries with the group key as key and the grouped lines as
* value
*/
public static Stream>> grouped(final Stream lines, final Function getGroupKey, final BiFunction, V, GroupedLineType> getLineType) {
return grouped(lines.iterator(), getGroupKey, getLineType);
}
/**
* Reads all characters of {@code reader} and splits them into lines using
* {@link BufferedReader#lines()}.
*
* @param reader character stream to split into lines
* @return list of lines
*/
public static Stream lines(final Reader reader) {
final BufferedReader bufferedReader = reader instanceof BufferedReader ? (BufferedReader) reader : new BufferedReader(reader);
return bufferedReader.lines();
}
/**
* Splits {@code value} into lines using {@link BufferedReader#lines()}.
*
* @param value value to split
* @return list of lines
*/
public static List lines(final String value) {
return lines(new StringReader(value)).collect(toList());
}
/**
* Specifies the way groups of lines are created and closed.
*/
@SuppressWarnings("PMD.UnnecessaryModifier")
public static enum GroupedLineType {
/**
* Start of a group of lines
*
*
* Creates a new group of lines. Closes a previously not-closed group, if any.
*/
START,
/**
* A grouped line, placed in a groups middle
*
*
* Adds the current line to an existing group of lines. If no group was open, a
* new group is created.
*/
MIDDLE,
/**
* End of a group of lines
*
*
* Closes an open group of lines, if any.
*/
END;
}
@java.lang.SuppressWarnings("all")
@edu.umd.cs.findbugs.annotations.SuppressFBWarnings(justification = "generated code")
@lombok.Generated
private Lines() {
throw new java.lang.UnsupportedOperationException("This is a utility class and cannot be instantiated");
}
}