All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.daisy.streamline.api.config.UserConfigurationsCollection Maven / Gradle / Ivy

The newest version!
package org.daisy.streamline.api.config;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;

/**
 * 

Provides a user configuration collection that lets a user add and * remove configurations at runtime. This class is not intended for direct * use. The intended use is as part of a {@link UserConfigurationsProvider} * implementation. By implementing the {@link UserConfigurationsProvider}, * the configurations will be available through the {@link ConfigurationsCatalogService} * API.

* *

This class has all the methods of the * {@link UserConfigurationsProvider} interface but doesn't * implements it. This is intentional. For example, this class lacks * a constructor without arguments, which is required for service discovery using * both SPI and OSGi. This class has also been made final to prohibit a * {@link UserConfigurationsProvider} implementation from extending it.

* *

Note: this class is not connected directly to the {@link ConfigurationsCatalog} * because of restrictions imposed by the API design guidelines. To solve the problem, * the {@link ExclusiveAccess} interface was created (an implementation of this * interface would have been too general to be a public part of this API but to useful * to keep private).

*/ public final class UserConfigurationsCollection { private static final Logger logger = Logger.getLogger(UserConfigurationsCollection.class.getCanonicalName()); private static final String MASTER_FILE_NAME = "catalog.ser"; private static final String CONFIG_EXT = ".ser"; private final File baseDir; private final ExclusiveAccess lock; private final File catalog; private Inventory inventory; private Date sync; /** * Creates a new configurations collection. * @param baseDir the folder to store the configurations * @param lock an exclusive lock, see this interface for more information * @throws NullPointerException if baseDir is null */ public UserConfigurationsCollection(File baseDir, ExclusiveAccess lock) { this.inventory = new Inventory(); this.sync = null; this.baseDir = Objects.requireNonNull(baseDir); baseDir.mkdirs(); this.lock = lock; this.catalog = new File(baseDir, MASTER_FILE_NAME); try { sync(this::cleanupInventory); } catch (IOException e) { Logger.getLogger(UserConfigurationsCollection.class.getCanonicalName()).log(Level.WARNING, "Failed to read custom configurations.", e); } } /** * Gets configuration details. * @return the configuration details */ public synchronized Set getConfigurationDetails() { return inventory.entries().stream() .map(c->c.getConfiguration().orElse(null)) .filter(v->v!=null) .map(v->v.getDetails()) .collect(Collectors.toSet()); } /** * Gets a configuration. * @param key the configuration key * @return a map, or null if the key is not found */ public synchronized Map getConfiguration(String key) { return Optional.ofNullable(inventory.get(key)) .flatMap(v->v.getConfiguration()) .map(v->v.getMap()) .orElse(null); } /** * Adds a configuration to this catalog. * @param niceName the display name * @param description the configuration description * @param config the configuration details * @return the identifier for the new configuration, or an empty optional if the configuration could not be added */ public synchronized Optional addConfiguration(String niceName, String description, Map config) { try { if (catalog==null) { throw new FileNotFoundException(); } return sync(()-> { ConfigurationDetails p = new ConfigurationDetails.Builder(inventory.nextIdentifier()) .niceName(niceName) .description(description).build(); try { InventoryEntry ret = InventoryEntry.create(new Configuration(p, new HashMap<>(config)), newConfigurationFile()); inventory.add(ret); return Optional.of(ret.getIdentifier()); } catch (IOException e) { logger.log(Level.WARNING, "Failed to add configuration.", e); return Optional.empty(); } }); } catch (IOException e) { logger.log(Level.WARNING, "Could not add configuration", e); return Optional.empty(); } } /** * Removes the configuration with the specified identifier. * @param key the identifier * @return true if the configuration was successfully removed, false otherwise */ public synchronized boolean removeConfiguration(String key) { try { if (catalog==null) { throw new FileNotFoundException(); } return sync(()->inventory.remove(key)!=null); } catch (IOException e) { logger.log(Level.WARNING, "Failed to remove configuration.", e); return false; } } /** * Returns true if the provider contains the specified key. * @param key the key * @return true if the provider contains the key, false otherwise */ public synchronized boolean containsConfiguration(String key) { return inventory.keys().contains(key); } private File newConfigurationFile() throws IOException { // Note that this file is not a temporary file, // it's just convenient to use it to create a unique name return File.createTempFile("config-", CONFIG_EXT, baseDir); } private synchronized T sync(Supplier func) throws IOException { // Check this early, to avoid reading from a file that didn't exist until it was opened. boolean fileExists = catalog.exists(); if (!fileExists && func==null) { return null; } try { acquireLock(); } catch (InterruptedException e1) { Thread.currentThread().interrupt(); return null; } // We now have exclusive access to the resources. // Note that "exclusive access" is an agreement made with other processes running this code. // Technically, we only have exclusive access to the lock file itself. try { // Read configurations from file if (sync == null || new Date(catalog.lastModified()).after(sync)) { if (fileExists) { inventory = Inventory.read(catalog); sync = new Date(catalog.lastModified()); } else { // reset inventory inventory = new Inventory(); } } // Update file if (func!=null) { T ret = func.get(); try { inventory.write("catalog-", catalog); sync = new Date(catalog.lastModified()); } catch (IOException e) { logger.log(Level.WARNING, "Failed to write catalog to file system.", e); throw e; } return ret; } else { return null; } } finally { lock.release(); } } /** * Cleans up the inventory. * @return true if the inventory was touched, false otherwise. */ private boolean cleanupInventory() { boolean modified = false; modified |= removeUnreadable(); modified |= recreateMismatching(); modified |= importConfigurations(); return modified; } /** * Removes unreadable configurations from the file system. * @return true if some configurations were removed, false otherwise */ private boolean removeUnreadable() { List unreadable = inventory.removeUnreadable(); unreadable.forEach(v->v.delete()); return !unreadable.isEmpty(); } /** * Recreates mismatching configurations. * @return true if some configurations were recreated, false otherwise */ private boolean recreateMismatching() { List mismatching = inventory.removeMismatching(); mismatching.forEach(entry->{ try { inventory.add(InventoryEntry.create(entry.copyWithIdentifier(inventory.nextIdentifier()), newConfigurationFile())); } catch (IOException e1) { logger.log(Level.WARNING, "Failed to write a configuration.", e1); } }); return !mismatching.isEmpty(); } /** * Imports new configurations from the file system. * @return true if something was imported, false otherwise */ private boolean importConfigurations() { // Create a set of files in the inventory Set files = inventory.entries().stream().map(v->v.getPath()).collect(Collectors.toSet()); List entriesToImport = Arrays.asList(baseDir.listFiles(f-> f.isFile() && !f.equals(catalog) // Exclude the master catalog (should it have the same extension as entries) && f.getName().endsWith(CONFIG_EXT) && !files.contains(f)) // Exclude files already in the inventory ); entriesToImport.forEach(f->{ try { inventory.add(InventoryEntry.create(Configuration.read(f).copyWithIdentifier(inventory.nextIdentifier()), newConfigurationFile())); f.delete(); } catch (IOException e) { if (logger.isLoggable(Level.FINE)) { logger.log(Level.FINE, "Failed to read: " + f, e); } } }); return !entriesToImport.isEmpty(); } private void acquireLock() throws IOException, InterruptedException { // Acquire lock int i = 0; boolean locked = false; while (!(locked = lock.acquire()) && i<20) { i++; try { //Sleep between 50-150 ms Thread.sleep(50+(int)Math.random()*100); } catch (InterruptedException e) { throw e; } } if (!locked) { throw new IOException("Failed to acquire lock."); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy