Detailed changes
@@ -0,0 +1,110 @@
+package de.gultsch.common;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.util.Collections;
+import java.util.Locale;
+import java.util.Map;
+import org.checkerframework.checker.nullness.qual.NonNull;
+
+public class MiniUri {
+
+ private static final String EMPTY_STRING = "";
+
+ private final String scheme;
+ private final String authority;
+ private final String path;
+ private final Map<String, String> parameter;
+
+ public MiniUri(final String uri) {
+ final var schemeAndRest = Splitter.on(':').limit(2).splitToList(uri);
+ if (schemeAndRest.size() < 2) {
+ this.scheme = uri;
+ this.authority = null;
+ this.path = null;
+ this.parameter = Collections.emptyMap();
+ return;
+ }
+ this.scheme = schemeAndRest.get(0);
+ final var rest = schemeAndRest.get(1);
+ final var authorityPathAndQuery = Splitter.on('?').limit(2).splitToList(rest);
+ final var authorityPath = authorityPathAndQuery.get(0);
+ System.out.println("authorityPath " + authorityPath);
+ if (authorityPath.length() >= 2 && authorityPath.startsWith("//")) {
+ final var authorityPathParts =
+ Splitter.on('/').limit(2).splitToList(authorityPath.substring(2));
+ this.authority = authorityPathParts.get(0);
+ this.path = authorityPathParts.size() == 2 ? authorityPathParts.get(1) : null;
+ } else {
+ this.authority = null;
+ // TODO path ; style path components from something like geo uri
+ this.path = authorityPath;
+ }
+ if (authorityPathAndQuery.size() == 2) {
+ this.parameter = parseParameters(authorityPathAndQuery.get(1), getDelimiter(scheme));
+ } else {
+ this.parameter = Collections.emptyMap();
+ }
+ }
+
+ private static char getDelimiter(final String scheme) {
+ return switch (scheme) {
+ case "xmpp", "geo" -> ';';
+ default -> '&';
+ };
+ }
+
+ private static Map<String, String> parseParameters(final String query, final char separator) {
+ final ImmutableMap.Builder<String, String> builder = new ImmutableMap.Builder<>();
+ for (final String pair : Splitter.on(separator).split(query)) {
+ final String[] parts = pair.split("=", 2);
+ if (parts.length == 0) {
+ continue;
+ }
+ final String key = parts[0].toLowerCase(Locale.US);
+ if (parts.length == 2) {
+ try {
+ builder.put(key, URLDecoder.decode(parts[1], "UTF-8"));
+ } catch (final UnsupportedEncodingException e) {
+ builder.put(key, EMPTY_STRING);
+ }
+ } else {
+ builder.put(key, EMPTY_STRING);
+ }
+ }
+ return builder.build();
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("scheme", scheme)
+ .add("authority", authority)
+ .add("path", path)
+ .add("parameter", parameter)
+ .toString();
+ }
+
+ public String getScheme() {
+ return this.scheme;
+ }
+
+ public String getAuthority() {
+ return this.authority;
+ }
+
+ public String getPath() {
+ return Strings.isNullOrEmpty(this.path) || this.authority == null
+ ? this.path
+ : '/' + this.path;
+ }
+
+ public Map<String, String> getParameter() {
+ return this.parameter;
+ }
+}
@@ -1,4 +1,4 @@
-package eu.siacs.conversations.utils;
+package de.gultsch.common;
import java.util.regex.Pattern;
@@ -6,7 +6,7 @@ public class Patterns {
public static final Pattern URI_GENERIC =
Pattern.compile(
- "(?<=^|\\s|\\()(tel|xmpp|http|https|geo|mailto|web\\+ap):[\\p{L}\\p{M}\\p{N}\\-._~:/?#\\[\\]@!$&'*+,;=%]+(\\([\\p{L}\\p{M}\\p{N}\\-._~:/?#\\[\\]@!$&'*+,;=%]+\\))*[\\p{L}\\p{M}\\p{N}\\-._~:/?#\\[\\]@!$&'*+,;=%]*");
+ "(?<=^|\\p{Zs}|\\p{P})(tel|xmpp|http|https|geo|mailto|web\\+ap|gemini):[\\p{L}\\p{M}\\p{N}\\-._~:/?#\\[\\]@!$&'*+,;=%]+(\\([\\p{L}\\p{M}\\p{N}\\-._~:/?#\\[\\]@!$&'*+,;=%]+\\))*[\\p{L}\\p{M}\\p{N}\\-._~:/?#\\[\\]@!$&'*+,;=%]*");
public static final Pattern URI_TEL =
Pattern.compile("^tel:\\+?(\\d{1,4}[-./()\\s]?)*\\d{1,4}(;.*)?$");
@@ -8,6 +8,7 @@ import com.google.common.base.Strings;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableSet;
import com.google.common.primitives.Longs;
+import de.gultsch.common.Patterns;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
@@ -18,7 +19,6 @@ import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.Emoticons;
import eu.siacs.conversations.utils.MessageUtils;
import eu.siacs.conversations.utils.MimeUtils;
-import eu.siacs.conversations.utils.Patterns;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xmpp.Jid;
import java.lang.ref.WeakReference;
@@ -65,6 +65,7 @@ import androidx.databinding.DataBindingUtil;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
+import de.gultsch.common.Patterns;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
@@ -112,7 +113,6 @@ import eu.siacs.conversations.utils.Compatibility;
import eu.siacs.conversations.utils.GeoHelper;
import eu.siacs.conversations.utils.MessageUtils;
import eu.siacs.conversations.utils.NickValidityChecker;
-import eu.siacs.conversations.utils.Patterns;
import eu.siacs.conversations.utils.PermissionUtils;
import eu.siacs.conversations.utils.QuickLoader;
import eu.siacs.conversations.utils.StylingHelper;
@@ -38,8 +38,8 @@ import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
+import de.gultsch.common.Patterns;
import eu.siacs.conversations.ui.text.FixedURLSpan;
-import eu.siacs.conversations.utils.Patterns;
import eu.siacs.conversations.utils.XmppUri;
import java.util.Arrays;
import java.util.Collection;
@@ -5,6 +5,7 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.preference.PreferenceManager;
+import de.gultsch.common.Patterns;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversational;
@@ -1,6 +1,7 @@
package eu.siacs.conversations.utils;
import com.google.common.net.InetAddresses;
+import de.gultsch.common.Patterns;
import java.net.InetAddress;
public class IP {
@@ -21,6 +21,7 @@ import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.primitives.Ints;
+import de.gultsch.common.Patterns;
import eu.siacs.conversations.AppSettings;
import eu.siacs.conversations.BuildConfig;
import eu.siacs.conversations.Config;
@@ -49,7 +50,6 @@ import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.util.PendingItem;
import eu.siacs.conversations.utils.AccountUtils;
import eu.siacs.conversations.utils.CryptoHelper;
-import eu.siacs.conversations.utils.Patterns;
import eu.siacs.conversations.utils.PhoneHelper;
import eu.siacs.conversations.utils.Resolver;
import eu.siacs.conversations.utils.SSLSockets;
@@ -97,7 +97,6 @@ import im.conversations.android.xmpp.model.sm.StreamManagement;
import im.conversations.android.xmpp.model.stanza.Iq;
import im.conversations.android.xmpp.model.stanza.Presence;
import im.conversations.android.xmpp.model.stanza.Stanza;
-import im.conversations.android.xmpp.model.streams.Features;
import im.conversations.android.xmpp.model.streams.StreamError;
import im.conversations.android.xmpp.model.tls.Proceed;
import im.conversations.android.xmpp.model.tls.StartTls;
@@ -0,0 +1,62 @@
+package de.gultsch.common;
+
+import com.google.common.collect.ImmutableMap;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class MiniUriTest {
+
+ @Test
+ public void httpsUrl() {
+ final var miniUri = new MiniUri("https://example.com");
+ Assert.assertEquals("https", miniUri.getScheme());
+ Assert.assertEquals("example.com", miniUri.getAuthority());
+ Assert.assertNull(miniUri.getPath());
+ }
+
+ @Test
+ public void httpsUrlHtml() {
+ final var miniUri = new MiniUri("https://example.com/test.html");
+ Assert.assertEquals("https", miniUri.getScheme());
+ Assert.assertEquals("example.com", miniUri.getAuthority());
+ Assert.assertEquals("/test.html", miniUri.getPath());
+ }
+
+ @Test
+ public void httpsUrlCgiFooBar() {
+ final var miniUri = new MiniUri("https://example.com/test.cgi?foo=bar");
+ Assert.assertEquals("https", miniUri.getScheme());
+ Assert.assertEquals("example.com", miniUri.getAuthority());
+ Assert.assertEquals("/test.cgi", miniUri.getPath());
+ Assert.assertEquals(ImmutableMap.of("foo", "bar"), miniUri.getParameter());
+ }
+
+ @Test
+ public void xmppUri() {
+ final var miniUri = new MiniUri("xmpp:user@example.com");
+ Assert.assertEquals("xmpp", miniUri.getScheme());
+ Assert.assertNull(miniUri.getAuthority());
+ Assert.assertEquals("user@example.com", miniUri.getPath());
+ }
+
+ @Test
+ public void xmppUriJoin() {
+ final var miniUri = new MiniUri("xmpp:room@chat.example.com?join");
+ Assert.assertEquals("xmpp", miniUri.getScheme());
+ Assert.assertNull(miniUri.getAuthority());
+ Assert.assertEquals("room@chat.example.com", miniUri.getPath());
+ Assert.assertEquals(ImmutableMap.of("join", ""), miniUri.getParameter());
+ }
+
+ @Test
+ public void xmppUriMessage() {
+ final var miniUri =
+ new MiniUri("xmpp:romeo@montague.net?message;body=Here%27s%20a%20test%20message");
+ Assert.assertEquals("xmpp", miniUri.getScheme());
+ Assert.assertNull(miniUri.getAuthority());
+ Assert.assertEquals("romeo@montague.net", miniUri.getPath());
+ Assert.assertEquals(
+ ImmutableMap.of("message", "", "body", "Here's a test message"),
+ miniUri.getParameter());
+ }
+}
@@ -0,0 +1,94 @@
+package de.gultsch.common;
+
+import com.google.common.collect.ImmutableList;
+import java.util.Arrays;
+import java.util.regex.MatchResult;
+import java.util.stream.Collectors;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class PatternTest {
+
+ @Test
+ public void shortImMessage() {
+ final var message =
+ "Hi. I'm refactoring how URIs are linked in Conversations. We now support more URI"
+ + " schemes like mailto:user@example.com and tel:+1-269-555-0107 and obviously"
+ + " maintain support for things like"
+ + " xmpp:conversations@conference.siacs.eu?join and https://example.com however"
+ + " we no longer link domains that aren't actual URIs like example.com to avoid"
+ + " some false positives.";
+
+ final var matches =
+ Patterns.URI_GENERIC
+ .matcher(message)
+ .results()
+ .map(MatchResult::group)
+ .collect(Collectors.toList());
+
+ Assert.assertEquals(
+ Arrays.asList(
+ "mailto:user@example.com",
+ "tel:+1-269-555-0107",
+ "xmpp:conversations@conference.siacs.eu?join",
+ "https://example.com"),
+ matches);
+ }
+
+ @Test
+ public void ambiguous() {
+ final var message =
+ "Please find more information in the corresponding page on Wikipedia"
+ + " (https://en.wikipedia.org/wiki/Ambiguity_(disambiguation)). Let me know if"
+ + " you have questions!";
+ final var matches =
+ Patterns.URI_GENERIC
+ .matcher(message)
+ .results()
+ .map(MatchResult::group)
+ .collect(Collectors.toList());
+
+ Assert.assertEquals(
+ ImmutableList.of("https://en.wikipedia.org/wiki/Ambiguity_(disambiguation)"),
+ matches);
+ }
+
+ @Test
+ public void parenthesis() {
+ final var message = "Daniel is on Mastodon (https://gultsch.social/@daniel)";
+ final var matches =
+ Patterns.URI_GENERIC
+ .matcher(message)
+ .results()
+ .map(MatchResult::group)
+ .collect(Collectors.toList());
+
+ Assert.assertEquals(ImmutableList.of("https://gultsch.social/@daniel"), matches);
+ }
+
+ @Test
+ public void fullWidthSpace() {
+ final var message = "\u3000https://conversations.im";
+ final var matches =
+ Patterns.URI_GENERIC
+ .matcher(message)
+ .results()
+ .map(MatchResult::group)
+ .collect(Collectors.toList());
+
+ Assert.assertEquals(ImmutableList.of("https://conversations.im"), matches);
+ }
+
+ @Test
+ public void fullWidthColon() {
+ final var message = "\uFF1Ahttps://conversations.im";
+ final var matches =
+ Patterns.URI_GENERIC
+ .matcher(message)
+ .results()
+ .map(MatchResult::group)
+ .collect(Collectors.toList());
+
+ Assert.assertEquals(ImmutableList.of("https://conversations.im"), matches);
+ }
+}