org.sakaiproject.shortenedurl.impl.RandomisedUrlService.java Source code

Java tutorial

Introduction

Here is the source code for org.sakaiproject.shortenedurl.impl.RandomisedUrlService.java

Source

/**********************************************************************************
 * $URL: https://source.sakaiproject.org/contrib/tinyurl/trunk/impl/src/java/org/sakaiproject/tinyurl/impl/TinyUrlServiceImpl.java $
 * $Id: TinyUrlServiceImpl.java 64964 2009-12-01 00:05:12Z steve.swinsburg@gmail.com $
 ***********************************************************************************
 *
 * Copyright (c) 2007, 2008 Sakai Foundation
 *
 * Licensed under the Educational Community License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.opensource.org/licenses/ECL-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 **********************************************************************************/

package org.sakaiproject.shortenedurl.impl;

import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URL;
import java.net.URLEncoder;
import java.sql.SQLException;

import org.apache.commons.lang.RandomStringUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.Hibernate;
import org.hibernate.HibernateException;
import org.hibernate.Query;
import org.hibernate.Session;
import org.sakaiproject.component.api.ServerConfigurationService;
import org.sakaiproject.event.api.EventTrackingService;
import org.sakaiproject.memory.api.Cache;
import org.sakaiproject.memory.api.MemoryService;
import org.sakaiproject.shortenedurl.api.ShortenedUrlService;
import org.sakaiproject.shortenedurl.model.RandomisedUrl;
import org.springframework.orm.hibernate3.HibernateCallback;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;

/**
 * An implementation of {@link org.sakaiproject.shortenedurl.api.ShortenedUrlService} to provide randomised URLs
 * 
 * <p>This implementation stores the shortened key and original URL in a local database table, and uses the resolver servlet to
 * translate the key back to it's original URL.</p>
 * 
 * <p>URLs created are of the form: http://your.sakai.server/x/1w2Kb8
 * 
 * @author Steve Swinsburg (steve.swinsburg@gmail.com)
 * 
 */
public class RandomisedUrlService extends HibernateDaoSupport implements ShortenedUrlService {

    private static Log log = LogFactory.getLog(RandomisedUrlService.class);

    //Hibernate stored queries
    private static final String QUERY_GET_URL = "getUrl";
    private static final String QUERY_GET_KEY = "getKey";

    //Hibernate object fields
    private static final String KEY = "key";
    private static final String URL = "url";

    /**
     * The prefix for URLs created
     */
    public final String PREFIX = "/x/";

    /**
     * Length of a short key
     */
    public static final int SHORT = 6;

    /**
     * length of a secure key
     */
    public static final int SECURE = 22;

    private Cache cache;
    private final String CACHE_NAME = "org.sakaiproject.shortenedurl.cache";

    /**
     * Generate a randomised URL for the given URL
     * Store it and returns it or null if errors
     * 
     * <p>
     * Defaults to short mode where keys are 6 characters long. This should be sufficient for most since the authentication is handled by the container.<br />
     * If you are passing sensitive information on an unauthenticated URL, you can use {@link #shorten(String url, boolean secure)} to create a longer key.
     * </p>
     * 
     * @param url - the long URL
     * @return   the shortened URL, or null if errors.
     */
    public String shorten(String url) {

        return shorten(url, false);
    }

    /**
     * Generate a randomised URL for the given URL but with a much longer key (22 chars vs 6 chars).
     * Store it and return it or null if errors.
     * 
     * @param url - the long URL
     * @param secure - if a longer key is required.
     * @return the shortened URL, or null if errors.
     */
    public String shorten(String url, boolean secure) {

        //check values
        if (StringUtils.isBlank(url)) {
            log.warn("URL was empty, aborting...");
            return null;
        }

        //check if a key already exists for this url
        String key = getExistingKey(url);
        if (key != null) {
            //log
            log.debug("Returning existing key: " + key);

            //post event
            postEvent(ShortenedUrlService.EVENT_CREATE_EXISTS, PREFIX + key, false);

            //make actual url and return it
            return generateActualUrl(key);
        }

        //or generate a new one
        String newKey = generateKey(secure);

        //if not unique, recalculate
        int attempts = 0;
        while (!isKeyUnique(newKey)) {
            //if this is the second or greater pass through, we had a collision. log it.
            if (attempts > 0) {
                log.warn("Collision detected for record: " + newKey + " and attempt: " + attempts
                        + ". Regenerating...");
                postEvent(ShortenedUrlService.EVENT_CREATE_COLLISION, newKey, false);
            }
            newKey = generateKey(secure);
            attempts++;
        }

        log.debug("Created:" + newKey + " for URL: " + url);
        postEvent(ShortenedUrlService.EVENT_CREATE_OK, PREFIX + newKey, true);

        //save 
        if (!saveNewShortenedUrl(newKey, url)) {
            return null;
        }

        //make actual url and return it
        return generateActualUrl(newKey);
    }

    /**
     * Gets the encoded URL for the given shortened URL.
     * This is used by the RandomisedUrlService servlet to translate short URLs back into their original URLs. 
     * 
     * @param key - the key value, eg 6whjq
     * @return the original encoded URL mapped to this record or null if errors
     */
    public String resolve(final String key) {

        if (StringUtils.isBlank(key)) {
            return null;
        }

        //first check cache
        String value = (String) cache.get(key);
        if (value != null) {
            return encodeUrl(value);
        }

        //then check db
        RandomisedUrl randomisedUrl = null;

        HibernateCallback hcb = new HibernateCallback() {
            public Object doInHibernate(Session session) throws HibernateException, SQLException {
                Query q = session.getNamedQuery(QUERY_GET_URL);
                q.setParameter(KEY, key, Hibernate.STRING);
                q.setMaxResults(1);
                return q.uniqueResult();
            }
        };

        //will be either a RandomisedUrl or null
        randomisedUrl = (RandomisedUrl) getHibernateTemplate().execute(hcb);
        if (randomisedUrl == null) {
            //log
            log.warn("Request for invalid record: " + key);

            //post failure event
            postEvent(ShortenedUrlService.EVENT_GET_URL_BAD, PREFIX + key, false);

            return null;
        }

        //log
        log.debug("Request for valid record: " + key);

        //post success event
        postEvent(ShortenedUrlService.EVENT_GET_URL_OK, PREFIX + key, false);

        //add to cache
        String url = randomisedUrl.getUrl();
        addToCache(key, url);

        String encodedUrl = encodeUrl(url);
        if (StringUtils.isBlank(encodedUrl)) {
            return null;
        }

        log.debug("URL: " + encodedUrl);

        return encodedUrl;
    }

    /**
     * Checks if a key already exists for a given url, if so returns it else returns null
     * @param url
     * @return
     */
    private String getExistingKey(final String url) {

        if (StringUtils.isBlank(url)) {
            return null;
        }

        //first check cache
        String value = (String) cache.get(url);
        if (value != null) {
            return value;
        }

        //then check db
        RandomisedUrl randomisedUrl = null;

        HibernateCallback hcb = new HibernateCallback() {
            public Object doInHibernate(Session session) throws HibernateException, SQLException {
                Query q = session.getNamedQuery(QUERY_GET_KEY);
                q.setParameter(URL, url, Hibernate.STRING);
                q.setMaxResults(1);
                return q.uniqueResult();
            }
        };

        //will be either a RandomisedUrl or null
        randomisedUrl = (RandomisedUrl) getHibernateTemplate().execute(hcb);
        if (randomisedUrl == null) {
            return null;
        }

        //add to cache
        String key = randomisedUrl.getKey();
        addToCache(url, key);

        return key;
    }

    /**
     * See if we should include this in the transversal map. Do so only
     * if a mapping exists, since otherwise there can't be any references
     * to it.
     */
    public boolean shouldCopy(String url) {
        return (getExistingKey(url) != null);
    }

