/* Copyright 2012 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 org.arbeitspferde.groningen.config;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.protobuf.TextFormat;
import org.arbeitspferde.groningen.proto.GroningenConfigProto.ProgramConfiguration;
import org.arbeitspferde.groningen.utility.FileEventNotifier;
import org.arbeitspferde.groningen.utility.FileEventNotifierFactory;
import org.arbeitspferde.groningen.utility.FileFactory;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.CharBuffer;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Construct configuration protocol buffers from a given file and potentially monitor that
* file for updates. Should an update occur, reload the protocol buffer and notify observers
* of the update. Observers are left to request the new protocol buffer at their leisure.
*/
public class ProtoBufFileSource implements ProtoBufSource {
// Logger for this class
private static final Logger logger = Logger.getLogger(ProtoBufFileSource.class.getName());
// Observers for notifications of updates to the configuration
private final List<ProtoBufSourceListener> observers = Lists.newArrayList();
// Path to the file which should be used as the text representation of the protobuf
private String inputPath = null;
// Polling period for updates to the above file
private long refreshPeriod = 0L;
// Notifier that polls the file for updates and executes a callback when one occurs
protected FileEventNotifier notifier = null;
// The current proto
public ProgramConfiguration proto = null;
// Lock protecting reloading of the source and pushing it into the protobuf such
// that multiple updates to the file cannot result in an old version overwriting
// a new one.
private final Object reloadLock = new Object();
private final FileFactory fileFactory;
private final FileEventNotifierFactory fileEventNotifierFactory;
private final LegacyProgramConfigurationMediator legacyProgramConfigurationMediator;
/**
* Construct a {@code ProtoBufFileSource}.
*
* @param location the path to the config file with format
* {@literal [(<flags>}:)*]<path>} where {@literal <flags>} can be:
* <ul>
* <li>refresh=\d+ sets file polling period in seconds.
* </ul>
* @throws IllegalArgumentException if the location string cannot be parsed into flags and a
* path
*/
public ProtoBufFileSource(final String location, final FileFactory fileFactory,
final FileEventNotifierFactory fileEventNotifierFactory,
final LegacyProgramConfigurationMediator legacyProgramConfigurationMediator) {
this.fileFactory = fileFactory;
this.fileEventNotifierFactory = fileEventNotifierFactory;
this.legacyProgramConfigurationMediator = legacyProgramConfigurationMediator;
Preconditions.checkNotNull(location, "location cannot be null");
String [] locationParts = location.split(":");
// parse out any flags included in the location, marking the last flag found
int lastFlag = -1;
for (int i = 0; i < locationParts.length; i++) {
if (locationParts[i].matches("^refresh=\\d+$")) {
lastFlag = i;
refreshPeriod = Integer.parseInt(locationParts[i].substring(8));
}
}
// verify we had a path
inputPath = locationParts[locationParts.length - 1];
if (inputPath == null || lastFlag == (locationParts.length - 1)) {
throw new IllegalArgumentException("no input path specified");
}
}
/**
* {@inheritDoc}
*
* On return, a protobuf will have been loaded guaranteeing a non-null return for
* {@code getConfigData()}
*/
@Override
public void initialize() throws IOException {
/*
* Setup the notifier before we grab the protobuf for the first time to make sure we don't
* miss a revision of the file. Do the explicit reload instead of allowing the notifier
* to trigger a reload on instantiation so we verify we have a valid path.
*/
Runnable wrapReloadProtoFromFile = new Runnable() {
@Override
public void run() {
try {
reloadProtoFromFile();
} catch (IOException e) {
logger.log(Level.SEVERE,
String.format("failed to reload file %s after changed noticed", inputPath), e);
}
}
};
if (refreshPeriod > 0) {
logger.log(Level.FINE, "instantiating file notifier for file: %s", inputPath);
notifier = fileEventNotifierFactory.fileEventNotifierFor(inputPath, refreshPeriod,
TimeUnit.SECONDS, wrapReloadProtoFromFile, false);
}
reloadProtoFromFile();
}
@Override
public void shutdown() {
if (notifier != null) {
notifier.stop();
notifier = null;
}
}
@Override
public void register(ProtoBufSourceListener protoConfigurer) {
synchronized (observers) {
if (!observers.contains(protoConfigurer)) {
observers.add(protoConfigurer);
}
}
}
@Override
public boolean deregister(ProtoBufSourceListener protoConfigurer) {
synchronized (observers) {
return observers.remove(protoConfigurer);
}
}
/**
* Return the cached version of the current protobuf.
*
* {@inheritDoc}
*/
@Override
public ProgramConfiguration getConfigData() {
return proto;
}
public String getInputPath() {
return inputPath;
}
public void setInputPath(String path) {
inputPath = path;
}
public long getRefreshPeriod() {
return refreshPeriod;
}
public void setRefreshPeriod(long period) {
refreshPeriod = period;
}
/**
* Read in the contents of a file and produce a protobuf from the contents.
*
* Wraps reloading the proto from a given file, but farms out the actual acting on the
* contents of the Readable to a method we can test without hitting the file system.
*/
public void reloadProtoFromFile() throws IOException {
logger.log(Level.FINE, "attempting to load proto from file: %s", inputPath);
final InputStream inputStream = fileFactory.forFile(inputPath).inputStreamFor();
// protect against multiple update race condition
try {
synchronized (reloadLock) {
reloadProtoFromStream(inputStream);
notifyObservers();
}
} finally {
// we want to verify that we close the file as well.
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
logger.log(Level.SEVERE,
String.format("failed to close input config file %s", inputPath), e);
}
}
}
}
/**
* Take an InputStream and load the protobuf from with in it. This method is
* marked protected as it is expected to be overriden in subclasses that operate on
* non text format protobufs.
*
* @param inputStream the source from which to read the file contents
*/
public void reloadProtoFromStream(InputStream inputStream) throws IOException {
final Reader reader = new InputStreamReader(inputStream);
try {
reloadProtoFromReadable(reader);
} finally {
if (reader != null) {
reader.close();
}
}
}
/** Slurps in the config from the readable and notifies registered observers of the update */
public void reloadProtoFromReadable(Readable inputReader) throws IOException {
// We basically hand the Readable with the text representation off to something that
// knows how to parse it and stuff it in the Builder
ProgramConfiguration.Builder configBuilder = ProgramConfiguration.newBuilder();
final StringBuilder intermediateString = new StringBuilder();
final CharBuffer buffer = CharBuffer.allocate(256);
while (true) {
final int length = inputReader.read(buffer);
if (length == -1) {
break;
}
buffer.flip();
intermediateString.append(buffer, 0, length);
}
// TODO(team): rework such that this is a Reader that does rewrite on
// the fly
final String migratedString = legacyProgramConfigurationMediator
.migrateLegacyConfiguration(intermediateString.toString());
TextFormat.merge(migratedString, configBuilder);
proto = configBuilder.build();
}
/** Notify all registered observers of the change */
public void notifyObservers() {
synchronized (observers) {
// hold a local copy in case another thread generates another proto part while we
// are still notifying for this one. That thread will block while we finish notification
// and then renotify...
ProgramConfiguration localProto = proto;
for (ProtoBufSourceListener observer : observers) {
observer.handleProtoBufUpdate(localProto);
}
}
}
}