/*
* Copyright (c) 2012 Data Harmonisation Panel
*
* All rights reserved. This program and the accompanying materials are made
* available under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, either version 3 of the License,
* or (at your option) any later version.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this distribution. If not, see <http://www.gnu.org/licenses/>.
*
* Contributors:
* Data Harmonisation Panel <http://www.dhpanel.eu>
*/
package eu.esdihumboldt.util.scavenger;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import org.apache.commons.io.FileUtils;
import de.fhg.igd.slf4jplus.ALogger;
import de.fhg.igd.slf4jplus.ALoggerFactory;
import eu.esdihumboldt.util.Pair;
import eu.esdihumboldt.util.PlatformUtil;
/**
* Scans for folder resources in a specific location or references a single
* resource.
*
* @param <T> the resource reference type
* @author Simon Templer
*/
public abstract class AbstractResourceScavenger<T> implements ResourceScavenger<T> {
/**
* Resource identifier in one resource mode.
*/
public static final String DEFAULT_RESOURCE_ID = "resource";
private static final ALogger log = ALoggerFactory.getLogger(AbstractResourceScavenger.class);
private final File huntingGrounds;
private final Map<String, T> resources = new HashMap<>();
private final Set<String> reserved = new HashSet<>();
/**
* Create a scavenger instance. Subclass constructors should call
* {@link #triggerScan()} after to initialize the scavenger.
*
* @param scavengeLocation the location to scan, if the location does not
* exist or is not accessible, a default location inside the
* platform instance location is used
* @param instanceLocPath the instance location sub-path to use if the
* scavengeLocation is invalid or <code>null</code>, may be
* <code>null</code> if the platform instance location should not
* be used as fall-back
*/
public AbstractResourceScavenger(File scavengeLocation, String instanceLocPath) {
if (scavengeLocation == null || !scavengeLocation.exists() && instanceLocPath != null) {
// use default location
File instanceLoc = PlatformUtil.getInstanceLocation();
if (instanceLoc != null) {
try {
scavengeLocation = new File(instanceLoc, instanceLocPath);
if (!scavengeLocation.exists()) {
scavengeLocation.mkdirs();
}
} catch (Exception e) {
log.error(
"Unable to determine instance location, can't initialize resource scavenger.",
e);
scavengeLocation = null;
}
huntingGrounds = scavengeLocation;
}
else {
log.error("No instance location, can't initialize resource scavenger.");
huntingGrounds = null;
}
}
else {
huntingGrounds = scavengeLocation;
}
if (huntingGrounds != null) {
log.info("Resources location is " + huntingGrounds.getAbsolutePath());
}
// triggerScan();
}
/**
* @see ResourceScavenger#triggerScan()
*/
@Override
public void triggerScan() {
synchronized (resources) {
if (huntingGrounds != null) {
if (huntingGrounds.isDirectory()) {
// scan for sub-directories
Set<String> foundIds = new HashSet<String>();
File[] resourceDirs = huntingGrounds.listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
// accept non-hidden directories
return pathname.isDirectory() && !pathname.isHidden();
}
});
for (File resourceDir : resourceDirs) {
String resourceId = resourceDir.getName();
foundIds.add(resourceId);
if (!resources.containsKey(resourceId)) {
// resource reference not loaded yet
T reference;
try {
reference = loadReference(resourceDir, null, resourceId);
resources.put(resourceId, reference);
onAdd(reference, resourceId);
} catch (IOException e) {
log.error("Error creating resource reference", e);
}
}
else {
// update existing resource
updateResource(resources.get(resourceId), resourceId);
}
}
Set<String> removed = new HashSet<String>(resources.keySet());
removed.removeAll(foundIds);
// deal with resources that have been removed
for (String resourceId : removed) {
T reference = resources.remove(resourceId);
if (reference != null) {
// remove active environment
onRemove(reference, resourceId);
}
}
}
else {
// one project mode
if (!resources.containsKey(DEFAULT_RESOURCE_ID)) {
// project configuration not loaded yet
T reference;
try {
reference = loadReference(huntingGrounds.getParentFile(),
huntingGrounds.getName(), DEFAULT_RESOURCE_ID);
resources.put(DEFAULT_RESOURCE_ID, reference);
onAdd(reference, DEFAULT_RESOURCE_ID);
} catch (IOException e) {
log.error("Error creating project handler", e);
}
}
else {
// update existing project
updateResource(resources.get(DEFAULT_RESOURCE_ID), DEFAULT_RESOURCE_ID);
}
}
}
}
}
/**
* Called when a resource has been added, either when adding the resource on
* the first scan or if it was added afterwards.
*
* @param reference the resource reference
* @param resourceId the resource identifier
*/
protected abstract void onAdd(T reference, String resourceId);
/**
* Called when a resource has been removed.
*
* @param reference the resource reference
* @param resourceId the resource identifier
*/
protected abstract void onRemove(T reference, String resourceId);
/**
* Called when an existing resource is visited during a scan.
*
* @param reference the resource reference to update
* @param resourceId the resource identifier
*/
protected abstract void updateResource(T reference, String resourceId);
/**
* Load a resource reference.
*
* @param resourceFolder the resource folder
* @param resourceFileName the name of the resource file in that folder, may
* be <code>null</code> if unknown
* @param resourceId the resource identifier
* @return the resource reference
* @throws IOException if the resource cannot be accessed or loaded
*/
protected abstract T loadReference(File resourceFolder, String resourceFileName,
String resourceId) throws IOException;
@Override
public File reserveResourceId(String resourceId) throws ScavengerException {
if (!allowAddResource()) {
throw new ScavengerException("Adding a resource not allowed.");
}
// trigger a scan to be up-to-date
triggerScan();
// TODO check if resourceId is valid
synchronized (resources) {
if (resources.containsKey(resourceId)) {
// there already is a project with that ID
throw new ScavengerException("Project ID already taken.");
}
if (reserved.contains(resourceId)) {
// the project ID is already reserved
throw new ScavengerException("Project ID already reserved.");
}
reserved.add(resourceId);
File projectFolder = new File(huntingGrounds, resourceId);
if (!projectFolder.exists()) {
// try creating the directory
try {
projectFolder.mkdir();
} catch (Exception e) {
throw new ScavengerException("Could not create project directory", e);
}
}
if (!projectFolder.exists()) {
throw new ScavengerException("Could not create project directory");
}
return projectFolder;
}
}
@Override
public Pair<String, File> reserveResource(String desiredId) throws ScavengerException {
if (!allowAddResource()) {
throw new ScavengerException("Adding a resource not allowed.");
}
// trigger a scan to be up-to-date
triggerScan();
synchronized (resources) {
// normalize desired identifier
if (desiredId != null) {
// replace spaces by -
desiredId = desiredId.replaceAll("\\s+", "-");
// remove all non-word characters (except -)
desiredId = desiredId.replaceAll("[^a-zA-Z0-9_\\-]", "");
// might be run on windows
desiredId = desiredId.toLowerCase();
if (desiredId.isEmpty()) {
// empty string not allowed
desiredId = null;
}
}
String id;
if (desiredId != null && !resources.containsKey(desiredId)
&& !reserved.contains(desiredId)) {
// desired ID is OK
id = desiredId;
}
else {
if (desiredId != null) {
// try postfix
int num = 2;
String testId = desiredId + num;
while (resources.containsKey(testId) || reserved.contains(testId)) {
testId = desiredId + (++num);
}
id = testId;
}
else {
// try numeric identifiers
int num = 1;
String testId = String.valueOf(num);
while (resources.containsKey(testId) || reserved.contains(testId)) {
testId = String.valueOf(++num);
}
id = testId;
}
}
reserved.add(id);
File projectFolder = new File(huntingGrounds, id);
if (!projectFolder.exists()) {
// try creating the directory
try {
projectFolder.mkdir();
} catch (Exception e) {
throw new ScavengerException("Could not create project directory", e);
}
}
if (!projectFolder.exists()) {
throw new ScavengerException("Could not create project directory");
}
return new Pair<String, File>(id, projectFolder);
}
}
/**
* @see ResourceScavenger#releaseResourceId(String)
*/
@Override
public void releaseResourceId(String resourceId) {
if (!allowAddResource()) {
return;
}
synchronized (resources) {
if (reserved.contains(resourceId)) {
reserved.remove(resourceId);
deleteResource(resourceId);
}
}
}
@Override
public void deleteResource(String resourceId) {
if (!allowAddResource()) {
return;
}
T removed = null;
synchronized (resources) {
// delete directory
File dir = new File(huntingGrounds, resourceId);
if (dir.exists()) {
try {
FileUtils.deleteDirectory(new File(huntingGrounds, resourceId));
} catch (IOException e) {
log.error("Error deleting resource directory content", e);
}
}
// removed resource ref
if (resources.containsKey(resourceId)) {
removed = resources.remove(resourceId);
}
}
if (removed != null) {
onRemove(removed, resourceId);
}
}
@Override
public boolean allowAddResource() {
return huntingGrounds.isDirectory();
}
@Override
public Set<String> getResources() {
synchronized (resources) {
return new TreeSet<String>(resources.keySet());
}
}
@Override
public T getReference(String resourceId) {
return resources.get(resourceId);
}
@Override
public File getHuntingGrounds() {
return huntingGrounds;
}
}