    /**
     * Checks if a given key is unique by checking for its existence
     * @param key
     * @return
     */
    private boolean isKeyUnique(final String key) {

        RandomisedUrl randomisedUrl = null;

        HibernateCallback hcb = new HibernateCallback() {
            public Object doInHibernate(Session session) throws HibernateException, SQLException {
                Query q = session.getNamedQuery(QUERY_GET_URL);
                q.setParameter(KEY, key, Hibernate.STRING);
                q.setMaxResults(1);
                return q.uniqueResult();
            }
        };

        //if null then it doesn't exist
        randomisedUrl = (RandomisedUrl) getHibernateTemplate().execute(hcb);
        if (randomisedUrl == null) {
            return true;
        }
        return false;
    }

    /**
     * Generate a random string
     * @param secure    if secure, makes it much longer
     * @return
     */
    private String generateKey(boolean secure) {
        if (secure) {
            return generateSecure();
        } else {
            return generateShort();
        }
    }

    /**
     * Generate a random of RandomisedUrlService.SHORT length
     * @return
     */
    private String generateShort() {
        return RandomStringUtils.random(SHORT, true, true);
    }

    /**
     * Generate a random of RandomisedUrlService.SECURE length
     * @return
     */
    private String generateSecure() {
        return RandomStringUtils.random(SECURE, true, true);
    }

    /**
     * Helper method to generate the final URL that we can use
     * @param key
     * @return
     */
    private String generateActualUrl(final String key) {

        //make actual url
        StringBuffer linkUrl = new StringBuffer();
        linkUrl.append(getServerBase());
        linkUrl.append(PREFIX);
        linkUrl.append(key);

        //return it
        return linkUrl.toString();
    }

    /**
     * Save entry
     * @param key
     * @param url
     * @return
     */
    private boolean saveNewShortenedUrl(final String key, final String url) {

        try {
            //add to db
            RandomisedUrl randomisedUrl = new RandomisedUrl(key, url);
            getHibernateTemplate().save(randomisedUrl);
            log.debug("RandomisedUrl saved as: " + key);

            //and put it in the cache, both ways
            addToCache(key, url);
            addToCache(url, key);

            return true;
        } catch (Exception e) {
            log.error("RandomisedUrl save failed. " + e.getClass() + ": " + e.getMessage());
            return false;
        }
    }

    /**
     * get server base URL from sakai.properties
     * @return
     */
    private String getServerBase() {
        return serverConfigurationService.getServerUrl();
    }

    /**
     * Post an event
     * 
     * @param event      - event id
     * @param reference   - reference for event
     * @param modify   - true if a modification event, false if not
     */
    public void postEvent(String event, String reference, boolean modify) {
        eventTrackingService.post(eventTrackingService.newEvent(event, reference, modify));
    }

    /**
     * Add data to the cache
     * @param k   key
     * @param v value
     */
    private void addToCache(String k, String v) {
        log.debug("Added entry to cache, key: " + k + ", value: " + v);
        cache.put(k, v);
    }

    /**
     * Encodes a full URL.
     * 
     * @param rawUrl the URL to encode.
     */
    private String encodeUrl(String rawUrl) {
        if (StringUtils.isBlank(rawUrl)) {
            return null;
        }
        String encodedUrl = null;

        try {
            URL url = new URL(rawUrl);
            URI uri = new URI(url.getProtocol(), url.getUserInfo(), url.getHost(), url.getPort(), url.getPath(),
                    url.getQuery(), url.getRef());
            encodedUrl = uri.toASCIIString();
        } catch (Exception e) {
            log.warn("encoding url: " + rawUrl + ", " + e.getMessage(), e);
        }

        return encodedUrl;
    }

    public void init() {
        log.debug("Sakai RandomisedUrlService init().");

        //setup cache
        cache = memoryService.getCache(CACHE_NAME);
    }

    private ServerConfigurationService serverConfigurationService;

    public void setServerConfigurationService(ServerConfigurationService serverConfigurationService) {
        this.serverConfigurationService = serverConfigurationService;
    }

    private EventTrackingService eventTrackingService;

    public void setEventTrackingService(EventTrackingService eventTrackingService) {
        this.eventTrackingService = eventTrackingService;
    }

    private MemoryService memoryService;

    public void setMemoryService(MemoryService memoryService) {
        this.memoryService = memoryService;
    }

}