Skip to content
/ mug Public

A small Java 8 library (string manipulation, BiStream, Structured Concurrency, SQL Templates)

License

Notifications You must be signed in to change notification settings

google/mug

Disclaimer: This is not an official Google product.

Mug

A small Java 8+ utilities library (javadoc), with 0 deps (Proto, BigQuery, Guava addons are in separate artifacts).

Offers:

  • Intuitive to read, powerful string manipulation (StringFormat, Substring)
    • new StringFormat("/{user}-home/{yyyy}/{mm}/{dd}").parse(path, (user, yyyy, mm, dd) -> ...)
    • String user = before(first('@')).from(email).orElseThrow();
  • Streaming pairs (BiStream)
    • Map<Instant, Long> histogram = zip(times, counts).toMap();
    • Map<K, V> combined = concat(map1, map2).toMap();
    • Map<Principal, V> keyedByPrincipal = BiStream.from(keyedByUserId).mapKeys(UserId::principal).toMap();
  • More (MoreStreams, Optionals, DateTimeFormats, ...)
    • Create Optional with a guard condition:
      • return optionally(count > 0, () -> total / count);
    • Parse any legit date/time string (without a pattern string):
      • Instant timestamp = DateTimeFormats.parseToInstant("2024-01-30 15:30:00-08")
      • DateTimeFormats.formatOf("Tue, 10 Jan 2023 10:00:00.123 America/Los_Angeles")

Installation

Maven

Add the following to pom.xml:

  <dependency>
    <groupId>com.google.mug</groupId>
    <artifactId>mug</artifactId>
    <version>8.3</version>
  </dependency>

Add mug-errorprone to your annotationProcessorPaths:

  <build>
    <pluginManagement>
      <plugins>
        <plugin>
          <artifactId>maven-compiler-plugin</artifactId>
          <configuration>
            <annotationProcessorPaths>
              <path>
                <groupId>com.google.errorprone</groupId>
                <artifactId>error_prone_core</artifactId>
                <version>2.23.0</version>
              </path>
              <path>
                <groupId>com.google.mug</groupId>
                <artifactId>mug-errorprone</artifactId>
                <version>8.3</version>
              </path>
            </annotationProcessorPaths>
          </configuration>
        </plugin>
      </plugins>
    </pluginManagement>
  </build>

Protobuf utils (javadoc):

  <dependency>
    <groupId>com.google.mug</groupId>
    <artifactId>mug-protobuf</artifactId>
    <version>8.3</version>
  </dependency>

Guava add-ons (with SafeSql, SafeQuery and GoogleSql):

  <dependency>
    <groupId>com.google.mug</groupId>
    <artifactId>mug-guava</artifactId>
    <version>8.3</version>
  </dependency>

Gradle

Add to build.gradle:

  implementation 'com.google.mug:mug:8.3'
  implementation 'com.google.mug:mug-guava:8.3'
  implementation 'com.google.mug:mug-protobuf:8.3'

StringFormat

Extracts structured data from string:

new StringFormat("/users/{user}/.{hidden_file_name}")
    .parse(filePath, (user, fileName) -> ...);
new StringFormat("{hour}:{minute}:{second}.{millis}")
    .parse(“10:26:30.748”, (hour, minute, second, millis) -> ...);

An ErrorProne check is in place to check that the number of lambda parameters and the parameter names match the format string.

This allows you to define StringFormat objects as private class constant, and safely use them many lines away.

Substring

Example 1: strip off a prefix if existent:

String httpStripped = Substring.prefix("http://").removeFrom(uri);

Example 2: strip off any scheme prefix from a uri:

String schemeStripped = Substring.upToIncluding(first("://")).removeFrom(uri);

Example 3: split a string in the format of "name=value" into name and value:

Substring.first('=').split("name=value").map((name, value) -> ...);

Example 4: replace trailing "//" with "/" :

Substring.suffix("//").replaceFrom(path, "/");

Example 5: strip off the suffix starting with a dash (-) character :

last('-').toEnd().removeFrom(str);

Example 6: extract a substring using regex :

String quoted = Substring.first(Pattern.compile("'(.*?)'"), 1)
    .from(str)
    .orElseThrow(...);

