org.sociotech.communitymashup.source.google.urlshortener.GoogleUrlShortenerSourceService.java Source code

Java tutorial

Introduction

Here is the source code for org.sociotech.communitymashup.source.google.urlshortener.GoogleUrlShortenerSourceService.java

Source

/*******************************************************************************
 * Copyright (c) 2014 Peter Lachenmaier - Cooperation Systems Center Munich (CSCM).
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * Contributors:
 *     Peter Lachenmaier - Design and initial implementation
 ******************************************************************************/
/**
 * 
 */
package org.sociotech.communitymashup.source.google.urlshortener;

import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.emf.common.notify.Notification;
import org.eclipse.emf.common.util.EList;
import org.osgi.service.log.LogService;
import org.sociotech.communitymashup.application.Property;
import org.sociotech.communitymashup.application.Source;
import org.sociotech.communitymashup.data.DataPackage;
import org.sociotech.communitymashup.data.DataSet;
import org.sociotech.communitymashup.data.WebSite;
import org.sociotech.communitymashup.data.observer.dataset.DataSetChangeObserver;
import org.sociotech.communitymashup.data.observer.dataset.DataSetChangedInterface;
import org.sociotech.communitymashup.source.authorization.OAuthAuthorizationRegistrator;
import org.sociotech.communitymashup.source.google.urlshortener.authorization.GoogleOAuth20AuthorizationServlet;
import org.sociotech.communitymashup.source.google.urlshortener.meta.GoogleUrlShortenerTags;
import org.sociotech.communitymashup.source.google.urlshortener.properties.GoogleUrlShortenerProperties;
import org.sociotech.communitymashup.source.impl.SourceServiceFacadeImpl;
import org.sociotech.communitymashup.source.properties.SourceServiceProperties;
import org.sociotech.communitymashup.util.servicetracker.HttpServiceTracker;

import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.client.googleapis.auth.oauth2.GoogleRefreshTokenRequest;
import com.google.api.client.googleapis.auth.oauth2.GoogleTokenResponse;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.services.urlshortener.Urlshortener;
import com.google.api.services.urlshortener.model.Url;
import com.google.api.services.urlshortener.model.UrlHistory;

/**
 * This is the main class of a source service that shortens the urls of web site
 * objects.
 * 
 * @author Peter Lachenmaier
 */
public class GoogleUrlShortenerSourceService extends SourceServiceFacadeImpl implements DataSetChangedInterface {

    /**
     * Observe to react on data set changes
     */
    private DataSetChangeObserver dataSetChangeObserver;

    /**
     * MetaTag needed for an website to be processed
     */
    private String neededWebSiteMetaTag;

    /**
     * The url shortener instance
     */
    private Urlshortener shortener;

    /**
     * The registrator for user base authorization
     */
    private OAuthAuthorizationRegistrator authenticationRegistrator;

    /**
     * Tracker for registering authorization servlet
     */
    private HttpServiceTracker httpServiceTracker;

    /**
     * Indicates a internal token update to self handle configuration changes.  
     */
    private boolean tokenUpdate = false;

    /**
     * Value of the client id for easier access 
     */
    private String clientID;

    /**
     * Value of the client secret for easier access 
     */
    private String clientSecret;

    /**
     * The expiration time of the access token
     */
    private Long expirationTime;

    /**
     * The refresh token
     */
    private String refreshToken;

    /**
     * Current access token
     */
    private String accessToken;

    /**
     * Configured application name
     */
    private String applicationName;

    /**
     * Whether to use history for shortening or not
     */
    private boolean useHistory;

    /**
     * The history of already shortened urls.
     */
    private Map<String, String> longToShortHistoryMap;

    /**
     * System time in millis of last api access
     */
    long lastApiAccessTime = 0;

    /**
     * Wait time in millis between two api calls
     */
    long apiWaitTime = 2000;

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.sociotech.communitymashup.source.impl.SourceServiceFacadeImpl#initialize
     * (org.sociotech.communitymashup.application.Source)
     */
    @Override
    public boolean initialize(Source configuration) {

        boolean initialized = super.initialize(configuration);

        if (initialized) {

            initialized = this.establishConnection();

            // read configuration
            neededWebSiteMetaTag = source
                    .getPropertyValue(GoogleUrlShortenerProperties.PROCESS_WEBSITE_ONLY_WITH_METATAG_PROPERTY);

            useHistory = source.isPropertyTrueElseDefault(GoogleUrlShortenerProperties.USE_HISTORY_PROPERTY,
                    GoogleUrlShortenerProperties.USE_HISTORY_DEFAULT);

            // if allowed register an authentication servlet
            initialized |= startUIAuthentication();

        }

        this.setInitialized(initialized);
        return initialized;
    }

