/*
* 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.hale.common.headless.transform;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
import java.util.Map;
import org.apache.commons.io.FileUtils;
import org.eclipse.core.runtime.content.IContentType;
import org.joda.time.Duration;
import org.joda.time.ReadableDuration;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import de.fhg.igd.slf4jplus.ALogger;
import de.fhg.igd.slf4jplus.ALoggerFactory;
import eu.esdihumboldt.hale.common.core.HalePlatform;
import eu.esdihumboldt.hale.common.core.io.IOProvider;
import eu.esdihumboldt.hale.common.core.io.project.model.IOConfiguration;
import eu.esdihumboldt.hale.common.core.io.supplier.FileIOSupplier;
import eu.esdihumboldt.hale.common.core.io.supplier.LocatableOutputSupplier;
import eu.esdihumboldt.hale.common.headless.EnvironmentService;
import eu.esdihumboldt.hale.common.headless.HeadlessIO;
import eu.esdihumboldt.hale.common.headless.TransformationEnvironment;
import eu.esdihumboldt.hale.common.headless.WorkspaceService;
import eu.esdihumboldt.hale.common.headless.report.ReportFile;
import eu.esdihumboldt.hale.common.instance.io.InstanceReader;
import eu.esdihumboldt.hale.common.instance.io.InstanceWriter;
/**
* A transformation workspace based on {@link WorkspaceService} and
* {@link EnvironmentService}.
*
* @author Simon Templer
*/
public class TransformationWorkspace {
private static final ALogger log = ALoggerFactory.getLogger(TransformationWorkspace.class);
/**
* Name of the report file placed in a workspace folder.
*/
private static final String WORKSPACE_REPORT_FILE = "reports.log";
/**
* Name of the folder containing the source files in a transformation
* workspace.
*/
private static final String WORKSPACE_SOURCE_FOLDER = "source";
/**
* Name of the folder containing the target files in a transformation
* workspace.
*/
private static final String WORKSPACE_TARGET_FOLDER = "target";
/**
* Name of the workspace setting that holds information about the completion
* of the transformation.
*/
private static final String SETTING_TRANSFORMATION_SUCCESS = "transformationSuccess";
private final WorkspaceService workspaces;
private final String workspaceId;
private final File workspace;
private final File targetFolder;
private final File sourceFolder;
private final File reportFile;
/**
* Create a new transformation workspace with a lease duration of one day.
*
* @throws IllegalStateException if the {@link WorkspaceService} is not
* available
*/
public TransformationWorkspace() {
this(Duration.standardDays(1));
}
/**
* Create a new transformation workspace with a custom lease duration.
*
* @param leaseDuration the lease duration of the workspace
* @throws IllegalStateException if the {@link WorkspaceService} is not
* available
*/
public TransformationWorkspace(ReadableDuration leaseDuration) {
this(null, leaseDuration);
}
/**
* Create a representation of an existing transformation workspace.
*
* @param workspaceId the workspace identifier
* @throws IllegalStateException if the {@link WorkspaceService} is not
* available or the workspace with the given identifier does not
* exist
*/
public TransformationWorkspace(String workspaceId) {
this(workspaceId, null);
}
/**
* Create a new workspace or use an existing one.
*
* @param workspaceId the workspace identifier if this object should
* represent an existing workspace, may be <code>null</code> if
* leaseDuration is set.
* @param leaseDuration the lease duration of a new workspace to create, may
* be <code>null</code> if workspaceId is set
* @throws IllegalStateException if the {@link WorkspaceService} is not
* available or the workspace with the given identifier does not
* exist
*/
protected TransformationWorkspace(final String workspaceId, ReadableDuration leaseDuration) {
workspaces = HalePlatform.getService(WorkspaceService.class);
if (workspaces == null) {
throw new IllegalStateException("WorkspaceService not available through OSGi");
}
if (workspaceId == null) {
this.workspaceId = workspaces.leaseWorkspace(leaseDuration);
}
else {
this.workspaceId = workspaceId;
}
try {
workspace = workspaces.getWorkspaceFolder(this.workspaceId);
} catch (FileNotFoundException e) {
throw new IllegalStateException("Error accessing transformation workspace");
}
sourceFolder = new File(workspace, WORKSPACE_SOURCE_FOLDER);
sourceFolder.mkdir();
targetFolder = new File(workspace, WORKSPACE_TARGET_FOLDER);
targetFolder.mkdir();
// report file
reportFile = new File(workspace, WORKSPACE_REPORT_FILE);
}
/**
* Transform the instances provided through the given instance readers and
* store the result in the {@link #getTargetFolder()}.
*
* @param envId the environment ID
* @param sources the instance readers
* @param target the configuration of the target instance writer
* @return the future representing the successful completion of the
* transformation (note that a successful completion doesn't
* necessary mean there weren't any internal transformation errors)
* @throws Exception if launching the transformation fails
*/
public ListenableFuture<Boolean> transform(String envId, List<InstanceReader> sources,
IOConfiguration target) throws Exception {
EnvironmentService environments = HalePlatform.getService(EnvironmentService.class);
if (environments == null) {
throw new IllegalStateException("WorkspaceService not available through OSGi");
}
TransformationEnvironment env = environments.getEnvironment(envId);
if (env == null) {
throw new IllegalStateException(
"Transformation environment for project " + envId + " not available.");
}
return transform(env, sources, target, null);
}
/**
* Transform the instances provided through the given instance readers and
* by default stores the result in the {@link #getTargetFolder()}.
*
* @param env the transformation environment
* @param sources the instance readers
* @param target the configuration of the target instance writer
* @param customTarget the custom output supplier to use for the target,
* <code>null</code> to use the default target in thet
* {@link #getTargetFolder()}
* @return the future representing the successful completion of the
* transformation (note that a successful completion doesn't
* necessary mean there weren't any internal transformation errors)
* @throws Exception if launching the transformation fails
*/
public ListenableFuture<Boolean> transform(TransformationEnvironment env,
List<InstanceReader> sources, IOConfiguration target,
LocatableOutputSupplier<? extends OutputStream> customTarget) throws Exception {
InstanceWriter writer = (InstanceWriter) HeadlessIO.loadProvider(target);
// TODO determine content type if not set?
// output file
if (customTarget != null) {
writer.setTarget(customTarget);
}
else {
File out = new File(targetFolder,
"result." + getFileExtension(writer.getContentType()));
writer.setTarget(new FileIOSupplier(out));
}
ListenableFuture<Boolean> result = Transformation.transform(sources, writer, env,
new ReportFile(reportFile), workspace.getName());
Futures.addCallback(result, new FutureCallback<Boolean>() {
@Override
public void onSuccess(Boolean result) {
try {
setTransformationSuccess(result);
} catch (IOException e) {
log.error("Failed to set transformation success for workspace", e);
}
}
@Override
public void onFailure(Throwable t) {
try {
setTransformationSuccess(false);
} catch (IOException e) {
log.error("Failed to set transformation success for workspace", e);
}
}
});
return result;
}
/**
* Determines if a previously with
* {@link #transform(String, List, IOConfiguration)} started transformation
* process is finished. Regardless of the success or failure.
*
* @return <code>true</code> if the transformation is finished,
* <code>false</code> if the transformation is still running, no
* transformation was started or the workspace no longer exists
*/
public boolean isTransformationFinished() {
try {
Map<String, String> settings = workspaces.getSettings(workspaceId);
return settings.containsKey(SETTING_TRANSFORMATION_SUCCESS);
} catch (IOException e) {
// ignore
return false;
}
}
/**
* Determines if a previously with
* {@link #transform(String, List, IOConfiguration)} started transformation
* process was complete successfully. Note that a successful completion
* doesn't necessary mean there weren't any internal transformation errors.
* The {@link #getReportFile()} holds more detailed information.<br>
* <br>
* This method may only be called of the transformation is finished,
* otherwise an {@link IllegalStateException} will be thrown.
*
* @return if the transformation was completed successfully
* @throws IllegalStateException if the transformation is not finished
*
* @see #isTransformationFinished()
*/
public boolean isTransformationSuccessful() throws IllegalStateException {
try {
Map<String, String> settings = workspaces.getSettings(workspaceId);
String success = settings.get(SETTING_TRANSFORMATION_SUCCESS);
if (success == null) {
throw new IllegalStateException("Transformation not finished");
}
return Boolean.parseBoolean(success);
} catch (IOException e) {
// ignore
return false;
}
}
/**
* Set if the transformation was successfully completed. Must be called when
* the transformation is finished. Also deletes the source folder in the
* workspace.
*
* @param success if the transformation was completed successfully
* @throws FileNotFoundException if the workspace does not exist
* @throws IOException if the workspace configuration file cannot be read or
* written
*/
protected void setTransformationSuccess(boolean success)
throws FileNotFoundException, IOException {
workspaces.set(workspaceId, SETTING_TRANSFORMATION_SUCCESS, String.valueOf(success));
FileUtils.deleteDirectory(getSourceFolder());
}
/**
* Get the workspace settings.
*
* @return the current workspace settings, changes to the map will not be
* reflected in the settings
* @throws FileNotFoundException if the workspace does not exist
* @throws IOException if the workspace configuration file cannot be read
*
* @see #set(String, String)
*/
public Map<String, String> getSettings() throws FileNotFoundException, IOException {
return workspaces.getSettings(workspaceId);
}
/**
* Change a workspace setting.
*
* @param setting the name of the setting
* @param value the value, <code>null</code> to remove the setting
* @throws FileNotFoundException if the workspace does not exist
* @throws IOException if the workspace configuration file cannot be read or
* written
*
* @see #getSettings()
*/
public void set(String setting, String value) throws FileNotFoundException, IOException {
workspaces.set(workspaceId, setting, value);
}
/**
* Guess the file extension for a given I/O configuration.
*
* @param config the I/O provider configuration
* @return the file extensions or a default, w/o leading dot
*/
public static String guessFileExtension(IOConfiguration config) {
String id = config.getProviderConfiguration().get(IOProvider.PARAM_CONTENT_TYPE)
.as(String.class);
IContentType contentType = null;
if (id != null) {
contentType = HalePlatform.getContentTypeManager().getContentType(id);
}
return getFileExtension(contentType);
}
/**
* Get the default file extension for the given content type.
*
* @param contentType the content type, may be <code>null</code>
* @return the file extension w/o leading dot
*/
private static String getFileExtension(IContentType contentType) {
if (contentType != null) {
String[] extensions = contentType.getFileSpecs(IContentType.FILE_EXTENSION_SPEC);
if (extensions != null && extensions.length > 0) {
return extensions[0];
}
}
// fall-back
return "out";
}
/**
* @return the workspace ID
*/
public String getId() {
return workspaceId;
}
/**
* @return the workspace folder
*/
public File getWorkspace() {
return workspace;
}
/**
* Get the target folder. This folder holds the transformation results after
* the transformation is finished and successful.
*
* @return the target folder
*
* @see #isTransformationFinished()
* @see #isTransformationSuccessful()
*/
public File getTargetFolder() {
return targetFolder;
}
/**
* Get the source folder. Files placed in this folder will be deleted after
* the transformation has finished.
*
* @return the source folder
*/
public File getSourceFolder() {
return sourceFolder;
}
/**
* Get the report file. It holds information about the finished
* transformation.
*
* @return the report file
*
* @see #isTransformationFinished()
*/
public File getReportFile() {
return reportFile;
}
/**
* Delete the workspace
*/
public void delete() {
workspaces.deleteWorkspace(workspaceId);
}
}