aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/com/juick/util
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/juick/util')
-rw-r--r--src/main/java/com/juick/util/DateFormatter.java57
-rw-r--r--src/main/java/com/juick/util/DateFormattersHolder.java75
-rw-r--r--src/main/java/com/juick/util/MessageUtils.java324
-rw-r--r--src/main/java/com/juick/util/PrettyTimeFormatter.java53
-rw-r--r--src/main/java/com/juick/util/StreamUtils.java38
5 files changed, 547 insertions, 0 deletions
diff --git a/src/main/java/com/juick/util/DateFormatter.java b/src/main/java/com/juick/util/DateFormatter.java
new file mode 100644
index 00000000..8f569562
--- /dev/null
+++ b/src/main/java/com/juick/util/DateFormatter.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.juick.util;
+
+import org.apache.commons.lang3.StringUtils;
+
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Locale;
+
+/**
+ * Created by aalexeev on 12/7/16.
+ */
+public class DateFormatter {
+ private final DateTimeFormatter formatter;
+
+ public DateFormatter(String pattern) {
+ formatter = DateTimeFormatter.ofPattern(pattern, Locale.ENGLISH);
+ }
+
+ public String format(final Instant ts) {
+ if (ts == null)
+ return null;
+
+ ZonedDateTime ldt = ZonedDateTime.ofInstant(ts, ZoneOffset.UTC);
+
+ return ldt.format(formatter);
+ }
+
+
+ public Instant parse(final String v) {
+ if (StringUtils.isBlank(v))
+ return null;
+
+ ZonedDateTime ldt = ZonedDateTime.parse(v, formatter);
+
+ return ldt.toInstant();
+ }
+}
diff --git a/src/main/java/com/juick/util/DateFormattersHolder.java b/src/main/java/com/juick/util/DateFormattersHolder.java
new file mode 100644
index 00000000..8292e68e
--- /dev/null
+++ b/src/main/java/com/juick/util/DateFormattersHolder.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.juick.util;
+
+/**
+ * Created by aalexeev on 12/7/16.
+ */
+public class DateFormattersHolder {
+
+ private DateFormattersHolder() {
+ throw new IllegalStateException();
+ }
+
+ private static volatile DateFormatter messageFormatter;
+
+ public static DateFormatter getMessageFormatterInstance() {
+ DateFormatter localInstance = messageFormatter;
+
+ if (localInstance == null) {
+ synchronized (DateFormatter.class) {
+ localInstance = messageFormatter;
+
+ if (localInstance == null)
+ messageFormatter = localInstance = new DateFormatter("yyyy-MM-dd HH:mm:ss");
+ }
+ }
+ return localInstance;
+ }
+
+ private static volatile DateFormatter rssFormatter;
+
+ public static DateFormatter getRssFormatterInstance() {
+ DateFormatter localInstance = rssFormatter;
+
+ if (localInstance == null) {
+ synchronized (DateFormatter.class) {
+ localInstance = rssFormatter;
+
+ if (localInstance == null)
+ rssFormatter = localInstance = new DateFormatter("EEE, d MMM yyyy HH:mm:ss");
+ }
+ }
+ return localInstance;
+ }
+
+ private static volatile DateFormatter httpDateFormatter;
+
+ public static DateFormatter getHttpDateFormatter() {
+ DateFormatter localInstance = httpDateFormatter;
+ if (localInstance == null) {
+ synchronized (DateFormatter.class) {
+ localInstance = httpDateFormatter;
+ if (localInstance == null) {
+ httpDateFormatter = localInstance = new DateFormatter("EEE, dd MMM yyyy HH:mm:ss O");
+ }
+ }
+ }
+ return localInstance;
+ }
+}
diff --git a/src/main/java/com/juick/util/MessageUtils.java b/src/main/java/com/juick/util/MessageUtils.java
new file mode 100644
index 00000000..fd357c32
--- /dev/null
+++ b/src/main/java/com/juick/util/MessageUtils.java
@@ -0,0 +1,324 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.juick.util;
+
+import com.juick.Message;
+import com.juick.Tag;
+import com.juick.User;
+import org.apache.commons.codec.CharEncoding;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+/**
+ * Created by aalexeev on 11/13/16.
+ */
+public class MessageUtils {
+ private MessageUtils() {
+ throw new IllegalStateException();
+ }
+
+ public static String formatQuote(final String quote) {
+ String result = quote;
+
+ if (quote != null) {
+ if (quote.length() > 50) {
+ result = ">" + quote.substring(0, 47).replace('\n', ' ') + "...\n";
+ } else if (!quote.isEmpty()) {
+ result = ">" + quote.replace('\n', ' ') + "\n";
+ }
+ }
+
+ return result;
+ }
+
+ private final static String urlWhiteSpacePrefix = "((?<=\\s)|(?<=\\A))";
+
+ private final static String urlRegex = "((?:ht|f)tps?://(?:www\\.)?([^\\/\\s\\n\\\"]+)/?[^\\]\\s\\n\\\"\\>]*)";
+
+ private final static String urlWithWhitespacesRegex =
+ urlWhiteSpacePrefix + urlRegex;
+
+ private final static Pattern regexLinks2 = Pattern.compile("((?<=\\s)|(?<=\\A))([\\[\\{]|&lt;)((?:ht|f)tps?://(?:www\\.)?([^\\/\\s\\\"\\)\\!]+)/?(?:[^\\]\\}](?<!&gt;))*)([\\]\\}]|&gt;)");
+
+ private final static String replyNumberRegex = "((?<=\\s)|(?<=\\A))\\/(\\d+)((?=\\s)|(?=\\Z)|(?=\\p{Punct}))";
+
+ private final static String usernameRegex = "((?<=\\s)|(?<=\\A))@([\\w\\-]{2,16})((?=\\s)|(?=\\Z)|(?=\\p{Punct}))";
+ private final static Pattern usernamePattern = Pattern.compile(usernameRegex);
+
+ private final static String jidRegex = "((?<=\\s)|(?<=\\A))@([\\w\\-\\.]+@[\\w\\-\\.]+)((?=\\s)|(?=\\Z)|(?=\\p{Punct}))";
+ private final static Pattern jidPattern = Pattern.compile(jidRegex);
+
+ public static String formatMessageCode(String msg) {
+ msg = msg.replaceAll("&", "&amp;");
+ msg = msg.replaceAll("<", "&lt;");
+ msg = msg.replaceAll(">", "&gt;");
+
+ // http://juick.com/last?page=2
+ // <a href="http://juick.com/last?page=2" rel="nofollow">http://juick.com/last?page=2</a>
+ msg = msg.replaceAll(urlWithWhitespacesRegex, "$1<a href=\"$2\" rel=\"nofollow\">$2</a>");
+
+ // (http://juick.com/last?page=2)
+ // (<a href="http://juick.com/last?page=2" rel="nofollow">http://juick.com/last?page=2</a>)
+ Matcher m = regexLinks2.matcher(msg);
+ StringBuffer sb = new StringBuffer();
+ while (m.find()) {
+ String url = m.group(3).replace(" ", "%20").replaceAll("\\s+", StringUtils.EMPTY);
+ m.appendReplacement(sb, "$1$2<a href=\"" + url + "\" rel=\"nofollow\">" + url + "</a>$5");
+ }
+ m.appendTail(sb);
+ msg = sb.toString();
+
+ return "<pre>" + msg + "</pre>";
+ }
+
+ public static String formatMessage(String msg) {
+ msg = msg.replaceAll("&", "&amp;");
+ msg = msg.replaceAll("<", "&lt;");
+ msg = msg.replaceAll(">", "&gt;");
+
+ // --
+ // &mdash;
+ msg = msg.replaceAll("((?<=\\s)|(?<=\\A))\\-\\-?((?=\\s)|(?=\\Z))", "$1&mdash;$2");
+
+ // http://juick.com/last?page=2
+ // <a href="http://juick.com/last?page=2" rel="nofollow">juick.com</a>
+ msg = msg.replaceAll(urlWithWhitespacesRegex, "$1<a href=\"$2\" rel=\"nofollow\">$3</a>");
+
+ // [link text][http://juick.com/last?page=2]
+ // <a href="http://juick.com/last?page=2" rel="nofollow">link text</a>
+ msg = msg.replaceAll("\\[([^\\]]+)\\]\\[((?:ht|f)tps?://[^\\]]+)\\]", "<a href=\"$2\" rel=\"nofollow\">$1</a>");
+ msg = msg.replaceAll("\\[([^\\]]+)\\]\\(((?:ht|f)tps?://[^\\)]+)\\)", "<a href=\"$2\" rel=\"nofollow\">$1</a>");
+
+ // #12345
+ // <a href="http://juick.com/12345">#12345</a>
+ msg = msg.replaceAll("((?<=\\s)|(?<=\\A)|(?<=\\p{Punct}))#(\\d+)((?=\\s)|(?=\\Z)|(?=\\))|(?=\\.)|(?=\\,))", "$1<a href=\"https://juick.com/m/$2\">#$2</a>$3");
+
+ // #12345/65
+ // <a href="http://juick.com/12345#65">#12345/65</a>
+ msg = msg.replaceAll("((?<=\\s)|(?<=\\A)|(?<=\\p{Punct}))#(\\d+)/(\\d+)((?=\\s)|(?=\\Z)|(?=\\p{Punct}))", "$1<a href=\"https://juick.com/m/$2#$3\">#$2/$3</a>$4");
+
+ // *bold*
+ // <b>bold</b>
+ msg = msg.replaceAll("((?<=\\s)|(?<=\\A)|(?<=\\p{Punct}))\\*([^\\*\\n<>]+)\\*((?=\\s)|(?=\\Z)|(?=\\p{Punct}))", "$1<b>$2</b>$3");
+
+ // /italic/
+ // <i>italic</i>
+ msg = msg.replaceAll("((?<=\\s)|(?<=\\A))/([^\\/\\n<>]+)/((?=\\s)|(?=\\Z)|(?=\\p{Punct}))", "$1<i>$2</i>$3");
+
+ // _underline_
+ // <span class="u">underline</span>
+ msg = msg.replaceAll("((?<=\\s)|(?<=\\A))_([^\\_\\n<>]+)_((?=\\s)|(?=\\Z)|(?=\\p{Punct}))", "$1<span class=\"u\">$2</span>$3");
+
+ // /12
+ // <a href="#12">/12</a>
+ msg = msg.replaceAll(replyNumberRegex, "$1<a href=\"#$2\">/$2</a>$3");
+
+ // @username@jabber.org
+ // <a href="http://juick.com/username@jabber.org/">@username@jabber.org</a>
+ msg = msg.replaceAll(jidRegex, "$1<a href=\"https://juick.com/$2/\">@$2</a>$3");
+
+ // @username
+ // <a href="http://juick.com/username/">@username</a>
+ msg = msg.replaceAll(usernameRegex, "$1<a href=\"https://juick.com/$2/\">@$2</a>$3");
+
+ // (http://juick.com/last?page=2)
+ // (<a href="http://juick.com/last?page=2" rel="nofollow">juick.com</a>)
+ Matcher m = regexLinks2.matcher(msg);
+ StringBuffer sb = new StringBuffer();
+ while (m.find()) {
+ String url = m.group(3).replace(" ", "%20").replaceAll("\\s+", StringUtils.EMPTY);
+ m.appendReplacement(sb, "$1$2<a href=\"" + url + "\" rel=\"nofollow\">$4</a>$5");
+ }
+ m.appendTail(sb);
+ msg = sb.toString();
+
+ // > citate
+ msg = msg.replaceAll("(?:(?<=\\n)|(?<=\\A))&gt; *(.*)?(\\n|(?=\\Z))", "<q>$1</q>");
+ msg = msg.replaceAll("</q><q>", "\n");
+
+ msg = msg.replaceAll("\n", "<br/>\n");
+ return msg;
+ }
+
+ public static String formatHtml(Message jmsg) {
+ StringBuilder sb = new StringBuilder();
+ boolean isReply = jmsg.getRid() > 0;
+ String title = isReply ? "<b>Reply by @" : "<b>@";
+ String subtitle = isReply ? "<blockquote>" + jmsg.getReplyQuote() + "</blockquote>" : "<i>" + getTagsString(jmsg) + "</i>";
+ boolean isCode = jmsg.getTags().stream().anyMatch(t -> t.getName().equals("code"));
+
+ sb.append(title).append(jmsg.getUser().getName()).append(":</b></br/>")
+ .append(subtitle).append("<br/>")
+ .append(isCode ? formatMessageCode(StringUtils.defaultString(jmsg.getText()))
+ : formatMessage(StringUtils.defaultString(jmsg.getText()))).append("<br />");
+ if (StringUtils.isNotEmpty(jmsg.getAttachmentType())) {
+ // FIXME: attachment does not serialized to xml
+ if (jmsg.getAttachment() == null) {
+ if (jmsg.getRid() > 0) {
+ sb.append(String.format("<img src=\"http://i.juick.com/photos-1024/%d-%d.%s\" />", jmsg.getMid(),
+ jmsg.getRid(), jmsg.getAttachmentType()));
+ } else {
+ sb.append(String.format("<img src=\"http://i.juick.com/photos-1024/%d.%s\" />", jmsg.getMid(),
+ jmsg.getAttachmentType()));
+ }
+ } else {
+ sb.append("<img src=\"").append(jmsg.getAttachment().getMedium().getUrl()).append("\" />");
+ }
+ }
+ return sb.toString();
+ }
+
+ public static String getMessageHashTags(final Message jmsg) {
+ StringBuilder hashtags = new StringBuilder();
+ for (Tag tag : jmsg.getTags()) {
+ hashtags.append("#").append(tag).append(" ");
+ }
+ return hashtags.toString();
+ }
+ public static String getMarkdownTags(final Message jmsg) {
+ return jmsg.getTags().stream().map(t -> String.format("[%s](http://juick.com/tag/%s)", t.getName(), percentEncode(t.getName())))
+ .collect(Collectors.joining(", "));
+ }
+
+ public static String getMarkdownUser(final User user) {
+ return String.format("[%s](https://juick.com/%s/)", user.getName(), user.getName());
+ }
+
+ // TODO: check if it is really needed
+ public static String percentEncode(final String s) {
+ String ret = StringUtils.EMPTY;
+ try {
+ ret = URLEncoder.encode(s, CharEncoding.UTF_8).replace("+", "%20").replace("*", "%2A").replace("%7E", "~");
+ } catch (UnsupportedEncodingException e) {
+ }
+ return ret;
+ }
+ public static String formatMarkdownText(final Message msg) {
+ String s = StringUtils.defaultString(msg.getText()).replaceAll(replyNumberRegex, String.format("$1[/$2](https://juick.com/m/%d#$2)$3", msg.getMid()));
+ return escapeMarkdown(s);
+ }
+ public static String escapeMarkdown(final String s) {
+ return s.replace("_", "\\_").replace("*", "\\*")
+ .replace("`", "\\`");
+ }
+ public static String attachmentUrl(final Message jmsg) {
+ if (StringUtils.isEmpty(jmsg.getAttachmentType())) {
+ return StringUtils.EMPTY;
+ }
+ // FIXME: attachment does not serialized to xml
+ if (jmsg.getAttachment() == null) {
+ if (jmsg.getRid() > 0) {
+ return String.format("http://i.juick.com/photos-1024/%d-%d.%s", jmsg.getMid(),
+ jmsg.getRid(), jmsg.getAttachmentType());
+ } else {
+ return String.format("http://i.juick.com/photos-1024/%d.%s", jmsg.getMid(),
+ jmsg.getAttachmentType());
+ }
+ } else {
+ return jmsg.getAttachment().getMedium().getUrl();
+ }
+ }
+ public static boolean replyStartsWithQuote(Message msg) {
+ return msg.getRid() > 0 && StringUtils.defaultString(msg.getText()).startsWith(">");
+ }
+ public static List<Tag> parseTags(String strTags) {
+ List<Tag> tags = new ArrayList<>();
+ if (StringUtils.isNotEmpty(strTags)) {
+ Set<Tag> tagSet = new TreeSet<>(tags);
+ for (String str : strTags.split(" ")) {
+ Tag tag = new Tag(str);
+ if (!tagSet.contains(tag)) {
+ tags.add(tag);
+ tagSet.add(tag);
+ }
+ }
+ }
+ return tags;
+ }
+ public static String getTagsString(Message msg) {
+ StringBuilder builder = new StringBuilder();
+ List<Tag> tags = msg.getTags();
+ if (!tags.isEmpty()) {
+ for (Tag Tag : tags)
+ builder.append(" *").append(Tag.getName());
+
+ if (msg.FriendsOnly)
+ builder.append(" *friends");
+
+ if (msg.getPrivacy() == -2)
+ builder.append(" *private");
+ else if (msg.getPrivacy() == -1)
+ builder.append(" *friends");
+ else if (msg.getPrivacy() == 2)
+ builder.append(" *public");
+
+ if (msg.ReadOnly)
+ builder.append(" *readonly");
+ }
+ return builder.toString();
+ }
+ public static boolean isPM(Message message) {
+ return message.getMid() == 0;
+ }
+ public static boolean isReply(Message message) {
+ return message.getRid() > 0;
+ }
+
+ public static String stripNonSafeUrls(String input) {
+ // strip login urls
+ try {
+ Matcher urlMatcher = Pattern.compile(MessageUtils.urlRegex).matcher(input);
+ while (urlMatcher.find()) {
+ URI uri = URI.create(urlMatcher.group(0));
+ if (uri.getHost().equals("juick.com")) {
+ UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUri(uri);
+ uriComponentsBuilder.replaceQueryParam("hash");
+ input = input.replace(urlMatcher.group(0), uriComponentsBuilder.build().toUriString());
+ }
+ }
+ } catch (IllegalArgumentException | NullPointerException e) {
+ return input;
+ }
+ return input;
+ }
+ private static List<String> collectMatches(Pattern pattern, String input) {
+ Matcher matcher = pattern.matcher(input);
+ List<String> result = new ArrayList<>();
+ while (matcher.find()) {
+ result.add(matcher.group());
+ }
+ return result;
+ }
+ public static List<String> getMentions(Message msg) {
+ return collectMatches(usernamePattern, msg.getText());
+ }
+ public static List<String> getGlobalMentions(Message msg) {
+ return collectMatches(jidPattern, msg.getText());
+ }
+}
diff --git a/src/main/java/com/juick/util/PrettyTimeFormatter.java b/src/main/java/com/juick/util/PrettyTimeFormatter.java
new file mode 100644
index 00000000..383f4d9a
--- /dev/null
+++ b/src/main/java/com/juick/util/PrettyTimeFormatter.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.juick.util;
+
+import org.ocpsoft.prettytime.PrettyTime;
+
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Created by vitalyster on 04.05.2017.
+ */
+public class PrettyTimeFormatter {
+ private static final int MAX_CACHE_SIZE = 20;
+
+ // Cache PrettyTime per locale. LRU cache to prevent memory leak.
+ private static final Map<Locale, PrettyTime> PRETTY_TIME_LOCALE_MAP =
+ new LinkedHashMap<Locale, PrettyTime>(MAX_CACHE_SIZE + 1, 1.1F, true)
+ {
+ @Override
+ protected boolean removeEldestEntry(Map.Entry<Locale, PrettyTime> eldest)
+ {
+ return size() > MAX_CACHE_SIZE;
+ }
+ };
+
+ public String format(final Locale locale, final Date value)
+ {
+ PrettyTime prettyTime;
+
+ synchronized (PRETTY_TIME_LOCALE_MAP) {
+ prettyTime = PRETTY_TIME_LOCALE_MAP.computeIfAbsent(locale, PrettyTime::new);
+ }
+ return prettyTime.format(value);
+ }
+}
diff --git a/src/main/java/com/juick/util/StreamUtils.java b/src/main/java/com/juick/util/StreamUtils.java
new file mode 100644
index 00000000..576107af
--- /dev/null
+++ b/src/main/java/com/juick/util/StreamUtils.java
@@ -0,0 +1,38 @@
+package com.juick.util;
+
+import java.util.Spliterator;
+import java.util.Spliterators;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+/**
+ * @deprecated switch to JDK9+ Stream API when possible
+ */
+@Deprecated
+public class StreamUtils {
+ private static <T> Spliterator<T> takeWhile(
+ Spliterator<T> splitr, Predicate<? super T> predicate) {
+ return new Spliterators.AbstractSpliterator<T>(splitr.estimateSize(), 0) {
+ boolean stillGoing = true;
+ @Override public boolean tryAdvance(Consumer<? super T> consumer) {
+ if (stillGoing) {
+ boolean hadNext = splitr.tryAdvance(elem -> {
+ if (predicate.test(elem)) {
+ consumer.accept(elem);
+ } else {
+ stillGoing = false;
+ }
+ });
+ return hadNext && stillGoing;
+ }
+ return false;
+ }
+ };
+ }
+
+ public static <T> Stream<T> takeWhile(Stream<T> stream, Predicate<? super T> predicate) {
+ return StreamSupport.stream(takeWhile(stream.spliterator(), predicate), false);
+ }
+}