Thursday, 26 October 2023

Trying my first JavaFX Program

So, I've looked up some websites on JavaFX as I wanted to quickly throw something nice together for drawing Geometry.

You can find my little tool on the GitHub1.

It works fine for what I needed. I wanted to be able to quickly show the difference between geometries.

The tool uses locationtech for geometry stuff, and javafx for display stuff.

It accepts WKT representations of geometry. Currently, these do not provide any coordinate systems, so you're unlucky there.

I did get the error:

Error: JavaFX runtime components are missing, and are required to run this application

But I changed my app to pull JavaFX from Maven Central and that seems to fixed the issues. JavaFX is no longer included in any JDK.

References

[1] GitHub.com - geoviewer
https://github.com/maartenl/geoviewer
JavaFX - Getting Started with JavaFX
https://openjfx.io/openjfx-docs/#maven
developer.com - Using Graphics in JavaFX
https://www.developer.com/open-source/using-graphics-in-javafx/
JavaFX - Main website
https://openjfx.io/

Thursday, 19 October 2023

Combining two collections using streams

So, I have two collections, and I wish to combine both lists somehow.

Requirements are thusly:

  • I have a collection newPersons
  • I have a collection oldPersons
  • I want a collection containing all the oldPersons but replaced (*some of) the oldPersons with the newPersons (based on id).
public record Person(Long id, String name, String surname) {
}

Using the record class above.

Solution 1.

Create a new list based on oldPersons, replace items in this new list with equivalent items in newPersons.

This solution leaves much to be desired. It's confusing and error prone.

I was looking for something better.

  public List<Person> mergeMetadata(List<Person> newPersons,
      List<Person> oldPersons) {
    var result = new ArrayList<>(oldPersons);
    newPersons.forEach(newPerson -> {
      result.stream()
          .filter(person -> person.id().equals(newPerson.id()))
          .findFirst()
          .ifPresent(result::remove);
      result.add(newPerson);
    });
    return result;
  }

Solution 2.

Sometimes getting back to our roots using for-loops can help readability.

We could try it the other way around, see if that helps.

This time we create a new list based on newPersons and add an oldPerson if it's not already in the list.

This seems a little more clear.

  public List<Person> mergeMetadata(List<Person> newPersons,
      List<Person> oldPersons) {
    var result = new ArrayList<>(newPersons);
    for (Person oldPerson : oldPersons) {
      if (result.stream().noneMatch(x -> x.id().equals(oldPerson.id()))) {
        result.add(oldPerson);
      }
    }
    return result;
  }

Solution 3.

Merge two collections into one list, by using a map.

  public List<Person> mergeMetadata(List<Person> newPersons,
      List<Person> oldPersons) {
    Map<Long, Person> result = Stream
        .concat(newPersons.stream(), oldPersons.stream())
        .collect(Collectors.toMap(Person::id, Function.identity(), (l, r) -> l));
    return new ArrayList<>(result.values());
  }

Although this solution seems to be the shortest (in code), using a Map function can be a bit daunting (it was for me) because of inherent complexity in the method call for creating it.

Still, perhaps it's just me and my inexperience with combining maps and streams.

I don't know if there's an even better way. Will keep an eye out.

Thursday, 12 October 2023

Optional Anti-Pattern

It might seem obvious, but recently I saw a colleague use Optional in a weird way.

When I see a piece of code that seems to contain about 3 or more Optional.isPresent() if checks and Optional.get() method calls, I kind of panic.

To be absolutely fair, the code wasn't ready yet, so was going to change anyways.

So, anyways, before we had Optionals, we had to deal with NULL a lot.

The way to do this was as follows:

  private boolean documentAccessAllowed() {
    Information information = session.getInformation();
    if (information != null) {
      Document document = database.getDocument(information.getDocumentnumber());
      if (document != null) {
        if (!document.getOwner().equals(user)) {
          Administrator administrator = database.getAdmininstrator(user.getName());
          if (administrator == null) {
            addErrorMessage("You are not allowed access to this document.");
            return false;
          }
        }
      }
    }
    return true;
  }

I realize this code could be refactored using "Replace Nested Conditional with Guard Clauses", but some purists frown upon multiple return statements in a method. I don't mind. But let's continue with the example as is.

Then we got Optionals, yay! But, translating the above code verbatim causes my head to explode:

  private boolean documentAccessAllowed() {
    Optional<Information> information = session.getInformation();
    if (information.isPresent()) {
      Optional<Document> document = database.getDocument(information.get().getDocumentnumber());
      if (document.isPresent()) {
        if (!document.get().getOwner().equals(user)) {
          Optional<Administrator> administrator = database.getAdmininstrator(user.getName());
          if (administrator.isEmpty()) {
            addErrorMessage("You are not allowed access to this document.");
            return false;
          }
        }
      }
    }
    return true;
  }

This code is of course hardly ideal!

A better way is to use the flatMap and map and filter functions of Optional, which seems more concise, but requires a bit of a mental adjustment.

  private boolean documentAccessAllowed() {
    Boolean allowed = session.getInformation()
        .map(Information::getDocumentnumber)
        .flatMap(database::getDocument)
        .filter(doc -> doc.getOwner().equals(user)) || database.getAdmininstrator(user.getName()).isEmpty())
        .isPresent();
    if (!allowed) {
      addErrorMessage("You are not allowed access to this document.");
    }
    return allowed;
  }

