Skip to content

Java PURL (Personalized URL) Generator encoder/decoder example

Have you ever noticed the unscubscribe url pattern when you unsubscribe from an online promotion or retail marketing campaign? It looks something like this:

www.somesite.com/unsubscribe/u/NSFAHPF/aW5aaW1pdHk

Ideally, you would want the user to click that (personalized) URL and the system should automatically unsubscribe, or perform any simple non-secure operations like registering for a poll. How would you then create a url that is unique to the user id, yet obfuscated enough to make it secure and personalized?

Here’s a Java utility class that will do encode a url and decode a url based on the java persistence id of the user. It’s based on Java’s inbuilt hash algorithm.

Of course, for a fine grain control over the URL generation, a Strategy interface could be used that can be passed to the PURLGenerator.

import java.net.URI;

public class PURLGenerator {

	// Maximum bits used by the value
	private static final int VALUE_BITS = Integer.SIZE - 1;

	// Maximum bits used by the encoded value
	private static final int ENCODED_BITS = Long.SIZE - 1;

	// Bits available for each encoding bucket
	private static final int BUCKET_BITS = ENCODED_BITS - VALUE_BITS;

	// Size of the encoding bucket
	private static final long BUCKET_SIZE = 1L << BUCKET_BITS;

	// Hash factor for encoding within the bucket
	private static final long BUCKET_HASH = (BUCKET_SIZE / 2) - 37;

	// Radix used for text encoding
	private static final int ENCODED_RADIX = 35;

	// Base URI
	private URI baseUri;

	public URI generateUri(Integer id) {
		if (id == null) {
			throw new NullPointerException();
		}

		if (id < 1) {
			throw new IllegalArgumentException("Id must be greater then zero");
		}

		// Find the base for the mapped encoding bucket
		final long valueBase = (id - 1) * BUCKET_SIZE;

		// Hash the value within the bucket
		final long valueHash = (id * BUCKET_HASH) % BUCKET_SIZE;

		// Generate the encoded value
		final long value = valueBase + valueHash;

		// Serialize encoded value as text
		final String code = Long.toString(value, ENCODED_RADIX);

		// Return the appended URI
		return this.baseUri.resolve(code);
	}

	public Integer parseUri(URI uri) {
		if (uri == null) {
			throw new NullPointerException();
		}

		if (!uri.getPath().startsWith(this.baseUri.getPath())) {
			throw new IllegalArgumentException("Invalid URI format: "
					+ uri);
		}

		// Get the serialized value
		final String code = uri.getPath().substring(
				this.baseUri.getPath().length());

		// Deserialize the encoded value
		final long value = Long.parseLong(code, ENCODED_RADIX);

		// Decode the id
		final int id = (int) ((value / BUCKET_SIZE) + 1);

		// Decode the id hash
		final long idHash = value % BUCKET_SIZE;

		// Validate the hash
		if (idHash != ((id * BUCKET_HASH) % BUCKET_SIZE)) {
			return null;
		}
		return id;
	}
}
[Slashdot] [Digg] [Reddit] [del.icio.us] [Facebook] [Technorati] [Google] [StumbleUpon]

One Comment

  1. I was curious if you ever considered changing the structure of your website?
    Its very well written; I love what youve got to say. But maybe you
    could a little more in the way of content so people could connect with it better.
    Youve got an awful lot of text for only having one or 2
    images. Maybe you could space it out better?

    Friday, March 8, 2013 at 4:02 am | Permalink

Post a Comment

Your email is never published nor shared. Required fields are marked *
*
*