package org.exist.xquery.modules.counter;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Hashtable;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.exist.EXistException;
import org.exist.backup.RawDataBackup;
import org.exist.indexing.RawBackupSupport;
import org.exist.util.FileUtils;
/**
* @author Jasper Linthorst (jasper.linthorst@gmail.com)
*
*/
public class Counters implements RawBackupSupport {
private final static Logger LOG = LogManager.getLogger(Counters.class);
private static volatile Counters instance;
public final static String COUNTERSTORE = "counters";
public final static String DELIMITER = ";";
private Path store = null;
private Map<String, Long> counters = new Hashtable<>();
private Counters(final Optional<Path> dataDir) throws EXistException {
this.store = FileUtils.resolve(dataDir, COUNTERSTORE);
loadStore();
}
/**
* Loads data from the on-disk counter store
*/
private void loadStore() throws EXistException {
try {
if(Files.exists(store)) {
try(final BufferedReader br = Files.newBufferedReader(store, StandardCharsets.UTF_8)) {
String line = "";
while ((line = br.readLine()) != null) {
//Use ; as a DELIMITER, counter names must be tested and rejected when they contain this character!
final String[] tokens = line.split(DELIMITER);
try {
counters.put(tokens[0], Long.parseLong(tokens[1]));
} catch (final NumberFormatException e) {
throw new EXistException("Corrupt counter store file: " + store.toAbsolutePath().toString());
}
}
}
}
} catch (final IOException e) {
throw new EXistException("IOException occurred when reading counter store file.");
}
}
/**
* Get singleton of Counters object.
*/
public static Counters getInstance(final Path dataDir) throws EXistException {
if (instance == null) {
LOG.debug("Initializing counters.");
instance = new Counters(Optional.ofNullable(dataDir));
}
return instance;
}
public static Counters getInstance() throws EXistException {
return getInstance(null);
}
/**
* Creates a new Counter, initializes it to 0 and returns the current value in a long.
*
* @param counterName
* @return the initial value of the newly created counter
* @throws EXistException
*/
public long createCounter(final String counterName) throws EXistException {
return createCounter(counterName, (long) 0);
}
/**
* Creates a new Counter, initializes it to initValue and returns the current value in a long.
* If there already is a counter with the same name, the current value of this counter is returned.
*
* @param counterName
* @param initValue
* @return the current value of the named counter
* @throws EXistException
*/
public synchronized long createCounter(final String counterName, final long initValue) throws EXistException {
if (counters.containsKey(counterName)) {
return counters.get(counterName);
} else {
counters.put(counterName, initValue);
try {
serializeTable();
} catch (final IOException e) {
throw new EXistException("Unable to save to counter store file.", e);
}
return counters.get(counterName);
}
}
/**
* Removes a counter by the specified name.
*
* @param counterName
* @return true if the counter is removed
* @throws EXistException
*/
public synchronized boolean destroyCounter(final String counterName) throws EXistException {
if (counters.containsKey(counterName)) {
counters.remove(counterName);
try {
serializeTable();
} catch (final IOException e) {
throw new EXistException("Unable to remove counter from counter store file.", e);
}
return true;
} else {
return false;
}
}
/**
* Retrieves the next value of a counter (specified by name).
*
* @param counterName
* @return the next counter value or -1 if the counter does not exist.
* @throws EXistException
*/
public synchronized long nextValue(final String counterName) throws EXistException {
if (!counters.containsKey(counterName)) {
return -1;
}
long c = counters.get(counterName);
c++;
counters.put(counterName, c);
try {
serializeTable();
} catch (final IOException e) {
throw new EXistException("Unable to save to counter store file.", e);
}
return c;
}
/**
* Returns all available counters in a Set of Strings.
*
* @return all available counters in a Set of Strings
*/
public Set<String> availableCounters() {
return counters.keySet();
}
/**
* Serializes the Map with counters to the filesystem.
*
* @throws IOException
*/
private synchronized void serializeTable() throws IOException {
try(final PrintWriter pw = new PrintWriter(Files.newBufferedWriter(store, StandardCharsets.UTF_8))) {
for(final Map.Entry<String, Long> counter : counters.entrySet()) {
pw.println(counter.getKey() + DELIMITER + counter.getValue().toString());
}
}
}
@Override
public void backupToArchive(final RawDataBackup backup) throws IOException {
if (!Files.exists(store)) {
return;
}
// do not use try-with-resources here, closing the OutputStream will close the entire backup
//try(final OutputStream os = backup.newEntry(FileUtils.fileName(store))) {
try {
final OutputStream os = backup.newEntry(FileUtils.fileName(store));
Files.copy(store, os);
} finally {
backup.closeEntry();
}
}
}