/**********************************************************************************
* $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.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
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.error("URL was empty, aborting...");
return null;
}
//check if a key already exists for this url
String key = getExistingKey(url);
if(key != null) {
//log
log.info("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++;
}
//new key created so post event
if(log.isDebugEnabled()) {
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 original 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 URL mapped to this record or null if errors
*/
public String resolve(final String key) {
//first check cache
if(cache.containsKey(key)){
log.debug("Fetching url from cache for key: " + key);
return (String)cache.get(key);
}
//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.info("Request for valid record: " + key);
//post success event
postEvent(ShortenedUrlService.EVENT_GET_URL_OK, PREFIX+key, false);
//add to cache
String url = randomisedUrl.getUrl();
//SHORTURL-39 encode it, reutn null if failure
String encodedUrl = encodeUrl(url);
if(StringUtils.isBlank(encodedUrl)) {
return null;
}
log.debug("Encoded URL: " + encodedUrl);
addToCache(key, 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) {
//first check cache
if(cache.containsKey(url)){
log.debug("Fetching key from cache for URL: " + url);
return (String)cache.get(url);
}
//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;
}
/**
* 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.info("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) {
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.toURL().toString();
} catch (Exception e) {
log.debug("Error encoding url: " + rawUrl +". " + e.getClass() + ": " + e.getMessage());
}
return encodedUrl;
}
public void init() {
log.debug("Sakai RandomisedUrlService init().");
//setup cache
cache = memoryService.newCache(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;
}
}