How to deploy artifact to the Maven Central Repository

It was quite a long time ago when I wrote the last blog post. Sometimes it just takes time till inspiration finds you… but do not rush forward!
Last year I promised: I will report about growing tomatoes and I keep my word.

Motivation

You have built something niche and you want to give back something to the community, right? Even if the idea (and the implementation) is revolutionary it will not be used by anyone if it is not accessible somehow. To be concrete: if we talk about a jar then this artifact must be available in the Maven Central Repository – or at least in a public repository somewhere. Since Maven Central Repository is the default repo the best is to have our world savior there.

I went through some tutorials but some of them were outdated while others worked only on Linux/Mac. Since I am more a Windows guy I had to walk sometimes in the dark. I do not say that it was difficult but this is something that no one does too frequently thus in a few years when I buy my next notebook I will just scratch my head: what have I done to make it work?

Prerequisites

I assume you have a basic understanding of

  • Windows
  • Git
  • Java
  • Maven

Steps

  1. Create your account on Github
  2. Upload your Maven project and take java-offline-geoip as an example
  3. Study the pom.xml, the important sections are
    • <version>${semver}</version>
    • <distributionManagement>
          <snapshotRepository>
              <id>ossrh</id>
              <url>https://oss.sonatype.org/content/repositories/snapshots</url>
          </snapshotRepository>
          <repository>
              <id>ossrh</id>
              <url>https://oss.sonatype.org/service/local/staging/deploy/maven2</url>
          </repository>
      </distributionManagement>
    • <scm>
          <url>https://github.com/tornaia/java-offline-geoip</url>
          <connection>scm:git:https://github.com/tornaia/java-offline-geoip.git</connection>
          <developerConnection>scm:git:git@github.com:tornaia/java-offline-geoip.git</developerConnection>
      </scm>
    • <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-source-plugin</artifactId>
          <version>${maven-source-plugin.version}</version>
          <executions>
              <execution>
                  <id>attach-sources</id>
                  <goals>
                      <goal>jar</goal>
                  </goals>
              </execution>
          </executions>
      </plugin>
    • <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-javadoc-plugin</artifactId>
          <version>${maven-javadoc-plugin.version}</version>
          <configuration>
              <additionalOptions>
                  <additionalOption>-html5</additionalOption>
              </additionalOptions>
          </configuration>
          <executions>
              <execution>
                  <id>attach-javadocs</id>
                  <goals>
                      <goal>jar</goal>
                  </goals>
              </execution>
          </executions>
      </plugin>
    • <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-gpg-plugin</artifactId>
          <version>${maven-gpg-plugin.version}</version>
          <executions>
              <execution>
                  <id>sign-artifacts</id>
                  <phase>verify</phase>
                  <goals>
                      <goal>sign</goal>
                  </goals>
              </execution>
          </executions>
      </plugin>
    • <plugin>
          <groupId>org.sonatype.plugins</groupId>
          <artifactId>nexus-staging-maven-plugin</artifactId>
          <version>${nexus-staging-maven-plugin.version}</version>
          <extensions>true</extensions>
          <configuration>
              <serverId>ossrh</serverId>
              <nexusUrl>https://oss.sonatype.org</nexusUrl>
              <autoReleaseAfterClose>true</autoReleaseAfterClose>
          </configuration>
      </plugin>
    • <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-scm-plugin</artifactId>
          <version>${maven-scm-plugin.version}</version>
          <configuration>
              <tag>${project.artifactId}-${project.version}</tag>
          </configuration>
      </plugin>
  4. Create a Sonatype account
  5. Open ticket to create your new project and take mine as an example
  6. Install GnuPG
  7. Create a new key pair with Kleopatra: add your name and email too
    • Remember for your passphrase
    • Make a backup of your key pair
    • Generate a revocation certificate – later you need to edit it manually to avoid accidental use
    • Keep these files in a very safe place
  8. Export your certificate to an OpenPGP Keyserver
    1. Configure Kleopatra by right clicking on its system tray icon
    2. Set OpenPGP Keyserver to hkp://pool.sks-keys under Directory Services
    3. Apply
    4. Open Kleopatra by a single click on the system tray icon
    5. Publish it by right clicking on the certificate and selecting Publish on server…
  9. Create/Edit ~/.m2/settings.xml
    • <settings>
        <servers>
          <server>
            <id>ossrh</id>
            <username>YOUR_SONARTYPE_ACCOUNT_NAME</username>
            <password>YOUR_SONARTYPE_ACCOUNT_PASSWORD</password>
          </server>
        </servers>
        <profiles>
          <profile>
            <id>ossrh</id>
            <activation>
              <activeByDefault>true</activeByDefault>
            </activation>
            <properties>
              <gpg.executable>gpg</gpg.executable>
            </properties>
          </profile>
        </profiles>
      </settings>
  10. Deploy your artifact: mvn clean deploy scm:tag -Dsemver=0.0.1
    • It will ask for your certificate’s passphrase
    • After you promoted your first release you need to add a comment to your “new project” ticket then central sync will be activated
    • After you successfully release, your component will be published to Maven Central Repository, typically within 10 minutes, though updates to search.maven.org can take up to two hours
    • If the version ends with -SNAPSHOT then it will go to the snapshots repository – but I know you know Maven very well! 😉