The advice of a colleague of mine is that lambdas, even slightly trivial ones, are a bit hard to read, and making it a method with a good name helps clear things up immensely, like so:

The idea here is not to make the code short (although that most often helps) but make the code very easy to read and follow.

  private boolean documentAccessAllowed(Session session, Database database, User user) {
    boolean allowed = session.getInformation()
        .map(Information::getDocumentnumber)
        .flatMap(database::getDocument)
        .filter(doc -> isOwnerOrAdministrator(database, user, doc))
        .isPresent();
    if (!allowed) {
      addErrorMessage("You are not allowed access to this document.");
    }
    return allowed;
  }

  private static boolean isOwnerOrAdministrator(Database database, User user, Document doc) {
    return isOwner(user, doc) || isAdministrator(database, user);
  }

Let me know if you have any suggestions, of which there are no doubt several, on how this can be improved in readability.

Thursday, 5 October 2023

Creating an @plantuml Javadoc Custom Taglet

I love PlantUML, easily create UML diagrams using a description instead of having to draw them myself.

One thing I didn't like was the need to have to install the library GraphViz1 everywhere, where I wanted to use PlantUML.

But imagine my surprise when I found out, that I don't need to any more.

The java library Smetana2, which apparently functions as a drop in replacement for GraphViz should work exactly the same. And it's already integrated in PlantUML!

Can things get any better?

So, once more, let me integrate it into my javadoc build workflow of my sample Java Project using maven.

I had the idea to create a Java Doclet for generating my javadoc, but I quickly realised I should be making a PlantUML Taglet instead. It's already been done, see [3], but I wanted something a little different. But I did get some good ideas from that site.

I wanted to inline the imagedata in the src attribute of the image tag, that seems nicer than separate png files.

I also wanted to use the Smetana by default.

I managed it and uploaded it on the github4.

Maven integration

Adding the custom taglet to your pom.xml in the javadoc plugin should be enough. You do need to have my custom Taglet installed in your maven .m2/repository directory though.

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-javadoc-plugin</artifactId>
  <version>3.6.0</version>
  <configuration>
    <taglet>org.taglet.plantuml.PlantumlTaglet</taglet>
    <!-- <tagletpath>/path/to/taglet.jar</tagletpath> -->
    <tagletArtifact>
      <groupId>org.taglet.plantuml</groupId>
      <artifactId>plantumltaglet</artifactId>
      <version>1.0</version>
    </tagletArtifact>
  </configuration>
</plugin>

The following is sufficient to run javadoc and generate the diagrams:

mvn javadoc:javadoc

Eating your own dogfood

It also serves as a good example of eating your own dog food5.

That's right!

I added PlantUML diagrams to the javadocs of my new PlantUML Taglet, and generated the javadoc+diagrams using my PlantUML Taglet!

/**
 * Created PlantUML Diagrams based on a plantuml description.
 * @plantuml
 * PlantumlImageDataFactory : +getImageData(plantuml: String): String
 */
public class PlantumlImageDataFactory {

Worked like a charm!

It will look like this:

The image will be as raw data in the html source code, like so:

<section class="class-description" id="class-description">
<dl class="notes">
<dt>All Implemented Interfaces:</dt>
<dd><code><a href="https://docs.oracle.com/en/java/javase/17/docs/api/jdk.javadoc/jdk/javadoc/doclet/Taglet.html" title="class or interface in jdk.javadoc.doclet" class="external-link">Taglet</a></code></dd>
</dl>
<hr>
<div class="type-signature"><span class="modifiers">public class </span><span class="element-name type-name-label">PlantumlTaglet</span>
<span class="extends-implements">extends <a href="https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/Object.html" title="class or interface in java.lang" class="external-link">Object</a>
implements <a href="https://docs.oracle.com/en/java/javase/17/docs/api/jdk.javadoc/jdk/javadoc/doclet/Taglet.html" title="class or interface in jdk.javadoc.doclet" class="external-link">Taglet</a></span></div>
<div class="block">A Taglet made by me to convert appropriate Plantuml codes into generated diagrams. Uses layout smetana instead
 of GraphViz.</div>
<dl class="notes">
<dt>See Also:</dt>
<dd>
<ul class="tag-list">
<li><a href="https://mnlipp.github.io/jdrupes-taglets/plantuml-taglet/javadoc/index.html">PlantUML Taglet</a></li>
</ul>
</dd>
<p><img alt="umldiagram" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAhMAAAC2CAIAAADsj5gHAAAAKnRFWHRjb3B5bGVmdABHZW5lcmF0ZWQgYnkgaHR0cHM6Ly9wbGFudHVtbC5jb212z..." /></p></dl>
</section>

References

[1] PlantUML - GraphViz-Dot
https://plantuml.com/graphviz-dot
[2] PlantUML - Porting GraphViz to Java
https://plantuml.com/smetana02
[3] PlantUML Taglet
https://mnlipp.github.io/jdrupes-taglets/plantuml-taglet/javadoc/index.html
[4] Github.com - PlantUML Taglet
https://github.com/maartenl/plantumltaglet
[5] Wikipedia - Eating your own dog food
https://en.wikipedia.org/wiki/Eating_your_own_dog_food