    /**
     * Establishes a connection with google based on the user information given in the
     * configuration.
     * 
     * @return True if it is possible to use the user information for the connection, false otherwise. 
     */
    private boolean establishConnection() {

        clientID = source.getPropertyValue(GoogleUrlShortenerProperties.CLIENT_ID_PROPERTY);
        clientSecret = source.getPropertyValue(GoogleUrlShortenerProperties.CLIENT_SECRET_PROPERTY);
        accessToken = source.getPropertyValue(GoogleUrlShortenerProperties.ACCESS_TOKEN_PROPERTY);
        applicationName = source.getPropertyValueElseDefault(GoogleUrlShortenerProperties.APPLICATION_NAME_PROPERTY,
                GoogleUrlShortenerProperties.APPLICATION_NAME_DEFAULT);

        // check properties
        if (clientID == null || clientID.isEmpty()) {
            log("A valid client id is needed in the configuration specified by "
                    + GoogleUrlShortenerProperties.CLIENT_ID_PROPERTY, LogService.LOG_WARNING);
            return false;
        } else if (clientSecret == null || clientSecret.isEmpty()) {
            log("A valid client secret is needed in the configuration specified by "
                    + GoogleUrlShortenerProperties.CLIENT_SECRET_PROPERTY, LogService.LOG_WARNING);
            return false;
        } else if (accessToken == null || accessToken.isEmpty()) {
            log("A valid access token is needed in the configuration specified by "
                    + GoogleUrlShortenerProperties.ACCESS_TOKEN_PROPERTY, LogService.LOG_WARNING);
            return false;
        }

        refreshToken = source.getPropertyValueElseDefault(GoogleUrlShortenerProperties.REFRESH_TOKEN_PROPERTY,
                GoogleUrlShortenerProperties.REFRESH_TOKEN_DEFAULT);
        String accessTokenExpiration = source.getPropertyValueElseDefault(
                GoogleUrlShortenerProperties.ACCESS_TOKEN_EXPIRATION_PROPERTY,
                GoogleUrlShortenerProperties.ACCESS_TOKEN_EXPIRATION_DEFAULT);

        // try to parse expiration time
        try {
            expirationTime = new Long(accessTokenExpiration);
        } catch (Exception e) {
            // set to 0 which means expired and triggers update
            expirationTime = 0l;
        }

        // refresh token if neeeded
        refreshTokenIfNeeded();

        instantiateAuthorizedShortener();

        return true;
    }

    /**
     * Instantiates a authorized shortener.
     */
    private void instantiateAuthorizedShortener() {
        GoogleCredential credential = new GoogleCredential().setAccessToken(accessToken);
        // create shortener instance
        shortener = new Urlshortener.Builder(new NetHttpTransport(), new JacksonFactory(), credential)
                .setApplicationName(applicationName).build();
    }

    /**
     * Refreshes the access token if it expires soon
     */
    private void refreshTokenIfNeeded() {
        // is expired or expires in the next 5 minutes
        if (expirationTime - System.currentTimeMillis() < 5000) {
            // refresh token
            log("Access token is expired, so refresh it", LogService.LOG_DEBUG);

            GoogleRefreshTokenRequest tokenRequest = new GoogleRefreshTokenRequest(new NetHttpTransport(),
                    new JacksonFactory(), refreshToken, clientID, clientSecret);
            // execute request
            try {
                // indicate token update
                tokenUpdate = true;

                GoogleTokenResponse googleToken = tokenRequest.execute();

                expirationTime = System.currentTimeMillis() + 1000 * googleToken.getExpiresInSeconds();

                // store access token
                accessToken = googleToken.getAccessToken();
                source.addProperty(GoogleUrlShortenerProperties.ACCESS_TOKEN_PROPERTY, accessToken);

                // store access token expiration date
                source.addProperty(GoogleUrlShortenerProperties.ACCESS_TOKEN_EXPIRATION_PROPERTY,
                        "" + expirationTime);

                // recreate shortener instance
                instantiateAuthorizedShortener();
            } catch (IOException e) {
                log("Could not refresh access token (" + e.getMessage() + ")", LogService.LOG_WARNING);
            } finally {
                // finished token update
                tokenUpdate = false;
            }
        }
    }