Misc

I was able to deploy snapshots but somehow I had no rights to deploy releases and encountered the following error:
you have no permissions to stage against profile with ID "62d9bff4b07028"? Get to Nexus admin...
I was not the only one with this so I guess it is a common issue but after opening a Jira ticket it just worked.

Shell overlay icon under Windows in C#/Java

Gardena Urlaubsbewässerungs-Set is up and running. It was easy to install and operate: irrigation of the garden is now automatized however not fully controlled. Fixed amount of water delivered in every 24 hours. I am off to Greece/Croatia in July – and just cannot ask anyone to come to my place every single day.  If they survive my absence then I give you a report in August – after harvesting sweet and juicy tomatoes.

Motivation

shell-overlay-icon-visual

As a developer of myCloud Desktop my mission is to overcome our competitors: Dropbox, OneDrive and Google Drive. The odds are against us but you never know: a year ago, no one thought we would get so far. Our beta period is over and the core functionality, the file synchronization is implemented. It is not perfect, we have some defects but we are on track: from week to week we release a better version of the app. To get to the next level I shared the results of this post with the PO – and he loved it since the product needs better integration to Windows and Mac OS. The app shows the current status, what kind of files are we transferring and how many transfers are pending but… overlay icons are superficially attractive and pleasing to the senses. They are still missing – but not for long.

How to do?

By using an Icon Overlay Handler we can tell Windows whether we want to use a specific icon for a file or not. I attempted to find an out-of-the-box solution and also keep the distance from other programming languages – not because I insist on Java but I did not want to add an another programming language to our already very heterogeneous project. But now I am quite sure that it is almost impossible without making our hands dirty.

Steps

  • Create a handler by implementing interface IShellIconOverlayIdentifier
  • Implement business logic in Java and call it from C#
  • Register handler as a COM Server
  • Force icon update from Java

Some trivial and irrelevant code fragments are removed from the code examples below. The complete sample project is available on GitHub: https://github.com/tornaia/blog-jshelloverlayicon

Create a handler by implementing interface IShellIconOverlayIdentifier

