/*
* Copyright (C) 2015 Google Inc. All Rights Reserved.
*
* 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 apps.provisioning.data;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import apps.provisioning.server.account.data.UsernameDataSource;
import apps.provisioning.server.apis.GoogleDirectory;
/**
* Contains an H2 DB and refreshes it periodically with all the user names from the Google
* Directory.
*/
public class UsernameCache implements UsernameDataSource {
private final Logger logger = Logger.getLogger(UsernameCache.class.getName());
public static String STATUS_READY = "ready";
public static String STATUS_REFRESHING = "refreshing";
public static String STATUS_CACHING = "caching";
public static String STATUS_EMPTY = "empty";
public static String STATUS_DISPOSED = "disposed";
private final ScheduledExecutorService updateScheduler = Executors
.newSingleThreadScheduledExecutor();
private int initialUpdateDelayInSeconds;
private int updateRateInSeconds;
private H2DataSource dataSource;
private GoogleDirectory googleDirectory;
private String databasePath;
private String databaseName;
private String status;
// List that is used to store usernames that were created before the cache is
// ready.
private ArrayList<String> tempUsernames;
/**
* Initializes the cache. Populates it after initialUpdateDelayInSeconds and refreshes it every
* updateRateInSeconds after that.
*
* @param initialUpdateDelayInSeconds The seconds to wait for the first time the cache will be
* populated.
* @param updateRateInSeconds The second rate to refresh the cache.
* @param databasePath The path where the H2 DB will be created.
* @param databaseName The name of the H2 DB that will be created.
* @param googleDirectory The Google Directory. Used to get the user names from Google.
* @throws SQLException
* @throws Exception
*/
public UsernameCache(int initialUpdateDelayInSeconds, int updateRateInSeconds,
String databasePath, String databaseName, GoogleDirectory googleDirectory)
throws SQLException, Exception {
this.initialUpdateDelayInSeconds = initialUpdateDelayInSeconds;
this.updateRateInSeconds = updateRateInSeconds;
this.googleDirectory = googleDirectory;
this.databasePath = databasePath;
this.databaseName = databaseName;
this.tempUsernames = new ArrayList<String>();
this.status = STATUS_EMPTY;
initDataSource();
}
/**
* Initializes the cache. Populates it after initialUpdateDelayInSeconds and refreshes it every
* updateRateInSeconds after that.
*
* @throws Exception
*/
private void initDataSource() throws Exception {
updateScheduler.scheduleWithFixedDelay(new Runnable() {
public void run() {
try {
refreshCache();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}, initialUpdateDelayInSeconds, updateRateInSeconds, TimeUnit.SECONDS);
}
/**
* Refreshes the cache.
*
* @throws Exception
*/
private synchronized void refreshCache() throws Exception {
if (dataSource == null) {
this.status = STATUS_CACHING;
} else {
this.status = STATUS_REFRESHING;
}
Date date = new Date();
H2DataSource newDataSource =
new H2DataSource(databasePath, databaseName + "(" + date.toString() + ")");
// Fill out the temporary data source while the old data source serves
// calls.
googleDirectory.copyToDataSource(newDataSource);
// Point the old data source to the new one and dispose the old one.
H2DataSource oldDataSource = dataSource;
dataSource = newDataSource;
if (oldDataSource != null) {
oldDataSource.dispose();
}
// Insert any username that was created while the cache was being updated
// and is not in the cache yet.
for (String username : tempUsernames) {
if (!dataSource.exists(username)) {
dataSource.insert(username);
}
}
tempUsernames.clear();
this.status = STATUS_READY;
}
/**
* @return Whether the cache is ready to be read.
*/
public boolean isReady() {
return status == STATUS_READY || status == STATUS_REFRESHING;
}
/**
* @return The current cache status.
*/
public String getStatus() {
return status;
}
public boolean exists(String username) throws SQLException, Exception {
if (dataSource == null) {
throw new Exception("Should not call exists if the data source hasn't been created");
}
if (this.status != STATUS_READY) {
if (this.status == STATUS_REFRESHING) {
logger.log(Level.WARNING, "Checking an out-of-date cache.");
} else {
throw new Exception("Trying to read the cache when it's not ready. Current status: "
+ status);
}
}
return dataSource.exists(username);
}
public void insert(String username) throws SQLException, Exception {
if (status != STATUS_READY) {
// Cache isn't ready. Insert the username in a temporary list, which will
// be inserted in the cache when ready.
tempUsernames.add(username);
} else {
dataSource.insert(username);
}
}
public void insertMultiple(ArrayList<String> usernames) throws SQLException, Exception {
throw new Exception("Should not call to insertMultiple outside of the cache.");
}
/**
* Disposes the H2 data source.
*
* @throws Exception
*/
public synchronized void disposeDataSource() throws Exception {
if (dataSource == null) {
return;
}
dataSource.dispose();
this.dataSource = null;
this.status = STATUS_DISPOSED;
}
/**
* Used only in testing.
*
* @throws Exception
*/
public synchronized void reset() throws Exception {
dataSource.reset();
}
}