    /**
     * Registers a specific google authentication servlet
     */
    private boolean startUIAuthentication() {

        if (!source.isPropertyTrue(GoogleUrlShortenerProperties.ALLOW_UI_AUTHENTICATION)) {
            return false;
        }

        // get property values from configuration
        String clientID = source.getPropertyValue(GoogleUrlShortenerProperties.CLIENT_ID_PROPERTY);
        String clientSecret = source.getPropertyValue(GoogleUrlShortenerProperties.CLIENT_SECRET_PROPERTY);

        // check properties
        if (clientID == null || clientID.isEmpty()) {
            log("A valid client id is needed in the configuration specified by "
                    + GoogleUrlShortenerProperties.CLIENT_ID_PROPERTY, LogService.LOG_WARNING);
            return false;
        } else if (clientSecret == null || clientSecret.isEmpty()) {
            log("A valid client secret is needed in the configuration specified by "
                    + GoogleUrlShortenerProperties.CLIENT_SECRET_PROPERTY, LogService.LOG_WARNING);
            return false;
        }

        // add empty property for authetication url
        source.addProperty(SourceServiceProperties.AUTHORIZATION_URL, "");
        Property authenticationUrlProperty = source.getProperty(SourceServiceProperties.AUTHORIZATION_URL);

        // create authentication registrator
        authenticationRegistrator = new OAuthAuthorizationRegistrator(new GoogleOAuth20AuthorizationServlet(source),
                authenticationUrlProperty);

        // enable it with a http service tracker
        httpServiceTracker = new HttpServiceTracker(GoogleUrlShortenerSourceBundleActivator.getContext(),
                authenticationRegistrator);
        httpServiceTracker.open();

        // TODO unregister and cleanup

        return true;
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.sociotech.communitymashup.source.impl.SourceServiceFacadeImpl#
     * enrichDataSet()
     */
    @Override
    public void enrichDataSet() {

        // let the default implementation work
        super.enrichDataSet();

        DataSet dataSet = source.getDataSet();

        // be nice and check null values
        if (dataSet == null) {
            // nothing can be done
            return;
        }

        if (shortener == null) {
            // no connection
            return;
        }

        // add qr markers to all information object having a website
        EList<WebSite> webSites = dataSet.getWebSites();

        if (webSites != null && !webSites.isEmpty()) {
            // refresh token if time was to long between initialization and enrichment
            refreshTokenIfNeeded();
            createHistory();

            for (WebSite webSite : webSites) {
                shortenUrlAndEnrichWebsite(webSite);
            }
        }

        // add adapter to get informed about new or changed items
        dataSetChangeObserver = new DataSetChangeObserver(dataSet, this);
    }

    /**
     * If enabled, the history will be loaded and localy stored.
     */
    private void createHistory() {
        if (!useHistory) {
            return;
        }

        longToShortHistoryMap = new HashMap<String, String>();
        // first run: get history an put in map
        try {
            UrlHistory history = shortener.url().list().execute();
            List<Url> shortenedUrls = history.getItems();

            while (shortenedUrls != null && !shortenedUrls.isEmpty()) {

                log("Got " + shortenedUrls.size() + " shortened urls from history", LogService.LOG_DEBUG);

                for (Url url : shortenedUrls) {
                    longToShortHistoryMap.put(url.getLongUrl(), url.getId());
                }

                if (history.getNextPageToken() != null) {
                    waitIfNeeded();
                    // load next page
                    history = shortener.url().list().setStartToken(history.getNextPageToken()).execute();
                    shortenedUrls = history.getItems();
                } else {
                    // end paging
                    shortenedUrls = null;
                    break;
                }
            }

        } catch (IOException e) {
            // no history
            log("Could not process history of shortened urls (" + e.getMessage() + ")", LogService.LOG_WARNING);
            // but continue
        }

        log("Got history with " + longToShortHistoryMap.size() + " entries." + LogService.LOG_INFO);
    }

    /**
     * Wait if otherwise two api calls will be to close
     */
    private void waitIfNeeded() {

        long diff = System.currentTimeMillis() - lastApiAccessTime - apiWaitTime;

        if (diff < 0) {
            try {
                Thread.sleep(-1 * diff);
            } catch (InterruptedException e) {
                log("Exception while waiting for next api call (" + e.getMessage() + ")", LogService.LOG_WARNING);
                // just continue
            }
        }

        // next access will happen after leaving this method
        lastApiAccessTime = System.currentTimeMillis();
    }

    /**
     * Shortens the url of the website and enriches it.
     * 
     * @param webSite Website to enrich.
     */
    private void shortenUrlAndEnrichWebsite(WebSite webSite) {
        // check if allowed
        if (neededWebSiteMetaTag != null && !webSite.hasMetaTag(neededWebSiteMetaTag)) {
            // skip
            return;
        }

        String longUrl = webSite.getAdress();
        String shortUrl = null;

        if (useHistory) {
            // lookup short url in history
            shortUrl = longToShortHistoryMap.get(longUrl);
        }

        if (shortUrl == null) {
            // second try: create short url
            try {
                // refresh token if it is expired
                refreshTokenIfNeeded();
                // be nice and wait a little bit
                waitIfNeeded();
                // shorten url
                Url shortened = shortener.url().insert(new Url().setLongUrl(longUrl)).execute();
                shortUrl = shortened.getId();
            } catch (IOException e) {
                log("Could not shorten url " + webSite.getAdress() + "(" + e.getMessage() + ")",
                        LogService.LOG_WARNING);
                return;
            }
        }

        if (shortUrl != null) {
            // and enrich website
            enrichWebSite(webSite, shortUrl);
            if (useHistory) {
                // put it into local history
                longToShortHistoryMap.put(longUrl, shortUrl);
            }
        }
    }

    /**
     * Shortens the url of the web site.
     * 
     * @param webSite
     *            The website to create a shortened url for
     * @param shortUrl The short url for the websites adress
     */
    private void enrichWebSite(WebSite webSite, String shortUrl) {
        if (webSite == null || shortUrl == null || shortUrl.isEmpty()) {
            // skip
            return;
        }
        if (neededWebSiteMetaTag != null && !webSite.hasMetaTag(neededWebSiteMetaTag)) {
            // skip
            return;
        }

        // set shortened url
        webSite.setShortenedUrl(shortUrl);
        // meta tag the web site
        webSite.metaTag(GoogleUrlShortenerTags.SHORTENED_URL);
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.sociotech.communitymashup.data.observer.dataset.DataSetChangedInterface
     * #dataSetChanged(org.eclipse.emf.common.notify.Notification)
     */
    @Override
    public void dataSetChanged(Notification notification) {
        if (notification == null) {
            return;
        }

        // new website added to the data set
        if (notification.getEventType() == Notification.ADD && notification.getNotifier() instanceof DataSet
                && notification.getNewValue() instanceof WebSite) {

            shortenUrlAndEnrichWebsite((WebSite) notification.getNewValue());
        }
        // website adress changed
        else if (notification.getEventType() == Notification.SET && notification.getNotifier() instanceof WebSite
                && notification.getFeatureID(WebSite.class) == DataPackage.WEB_SITE__ADRESS) {

            // enrich website
            shortenUrlAndEnrichWebsite((WebSite) notification.getNotifier());
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.sociotech.communitymashup.source.impl.SourceServiceFacadeImpl#stop()
     */
    @Override
    protected void stop() {
        // disconnect data set observer
        if (dataSetChangeObserver != null) {
            dataSetChangeObserver.disconnect();
        }

        // close service tracker
        if (httpServiceTracker != null) {
            httpServiceTracker.close();
        }

        // unregister authentication
        if (authenticationRegistrator != null) {
            authenticationRegistrator.unregisterAll();
        }

        super.stop();
    }

    /* (non-Javadoc)
     * @see org.sociotech.communitymashup.source.impl.SourceServiceFacadeImpl#handleProperty(org.eclipse.emf.common.notify.Notification)
     */
    @Override
    protected boolean handleProperty(Notification notification) {
        if (super.handleProperty(notification)) {
            return true;
        }

        if (notification.getNotifier() instanceof Property) {
            Property property = (Property) notification.getNotifier();
            if (property.getKey().equals(GoogleUrlShortenerProperties.ACCESS_TOKEN_PROPERTY) && tokenUpdate) {
                // internal token update is handled
                return true;
            }

            if (property.getKey().equals(GoogleUrlShortenerProperties.ACCESS_TOKEN_EXPIRATION_PROPERTY)
                    && tokenUpdate) {
                // internal token update is handled
                return true;
            }

            if (property.getKey().equals(GoogleUrlShortenerProperties.REFRESH_TOKEN_PROPERTY) && tokenUpdate) {
                // internal token update is handled
                return true;
            }
        }
        return false;
    }
}