com.google.cloud.backend.config.BackendConfigManager.java Source code

Java tutorial

Introduction

Here is the source code for com.google.cloud.backend.config.BackendConfigManager.java

Source

/*
 * Copyright (c) 2013 Google Inc.
 *
 * Licensed under the Apache 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.apache.org/licenses/LICENSE-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 com.google.cloud.backend.config;

import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.EntityNotFoundException;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.datastore.Text;
import com.google.appengine.api.memcache.MemcacheService;
import com.google.appengine.api.memcache.MemcacheServiceFactory;
import com.google.cloud.backend.spi.BlobEndpoint;
import com.google.cloud.backend.spi.EndpointV1;

import org.apache.commons.codec.binary.Base64;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.logging.Logger;

/**
 * Class that manages the backend configuration stored in App Engine Datastore.
 */
public class BackendConfigManager {

    /**
     * Kind name for storing backend configuration.
     */
    public static final String CONFIGURATION_ENTITY_KIND = "_BackendConfiguration";
    static final String AUTHENTICATION_MODE = "authMode";
    static final String ANDROID_CLIENT_ID = "androidClientId";
    static final String IOS_CLIENT_ID = "iOsClientId";
    static final String AUDIENCE = "audience";
    static final String PUSH_ENABLED = "pushEnabled";
    static final String ANDROID_GCM_KEY = "gCMKey";
    static final String PUSH_NOTIFICATION_CERT_PASSWORD = "pushCertPasswd";
    static final String PUSH_NOTIFICATION_CERT_BINARY = "pushCertBinary";
    static final String LAST_SUBSCRIPTION_DELETE_TIMESTAMP = "lastSubsciptionDeleteAllTime";
    static final String MEMCACHE_FILE_KEY = "memcache file key";
    static final String PKCS12_BASE64_PREFIX = "pkcs12;base64,";

    private static final String PER_APP_SECRET_KEY = "secretKey";
    private static final String CURRENT_CONFIGURATION = "Current";

    private final DatastoreService datastoreService;
    private final MemcacheService memcache;

    private final CloudEndpointsConfigManager endpointsConfigManager;
    private static final Logger log = Logger.getLogger(BackendConfigManager.class.getName());

    /**
     * Authentication Mode for the backend
     */
    public enum AuthMode {
        /**
         * All requests from the clients to APIs exposed over Cloud Endpoints should
         * be rejected
         */
        LOCKED,
        /**
         * All unauthenticated requests from the clients should be allowed. Useful
         * during early development.
         */
        OPEN,
        /**
         * Only calls from the explicitly listed clients should be allowed. The list
         * of client ids and audiences can be passed to setConfiguration() method.
         */
        CLIENT_ID
    }

    /**
     * Default constructor.
     */
    public BackendConfigManager() {
        this(DatastoreServiceFactory.getDatastoreService(), MemcacheServiceFactory.getMemcacheService());
    }

    /**
     * Constructs and instance of the class using specified DatastoreService.
     * 
     * @param datastoreService
     *          datastoreService that will be used for persistence.
     */
    public BackendConfigManager(DatastoreService datastoreService, MemcacheService memcache) {
        this.datastoreService = datastoreService;
        this.memcache = memcache;
        this.endpointsConfigManager = new CloudEndpointsConfigManager(datastoreService, memcache);
    }

    /**
     * Returns the current configuration of the backend.
     * 
     * @result Entity that represents the current configurations of the backend.
     */
    protected Entity getConfiguration() {

        // check memcache
        Key key = getKey();
        Entity config = (Entity) memcache.get(getMemKeyForConfigEntity(key));
        if (config != null) {
            return config;
        }

        // get from datastore
        try {
            config = datastoreService.get(key);
        } catch (EntityNotFoundException e) {
            // Default to the LOCKED authentication mode and disabled push
            // notification
            config = new Entity(key);
            config.setProperty(AUTHENTICATION_MODE, AuthMode.LOCKED.name());
            config.setProperty(PUSH_ENABLED, false);
            config.setProperty(LAST_SUBSCRIPTION_DELETE_TIMESTAMP, null);

            // Generate unique secret for this app to be used for XSRF token
            SecureRandom rnd = new SecureRandom();
            String secret = new BigInteger(256, rnd).toString();
            config.setProperty(PER_APP_SECRET_KEY, secret);

            datastoreService.put(config);
        }

        // put the config entity to memcache and return it
        memcache.put(getMemKeyForConfigEntity(key), config);
        return config;
    }