Example 7: find the substring between the first and last curly braces ({) :

String body = Substring.between(first('{'), last('}'))
    .from(source)
    .orElseThrow(...);

Stream

BiStream streams pairs of objects.

This class closely mirrors JDK Stream API (the few extra methods of "its own" are very straight-forward). If you are familiar with Jdk stream, learning curve is minimal.

Example 1: to concatenate Maps:

import static com.google.mu.util.stream.BiStream.concat;

Map<AccountId, Account> allAccounts = concat(primaryAccouunts, secondaryAccounts).toMap();

Example 2: to combine two streams:

BiStream.zip(requests, responses)
    .mapToObj(RequestAndResponseLog::new);

Example 3: to build a Map fluently:

Map<DoctorId, Patient> patientsByDoctorId = BiStream.zip(doctors, patients)
    .filter((doctor, patient) -> patient.likes(doctor))
    .mapKeys(Doctor::getId)
    .collect(toMap());

Example 4: to build Guava ImmutableListMultimap fluently:

ImmutableListMultimap<ZipCode, Address> addressesByZipCode = BiStream.from(addresses)
    .mapKeys(Address::getZipCode)
    .collect(ImmutableListMultimap::toImmutableListMultimap);

Example 5: to a Map into sub-maps:

import static com.google.mu.util.stream.BiCollectors.groupingBy;

Map<Address, PhoneNumber> phonebooks = ...;
Map<State, Map<Address, PhoneNumber>> statePhonebooks = BiStream.from(phonebooks)
    .collect(groupingBy(Address::state, Collectors::toMap))
    .toMap();

Example 6: to merge Map entries:

import static com.google.mu.util.stream.BiCollectors.toMap;
import static com.google.mu.util.stream.MoreCollectors.flatteningMaps;

Map<Account, Money> totalPayouts = projects.stream()
    .map(Project::payments)  // Stream<Map<Account, Money>>
    .collect(flatteningMaps(toMap(Money::add)));

Example 7: to apply grouping over Map entries:

import static com.google.mu.util.stream.BiCollectors.toMap;
import static com.google.mu.util.stream.MoreCollectors.flatteningMaps;
import static java.util.stream.Collectors.summingInt;

Map<EmployeeId, Integer> workerHours = projects.stream()
    .map(Project::getTaskAssignments)  // Stream<Map<Employee, Task>>
    .collect(flatteningMaps(toMap(summingInt(Task::hours))));

Example 8: to turn a Collection<Pair<K, V>> to BiStream<K, V>:

BiStream<K, V> stream = RiStream.from(pairs, Pair::getKey, Pair::getValue);

Q: Why not Map<Foo, Bar> or Multimap<Foo, Bar>?

A: Sometimes Foo and Bar are just an arbitrary pair of objects, with no key-value relationship. Or you may not trust Foo#equals() and hashCode(). Instead, drop-in replace your Stream<Pair<Foo, Bar>>/List<Pair<Foo, Bar>> with BiStream<Foo, Bar>/BiCollection<Foo, Bar> to get better readability.

Q: Why not Stream<FooAndBar>?

A: When you already have a proper domain object, sure. But you might find it cumbersome to define a bunch of FooAndBar, PatioChairAndKitchenSink one-off classes especially if the relationship between the two types is only relevant in the local code context.

Q: Why not Stream<Pair<Foo, Bar>>?

A: It's distracting to read code littered with opaque method names like getFirst() and getSecond().

Example 1: to group consecutive elements in a stream:

List<StockPrice> pricesOrderedByTime = ...;

List<List<StockPrice>> priceSequences =
    MoreStreams.groupConsecutive(
            pricesOrderedByTime.stream(), (p1, p2) -> closeEnough(p1, p2), toList())
        .collect(toList());

Example 2: to iterate over Streams in the presence of checked exceptions or control flow:

The Stream API provides forEach() to iterate over a stream, if you don't have to throw checked exceptions.

When checked exception is in the way, or if you need control flow (continue, return etc.), iterateThrough() and iterateOnce() can help. The following code uses iterateThrough() to write objects into an ObjectOutputStream, with IOException propagated:

Stream<?> stream = ...;
ObjectOutput out = ...;
iterateThrough(stream, out::writeObject);

with control flow:

for (Object obj : iterateOnce(stream)) {
  if (...) continue;
  else if (...) return;
  out.writeObject(obj);
}

Example 3: to merge maps:

interface Page {
  Map<Day, Long> getTrafficHistogram();
}

List<Page> pages = ...;

// Merge traffic histogram across all pages of the web site
Map<Day, Long> siteTrafficHistogram = pages.stream()
    .map(Page::getTrafficHistogram)
    .collect(flatteningMaps(groupingBy(day -> day, Long::sum)))
    .toMap();

Optionals

Example 1: to combine two Optional instances into a single one:

Optional<Couple> couple = Optionals.both(optionalHusband, optionalWife).map(Couple::new);

Example 2: to run code when two Optional instances are both present:

Optionals.both(findTeacher(), findStudent()).ifPresent(Teacher::teach);

Example 3: or else run a fallback code block:

static import com.google.mu.util.Optionals.ifPresent;

Optional<Teacher> teacher = findTeacher(...);
Optional<Student> student = findStudent(...);
ifPresent(teacher, student, Teacher::teach)             // teach if both present
    .or(() -> ifPresent(teacher, Teacher::workOut))     // teacher work out if present
    .or(() -> ifPresent(student, Student::doHomework))  // student do homework if present
    .orElse(() -> log("no teacher. no student"));       // or else log

Example 4: wrap a value in Optional if it exists:

static import com.google.mu.util.Optionals.optionally;

Optional<String> id = optionally(request.hasId(), request::getId);

Example 5: add an optional element to a list if present:

static import com.google.mu.util.Optionals.asSet;

names.addAll(asSet(optionalName));

All Optionals utilites propagate checked exception from the the lambda/method references.