The documentation is clear: a specific interface must be implemented.

 [ComVisible(false)]
 [ComImport]
 [Guid("0C6C4200-C589-11D0-999A-00C04FD655E1")]
 [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
 public interface IShellIconOverlayIdentifier
 {
    [PreserveSig]
    int IsMemberOf([MarshalAs(UnmanagedType.LPWStr)] string absolutePath, [MarshalAs(UnmanagedType.U4)] int attributes);

    [PreserveSig]
    int GetOverlayInfo(IntPtr iconFileBuffer, int iconFileBufferSize, out int iconIndex, out uint flags);

    [PreserveSig]
    int GetPriority(out int priority);
 }

GetOverlayInfo: the fully qualified path of the file containing the icon overlay image.
GetPriority: priority value to the handler’s icon overlay.
IsMemberOf: to determine whether it should display a handler’s icon overlay for a particular object.

ComVisible: indicates that the managed type is visible to COM. By default it is set to true.
ComImport: marks a class as an externally implemented COM class. COM coclasses are represented in C# as classes. These classes must have the ComImport attribute associated with them.
Guid: is used to specify a universally unique identifier (UUID) for a class or an interface.
ComInterfaceType.InterfaceIsIUnknown: indicates that an interface is exposed to COM.
PreserveSig: specifies whether the native return value should be converted from an HRESULT to a .NET Framework exception.

There are four different icons in the sample project and one implementation per icon is required. Repetition is usually bad so extract the shared code into an abstract class.

 [ComVisible(false)]
 public abstract class AbstractOverlayIconHandler : IShellIconOverlayIdentifier
 {
    [ComRegisterFunction]
    public static void Register(Type type)
    {
       RegistryKey registryKey = Registry.LocalMachine.CreateSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\ShellIconOverlayIdentifiers\ " + type.Name);
       registryKey.SetValue(string.Empty, type.GUID.ToString("B").ToUpper());
       registryKey.Close();
       Shell32Utils.FileAssociationsChanged();
    }

    [ComUnregisterFunction]
    public static void Unregister(Type type)
    {
       Registry.LocalMachine.DeleteSubKeyTree(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\ShellIconOverlayIdentifiers\ " + type.Name);
       Shell32Utils.FileAssociationsChanged();
    }
 }

One concrete implementation out of the four.

 [ComVisible(true)]
 [ClassInterface(ClassInterfaceType.None)]
 [Guid("19841221-F0EE-4A04-8E8C-0D8698CD0001")]
 public class MyCloud1SyncedOverlayIcon : AbstractOverlayIconHandler
 {
    public MyCloud1SyncedOverlayIcon() : base(@"icon_synced.ico")
    {
    }

    protected override bool IsHandled(string absolutePath, int attributes)
    {
       var fileStatus = SyncRestClient.GetFileStatus(absolutePath);
       return fileStatus == FileStatus.SYNCED;
    }
 }

IsHandled is up to you. In the current implementation we ask a Spring Boot app with an HTTP request to decide. The used icon must be next to the dll. If you want to use an absolute path then modify the constructor in AbstractOverlayIconHandler class accordingly. The Guid of the concrete implementations must be unique.

The handlers must finish with their code as fast as possible: while their code is running the UI will show nothing but white and dummy icons. What if our client app is not running? In every two seconds the handlers (see method CheckSyncClientPort) check whether someone is listening on port 8080 or not. To make it more robust a 2000 ms timeout is also added to the requests. Under normal conditions 1-10 ms is more than sufficient.

Implement business logic in Java and call it from C#

REST provides interoperability between the platforms. In the current implementation the JSON responses are very simple so I was not forced to add Newtonsoft.com/Json.NET or any other JSON framework to the solution thus the C# part depends only on .NET 2.0. It was released in 2005 and it is optional for Windows XP but starting with Windows 7 it is there.

Register handler as a COM Server

It is essential to know what kind of OS are we running on: Windows version (7/8/8.1/10) and both 32 and 64-bit systems are working fine with this example but to support all the combinations 4 different binaries must be compiled and registered respectively. These eye-candy visuals will not be there at all or just will not appear for some apps if you register inappropriately. Elevated privileges or disabled UAC is also necessary to execute the commands.

shell-overlay-icon-32-64-bit-issue
64-bit explorer with overlay icons on left side and 32-bit Notepad++ without icons on right: inappropriately registered handlers

In AbstractOverlayIconHandler.cs there are two important methods: Register(Type) and Unregister(Type) with attributes ComRegisterFunction and ComUnregisterFunction.

Regasm can register dll to Windows. Windows will invoke the method with ComRegisterFunction attribute. That method will create one registry entry (per icon handler) under SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\ShellIconOverlayIdentifiers. If you have Dropbox/OneDrive/TortoiseSVN/etc installed then you will see the handlers of those other applications too. Unfortunately there is a limitation within Windows: you cannot have more than 15 active handlers at the same time. What happens when we have more? Windows will go in alphabetical order – as a consequence developers started to put spaces in front of registry keys’ name. Today OneDrive has one space and Dropbox two – our one will put four.

.NET runtime is bundled into these Windows versions. While Windows 7 comes with v2.0.50727, Windows 8/8.1/10 come with v4.0.30319. The x64 versions will contain a runtime for x86 and an another for x64.

After registration it is also important to restart explorer.exe and all the applications where we want to see the icons. Or just restart the computer.

 @echo off
 echo Register for Win8/8.1/10 x64
 %SystemRoot%\Microsoft.NET\Framework64\v4.0.30319\regasm bin\x64\Debug\JShellOverlayIconHandler.net4.x64.dll /codebase
 echo Kill all explorer.exe instances
 taskkill /F /IM explorer.exe
 echo Restart explorer.exe
 explorer.exe
 echo Done

Unregistration works in the other way: after removing the keys from the registry the explorer and other applications must be restarted. See unregister.win8.71.10.x64.cmd for the details.

Force icon update from Java

Shell32 and its SHChangeNotify method will do the job. I found Java Native Access pretty handy and only some glue code was required:

 public void refreshOverlayIcon(Path path) {
    String absolutePath = path.toFile().getAbsolutePath();
    Pointer filePointer = new NativeString(absolutePath).getPointer();
    Shell32.INSTANCE.SHChangeNotify(new WinDef.LONG(SHCNE_UPDATEITEM), new WinDef.UINT(SHCNF_PATHW), new WinDef.LPVOID(filePointer), new WinDef.LPVOID(Pointer.NULL));
 }

where SHCNE_UPDATEITEM and SHCNF_PATHW are just constants. You cannot pass directly a String reference to Windows so you have to allocate some memory first, write the String/wchar_t* there and pass just a pointer. If you are interested in the details check NativeString.java.

Traps

  • dll(s) will be locked by Windows after regasm is called, so unregister before rebuild – otherwise Visual Studio will give you an error like this: Unable to copy file “obj\x64\Debug\JShellOverlayIconHandler.dll” to “bin\x64\Debug\JShellOverlayIconHandler.dll”. The process cannot access the file ‘bin\x64\Debug\JShellOverlayIconHandler.dll’ because it is being used by another process.
  • Debugging is difficult. What helped me out in some situations is the Files.AppendAllText method.
  • Use Visual Studio 2017 instead of JetBrains’ Rider. I failed to build 32-bit dll with Rider. I am not blaming it since it is just a pre-release software at the moment. What is more: I switched to VS in 5 minutes.