    protected void setConfiguration(String authMode, String androidClientId, String iOSClientId, String audience,
            boolean pushEnabled, String androidGCMKey, String pushCertPasswd, String pushCertBase64String) {
        // put config entity into Datastore and Memcache
        Key key = getKey();
        Entity configuration;
        try {
            configuration = datastoreService.get(key);
        } catch (EntityNotFoundException e) {
            throw new IllegalStateException(e);
        }
        configuration.setProperty(AUTHENTICATION_MODE, authMode);
        configuration.setProperty(ANDROID_CLIENT_ID, androidClientId);
        configuration.setProperty(IOS_CLIENT_ID, iOSClientId);
        configuration.setProperty(AUDIENCE, audience);
        configuration.setProperty(PUSH_ENABLED, pushEnabled);
        configuration.setProperty(ANDROID_GCM_KEY, androidGCMKey);
        configuration.setProperty(PUSH_NOTIFICATION_CERT_PASSWORD, pushCertPasswd);
        if (!StringUtility.isNullOrEmpty(pushCertPasswd)) {
            Text data = removeClientHeaderFromData(pushCertBase64String);
            if (StringUtility.isNullOrEmpty(data)) {
                log.severe("Input file is not saved as it is not in expected format and encoding");
            } else {
                configuration.setProperty(PUSH_NOTIFICATION_CERT_BINARY, data);
            }
        }

        datastoreService.put(configuration);
        memcache.put(getMemKeyForConfigEntity(key), configuration);

        // Set endpoints auth config using client Ids that are not empty.
        List<String> clientIds = new ArrayList<String>();
        List<String> audiences = new ArrayList<String>();

        if (!StringUtility.isNullOrEmpty(androidClientId)) {
            clientIds.add(androidClientId);
        }

        if (!StringUtility.isNullOrEmpty(iOSClientId)) {
            clientIds.add(iOSClientId);
        }

        // Android client requires both Android Client ID and Web Client ID.
        // The latter is the same as audience field.
        if (!StringUtility.isNullOrEmpty(audience)) {
            clientIds.add(audience);
            audiences.add(audience);
        } else {
            audiences.add(new String());
        }

        if (clientIds.size() == 0) {
            clientIds.add(new String());
        }

        endpointsConfigManager.setAuthenticationInfo(EndpointV1.class, clientIds, audiences);
        endpointsConfigManager.setAuthenticationInfo(BlobEndpoint.class, clientIds, audiences);
    }

    protected Key getKey() {
        return KeyFactory.createKey(CONFIGURATION_ENTITY_KIND, CURRENT_CONFIGURATION);
    }

    private String getMemKeyForConfigEntity(Key key) {
        return CONFIGURATION_ENTITY_KIND + KeyFactory.keyToString(key);
    }

    /**
     * Sets the last subscription delete time to current time.
     */
    public void setLastSubscriptionDeleteAllTime(Date time) {
        Entity config = getConfiguration();
        config.setProperty(LAST_SUBSCRIPTION_DELETE_TIMESTAMP, time);
        this.datastoreService.put(config);
        this.memcache.put(getMemKeyForConfigEntity(getKey()), config);
    }

    /**
     * Gets the last subscription deletion time.
     * @return The last subscription deletion time.  If it's null, then no subscription delete all
     *         has been issued yet.
     */
    public Date getLastSubscriptionDeleteAllTime() {
        Entity config = getConfiguration();
        return (Date) config.getProperty(LAST_SUBSCRIPTION_DELETE_TIMESTAMP);
    }

    /**
     * Gets {@link com.google.cloud.backend.config.BackendConfigManager.AuthMode} of the current configuration.
     */
    public AuthMode getAuthMode() {
        return AuthMode.valueOf((String) getConfiguration().getProperty(AUTHENTICATION_MODE));
    }

    /**
     * Gets GCM API key.
     */
    public String getGcmKey() {
        return (String) getConfiguration().getProperty(ANDROID_GCM_KEY);
    }

    /**
     * Gets Push Notification Certificate password.
     */
    public String getPushCertPassword() {
        return (String) getConfiguration().getProperty(PUSH_NOTIFICATION_CERT_PASSWORD);
    }

    /**
     * Gets the push notification certificate as an input stream.
     * 
     * @return the push notification certificate as an input stream or null if the certificate is
     *         not available.
     */
    public InputStream getPushNotificationCertificate() {
        Text binary = (Text) getConfiguration().getProperty(PUSH_NOTIFICATION_CERT_BINARY);
        if (StringUtility.isNullOrEmpty(binary)) {
            return null;
        }

        return new ByteArrayInputStream(Base64.decodeBase64(binary.getValue().getBytes()));
    }

    /**
     * Gets the push notification certificates as a byte array.
     *
     * @return the push notification certificate as a byte array or null if the certificate is not
     *         available.
     */
    public byte[] getPushNotificationCertificateBytes() {
        Text binary = (Text) getConfiguration().getProperty(PUSH_NOTIFICATION_CERT_BINARY);
        if (StringUtility.isNullOrEmpty(binary)) {
            return null;
        }

        return Base64.decodeBase64(binary.getValue().getBytes());
    }

    /**
     * Returns true if Push Notification is enabled; False otherwise.
     */
    public boolean isPushEnabled() {
        return (Boolean) getConfiguration().getProperty(PUSH_ENABLED);
    }

    protected String getSecretKey() {
        return (String) getConfiguration().getProperty(PER_APP_SECRET_KEY);
    }

    /**
     * Extracts data without prefix since data from the front end is based64-encoded and prefixed with
     * "data:application/x-pkcs12;base64,".
     *
     * @param base64String from client
     * @return extracted data without prefix
     */
    private Text removeClientHeaderFromData(String base64String) {
        if (StringUtility.isNullOrEmpty(base64String)) {
            return null;
        }

        int index = base64String.indexOf(PKCS12_BASE64_PREFIX);
        if (index < 0) {
            return null;
        }

        String data = base64String.substring(index + PKCS12_BASE64_PREFIX.length());
        if (Base64.isBase64(data)) {
            return new Text(data);
        } else {
            return null;
        }
    }
}