From 0995ccf6182620cf5ec26317d9bc660048f7c895 Mon Sep 17 00:00:00 2001 From: felmer <felmer> Date: Wed, 25 Feb 2009 11:14:49 +0000 Subject: [PATCH] LMS-768 rename 'etlserver' project to 'datastore_server' SVN: 9972 --- datastore_server/.checkstyle | 9 + datastore_server/.classpath | 23 + datastore_server/.project | 23 + datastore_server/build/antrun.sh | 6 + datastore_server/build/build.xml | 82 ++ .../dist/data/incoming/.gitignore | 0 datastore_server/dist/data/store/.gitignore | 0 datastore_server/dist/etc/etlserver.conf | 18 + datastore_server/dist/etc/log.xml | 55 ++ datastore_server/dist/etc/openBIS.keystore | Bin 0 -> 1426 bytes datastore_server/dist/etc/service.properties | 110 +++ datastore_server/dist/etlserver.bat | 3 + datastore_server/dist/etlserver.sh | 250 +++++ datastore_server/dist/lib/.gitignore | 0 datastore_server/dist/log/.gitignore | 0 datastore_server/etc/log.xml | 19 + datastore_server/etc/service.properties | 111 +++ .../resource/dependency-structure.ddf | 18 + .../eclipse/ETL Server Fast Suite.launch | 22 + .../resource/eclipse/ETL Server.launch | 13 + .../AbstractDataSetInfoExtractor.java | 57 ++ .../etlserver/AbstractStorageProcessor.java | 65 ++ .../cisd/etlserver/BDSStorageProcessor.java | 598 ++++++++++++ .../cisd/etlserver/BaseDirectoryHolder.java | 68 ++ .../cisd/etlserver/ChannelSetHelper.java | 106 ++ .../cisd/etlserver/DataSetInformation.java | 274 ++++++ .../DataSetNameEntitiesProvider.java | 101 ++ .../cisd/etlserver/DataStoreStrategyKey.java | 50 + .../cisd/etlserver/DataStrategyStore.java | 192 ++++ .../DefaultDataSetInfoExtractor.java | 246 +++++ .../etlserver/DefaultStorageProcessor.java | 87 ++ .../cisd/etlserver/ETLServerPlugin.java | 67 ++ .../etlserver/EncapsulatedLimsService.java | 211 ++++ .../cisd/etlserver/FileBasedFile.java | 187 ++++ .../cisd/etlserver/FileBasedFileFactory.java | 98 ++ .../systemsx/cisd/etlserver/FileRenamer.java | 83 ++ .../cisd/etlserver/HCSImageCheckList.java | 173 ++++ .../HCSImageFileExtractionResult.java | 82 ++ .../cisd/etlserver/IDataSetInfoExtractor.java | 75 ++ .../cisd/etlserver/IDataStoreStrategy.java | 55 ++ .../cisd/etlserver/IDataStrategyStore.java | 43 + .../cisd/etlserver/IETLServerPlugin.java | 41 + .../etlserver/IEncapsulatedLimsService.java | 75 ++ .../ch/systemsx/cisd/etlserver/IFile.java | 58 ++ .../systemsx/cisd/etlserver/IFileFactory.java | 30 + .../cisd/etlserver/IHCSImageFileAccepter.java | 37 + .../etlserver/IHCSImageFileExtractor.java | 44 + .../IProcedureAndDataTypeExtractor.java | 55 ++ .../systemsx/cisd/etlserver/IProcessor.java | 42 + .../cisd/etlserver/IProcessorFactory.java | 38 + .../cisd/etlserver/IStorageProcessor.java | 90 ++ .../etlserver/IStoreRootDirectoryHolder.java | 41 + .../etlserver/IdentifiedDataStrategy.java | 180 ++++ .../java/ch/systemsx/cisd/etlserver/Main.java | 497 ++++++++++ .../cisd/etlserver/NamedDataStrategy.java | 94 ++ .../systemsx/cisd/etlserver/Parameters.java | 403 ++++++++ .../cisd/etlserver/PlateDimension.java | 73 ++ .../cisd/etlserver/PlateDimensionParser.java | 120 +++ .../PropertiesBasedETLServerPlugin.java | 123 +++ .../cisd/etlserver/SimpleTypeExtractor.java | 93 ++ .../cisd/etlserver/StandardProcessor.java | 187 ++++ .../etlserver/StandardProcessorFactory.java | 135 +++ .../cisd/etlserver/ThreadParameters.java | 146 +++ .../etlserver/TransferredDataSetHandler.java | 743 ++++++++++++++ .../etlserver/imsb/HCSImageFileExtractor.java | 335 +++++++ .../AbstractDataSetInfoExtractorFor3V.java | 108 +++ ...ataSetInfoExtractorForDataAcquisition.java | 83 ++ .../DataSetInfoExtractorForImageAnalysis.java | 83 ++ .../threev/HCSImageFileExtractor.java | 211 ++++ .../sourceTest/bash/createFakeDataSet.sh | 179 ++++ .../etlserver/BDSStorageProcessorTest.java | 539 +++++++++++ .../cisd/etlserver/ChannelSetHelperTest.java | 68 ++ .../etlserver/CodeExtractortTestCase.java | 44 + .../DataSetNameEntitiesProviderTest.java | 94 ++ .../cisd/etlserver/DataStrategyStoreTest.java | 261 +++++ .../DefaultDataSetInfoExtractorTest.java | 155 +++ .../DefaultStorageProcessorTest.java | 157 +++ .../EncapsulatedLimsServiceTest.java | 143 +++ .../cisd/etlserver/FileBasedFileTest.java | 87 ++ .../cisd/etlserver/HCSImageCheckListTest.java | 108 +++ .../etlserver/IdentifiedDataStrategyTest.java | 141 +++ .../ch/systemsx/cisd/etlserver/MainTest.java | 136 +++ .../cisd/etlserver/NamedDataStrategyTest.java | 127 +++ .../etlserver/SimpleTypeExtractorTest.java | 68 ++ .../StandardProcessingFactoryTest.java | 127 +++ .../cisd/etlserver/StandardProcessorTest.java | 209 ++++ .../cisd/etlserver/ThreadParametersTest.java | 48 + .../TransferredDataSetHandlerTest.java | 905 ++++++++++++++++++ .../imsb/HCSImageFileExtractorTest.java | 247 +++++ ...etInfoExtractorForDataAcquisitionTest.java | 119 +++ ...aSetInfoExtractorForImageAnalysisTest.java | 70 ++ .../threev/HCSImageFileExtractorTest.java | 186 ++++ datastore_server/sourceTest/java/tests.xml | 14 + .../sourceTest/java/tests_fast.xml | 15 + 94 files changed, 12092 insertions(+) create mode 100644 datastore_server/.checkstyle create mode 100644 datastore_server/.classpath create mode 100644 datastore_server/.project create mode 100755 datastore_server/build/antrun.sh create mode 100644 datastore_server/build/build.xml create mode 100644 datastore_server/dist/data/incoming/.gitignore create mode 100644 datastore_server/dist/data/store/.gitignore create mode 100644 datastore_server/dist/etc/etlserver.conf create mode 100644 datastore_server/dist/etc/log.xml create mode 100644 datastore_server/dist/etc/openBIS.keystore create mode 100644 datastore_server/dist/etc/service.properties create mode 100755 datastore_server/dist/etlserver.bat create mode 100755 datastore_server/dist/etlserver.sh create mode 100644 datastore_server/dist/lib/.gitignore create mode 100644 datastore_server/dist/log/.gitignore create mode 100644 datastore_server/etc/log.xml create mode 100644 datastore_server/etc/service.properties create mode 100644 datastore_server/resource/dependency-structure.ddf create mode 100644 datastore_server/resource/eclipse/ETL Server Fast Suite.launch create mode 100644 datastore_server/resource/eclipse/ETL Server.launch create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/AbstractDataSetInfoExtractor.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/AbstractStorageProcessor.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/BDSStorageProcessor.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/BaseDirectoryHolder.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/ChannelSetHelper.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/DataSetInformation.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/DataSetNameEntitiesProvider.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/DataStoreStrategyKey.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/DataStrategyStore.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/DefaultDataSetInfoExtractor.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/DefaultStorageProcessor.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/ETLServerPlugin.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/EncapsulatedLimsService.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/FileBasedFile.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/FileBasedFileFactory.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/FileRenamer.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/HCSImageCheckList.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/HCSImageFileExtractionResult.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/IDataSetInfoExtractor.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/IDataStoreStrategy.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/IDataStrategyStore.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/IETLServerPlugin.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/IEncapsulatedLimsService.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/IFile.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/IFileFactory.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/IHCSImageFileAccepter.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/IHCSImageFileExtractor.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/IProcedureAndDataTypeExtractor.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/IProcessor.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/IProcessorFactory.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/IStorageProcessor.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/IStoreRootDirectoryHolder.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/IdentifiedDataStrategy.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/Main.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/NamedDataStrategy.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/Parameters.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/PlateDimension.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/PlateDimensionParser.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/PropertiesBasedETLServerPlugin.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/SimpleTypeExtractor.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/StandardProcessor.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/StandardProcessorFactory.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/ThreadParameters.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/TransferredDataSetHandler.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/imsb/HCSImageFileExtractor.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/threev/AbstractDataSetInfoExtractorFor3V.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/threev/DataSetInfoExtractorForDataAcquisition.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/threev/DataSetInfoExtractorForImageAnalysis.java create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/threev/HCSImageFileExtractor.java create mode 100755 datastore_server/sourceTest/bash/createFakeDataSet.sh create mode 100644 datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/BDSStorageProcessorTest.java create mode 100644 datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/ChannelSetHelperTest.java create mode 100644 datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/CodeExtractortTestCase.java create mode 100644 datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/DataSetNameEntitiesProviderTest.java create mode 100644 datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/DataStrategyStoreTest.java create mode 100644 datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/DefaultDataSetInfoExtractorTest.java create mode 100644 datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/DefaultStorageProcessorTest.java create mode 100644 datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/EncapsulatedLimsServiceTest.java create mode 100644 datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/FileBasedFileTest.java create mode 100644 datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/HCSImageCheckListTest.java create mode 100644 datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/IdentifiedDataStrategyTest.java create mode 100644 datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/MainTest.java create mode 100644 datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/NamedDataStrategyTest.java create mode 100644 datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/SimpleTypeExtractorTest.java create mode 100644 datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/StandardProcessingFactoryTest.java create mode 100644 datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/StandardProcessorTest.java create mode 100644 datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/ThreadParametersTest.java create mode 100644 datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/TransferredDataSetHandlerTest.java create mode 100644 datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/imsb/HCSImageFileExtractorTest.java create mode 100644 datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/threev/DataSetInfoExtractorForDataAcquisitionTest.java create mode 100644 datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/threev/DataSetInfoExtractorForImageAnalysisTest.java create mode 100644 datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/threev/HCSImageFileExtractorTest.java create mode 100644 datastore_server/sourceTest/java/tests.xml create mode 100644 datastore_server/sourceTest/java/tests_fast.xml diff --git a/datastore_server/.checkstyle b/datastore_server/.checkstyle new file mode 100644 index 00000000000..f048a51b530 --- /dev/null +++ b/datastore_server/.checkstyle @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> +<fileset-config file-format-version="1.2.0" simple-config="false"> + <local-check-config name="CISD Checks" location="/build_resources/checkstyle/cisd_checkstyle.xml" type="project" description=""> + <additional-data name="protect-config-file" value="false"/> + </local-check-config> + <fileset name="all" enabled="true" check-config-name="CISD Checks" local="true"> + <file-match-pattern match-pattern=".+ch/systemsx/cisd.+" include-pattern="true"/> + </fileset> +</fileset-config> diff --git a/datastore_server/.classpath b/datastore_server/.classpath new file mode 100644 index 00000000000..105dc732c45 --- /dev/null +++ b/datastore_server/.classpath @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<classpath> + <classpathentry kind="src" path="source/java"/> + <classpathentry kind="src" path="sourceTest/java"/> + <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/> + <classpathentry combineaccessrules="false" kind="src" path="/common"/> + <classpathentry kind="lib" path="/libraries/log4j/log4j.jar" sourcepath="/libraries/log4j/src.zip"/> + <classpathentry kind="lib" path="/libraries/commons-lang/commons-lang.jar" sourcepath="/libraries/commons-lang/src.zip"/> + <classpathentry kind="lib" path="/libraries/commons-io/commons-io.jar" sourcepath="/libraries/commons-io/src.zip"/> + <classpathentry kind="lib" path="/libraries/testng/testng-jdk15.jar" sourcepath="/libraries/testng/src.zip"/> + <classpathentry kind="lib" path="/libraries/mail/mail.jar"/> + <classpathentry kind="lib" path="/libraries/jmock/jmock.jar"/> + <classpathentry combineaccessrules="false" kind="src" path="/bds"/> + <classpathentry kind="lib" path="/libraries/restrictionchecker/restrictions.jar"/> + <classpathentry combineaccessrules="false" kind="src" path="/args4j"/> + <classpathentry kind="lib" path="/libraries/cglib/cglib-nodep.jar"/> + <classpathentry kind="lib" path="/libraries/jmock/hamcrest/hamcrest-core.jar"/> + <classpathentry kind="lib" path="/libraries/jmock/hamcrest/hamcrest-library.jar"/> + <classpathentry kind="lib" path="/libraries/jmock/objenesis/objenesis-1.0.jar"/> + <classpathentry combineaccessrules="false" kind="src" path="/openbis"/> + <classpathentry combineaccessrules="false" kind="src" path="/server-common"/> + <classpathentry kind="output" path="targets/classes"/> +</classpath> diff --git a/datastore_server/.project b/datastore_server/.project new file mode 100644 index 00000000000..834c47cfcc7 --- /dev/null +++ b/datastore_server/.project @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>datastore_server</name> + <comment></comment> + <projects> + </projects> + <buildSpec> + <buildCommand> + <name>org.eclipse.jdt.core.javabuilder</name> + <arguments> + </arguments> + </buildCommand> + <buildCommand> + <name>com.atlassw.tools.eclipse.checkstyle.CheckstyleBuilder</name> + <arguments> + </arguments> + </buildCommand> + </buildSpec> + <natures> + <nature>org.eclipse.jdt.core.javanature</nature> + <nature>com.atlassw.tools.eclipse.checkstyle.CheckstyleNature</nature> + </natures> +</projectDescription> diff --git a/datastore_server/build/antrun.sh b/datastore_server/build/antrun.sh new file mode 100755 index 00000000000..5ff1a62b259 --- /dev/null +++ b/datastore_server/build/antrun.sh @@ -0,0 +1,6 @@ +#! /bin/bash + +ME="$0" +MYDIR=${ME%/*} +cd $MYDIR +ant -lib ../../build_resources/lib/ecj.jar "$@" diff --git a/datastore_server/build/build.xml b/datastore_server/build/build.xml new file mode 100644 index 00000000000..6d5817155d4 --- /dev/null +++ b/datastore_server/build/build.xml @@ -0,0 +1,82 @@ +<project name="etlserver" default="dist" basedir=".."> + <import file="../../build_resources/ant/build-common.xml" /> + <project-classpath name="ecp" classes="${classes}" /> + + <property name="original.dist" value="dist" /> + <property name="mainfolder" value="etlserver" /> + <property name="dist.etlserver" value="${dist}/${mainfolder}" /> + <property name="dist.etlserver.lib" value="${dist.etlserver}/lib" /> + <property name="jar.file" value="${dist.etlserver.lib}/etlserver.jar" /> + <property name="dist.file.prefix" value="${dist}/etlserver" /> + <property name="nativesrc" value="${lib}/unix/native" /> + <property name="nativeroot" value="${targets}/ant" /> + <property name="native" value="${nativeroot}/native" /> + + <target name="clean"> + <delete dir="${dist}" /> + </target> + + <target name="compile" depends="build-common.compile, clean" /> + + <target name="run-tests"> + <antcall target="build-common.run-tests"> + <param name="test.suite" value="tests_fast.xml" /> + </antcall> + </target> + + <target name="jar" depends="compile"> + <mkdir dir="${dist.etlserver.lib}" /> + <build-info revision="revision.number" version="version.number" clean="clean.flag" /> + <echo file="${build.info.file}">${version.number}:${revision.number}:${clean.flag}</echo> + <copy todir="${native}"> + <fileset dir="${nativesrc}"> + <include name="**/unix.so" /> + </fileset> + </copy> + <recursive-jar destfile="${jar.file}"> + <fileset dir="${classes}"> + <include name="**/*.class" /> + <include name="${build.info.filename}" /> + </fileset> + <fileset dir="${nativeroot}"> + <include name="**/unix.so"/> + </fileset> + <manifest> + <attribute name="Main-Class" value="ch.systemsx.cisd.etlserver.Main" /> + <attribute name="Class-Path" + value="etlserver-plugins.jar log4j.jar activation.jar mail.jar spring.jar fast-md5.jar + commons-codec.jar commons-lang.jar commons-io.jar commons-logging.jar commons-httpclient.jar" /> + <attribute name="Version" value="${version.number}" /> + <attribute name="Build-Number" + value="${version.number} (r${revision.number},${clean.flag})" /> + </manifest> + </recursive-jar> + </target> + + <target name="dist" depends="jar"> + <copy file="${lib}/activation/activation.jar" todir="${dist.etlserver.lib}" /> + <copy file="${lib}/mail/mail.jar" todir="${dist.etlserver.lib}" /> + <copy file="${lib}/log4j/log4j.jar" todir="${dist.etlserver.lib}" /> + <copy file="${lib}/commons-codec/commons-codec.jar" todir="${dist.etlserver.lib}" /> + <copy file="${lib}/commons-io/commons-io.jar" todir="${dist.etlserver.lib}" /> + <copy file="${lib}/commons-lang/commons-lang.jar" todir="${dist.etlserver.lib}" /> + <copy file="${lib}/commons-logging/commons-logging.jar" todir="${dist.etlserver.lib}" /> + <copy file="${lib}/commons-httpclient/commons-httpclient.jar" + todir="${dist.etlserver.lib}" /> + <copy file="${lib}/spring/spring.jar" todir="${dist.etlserver.lib}" /> + <copy file="${lib}/fast-md5/fast-md5.jar" todir="${dist.etlserver.lib}" /> + <property name="dist.file" + value="${dist.file.prefix}-${version.number}-r${revision.number}.zip" /> + <zip basedir="${dist}" destfile="${dist.file}"> + <zipfileset dir="${original.dist}" excludes="**/etlserver.sh" prefix="${mainfolder}" /> + <zipfileset file="${original.dist}/etlserver.sh" + filemode="755" + prefix="${mainfolder}" /> + </zip> + <delete dir="${dist.etlserver}" /> + </target> + + <target name="ci" depends="run-tests, check-dependencies, dist"> + </target> + +</project> \ No newline at end of file diff --git a/datastore_server/dist/data/incoming/.gitignore b/datastore_server/dist/data/incoming/.gitignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/datastore_server/dist/data/store/.gitignore b/datastore_server/dist/data/store/.gitignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/datastore_server/dist/etc/etlserver.conf b/datastore_server/dist/etc/etlserver.conf new file mode 100644 index 00000000000..f51846de0bd --- /dev/null +++ b/datastore_server/dist/etc/etlserver.conf @@ -0,0 +1,18 @@ +# +# ETL Server configuration file +# + +# +# Home directory of the JRE that should be used +# +#JAVA_HOME=${JAVA_HOME:=/usr/java/latest} + +# +# Options to the JRE +# +JAVA_OPTS=${JAVA_OPTS:=-server} + +# +# Maximal number of log files to keep +# +MAXLOGS=5 \ No newline at end of file diff --git a/datastore_server/dist/etc/log.xml b/datastore_server/dist/etc/log.xml new file mode 100644 index 00000000000..41d695d4d78 --- /dev/null +++ b/datastore_server/dist/etc/log.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd"> + +<log4j:configuration xmlns:log4j='http://jakarta.apache.org/log4j/'> + + <appender name="DEFAULT" class="org.apache.log4j.DailyRollingFileAppender"> + + <param name="File" value="log/etlserver_log.txt"/> + <param name="DatePattern" value="'.'yyyy-MM-dd"/> + + <layout class="org.apache.log4j.PatternLayout"> + <param name="ConversionPattern" value="%d %-5p [%t] %c - %m%n"/> + </layout> + + </appender> + + <appender name="STDOUT" class="org.apache.log4j.ConsoleAppender"> + <layout class="org.apache.log4j.PatternLayout"> + <param name="ConversionPattern" value="%d %-5p [%t] %c - %m%n"/> + </layout> + </appender> + + <appender name="NULL" class="org.apache.log4j.varia.NullAppender" /> + + <appender name="EMAIL" class="org.apache.log4j.net.SMTPAppender"> + + <param name="BufferSize" value="512" /> + <param name="SMTPHost" value="localhost" /> + <param name="From" value="etlserver@localhost" /> + <param name="To" value="root@localhost" /> + <param name="Subject" value="ATTENTION: etl server" /> + <param name="EvaluatorClass" value="ch.systemsx.cisd.common.logging.AlwaysTrueTriggeringEventEvaluator" /> + + <layout class="org.apache.log4j.PatternLayout"> + <param name="ConversionPattern" value="%d %-5p [%t] %c - %m%n"/> + </layout> + + <!--filter class="org.apache.log4j.varia.LevelRangeFilter"> + <param name="LevelMin" value="ERROR"/> + <param name="LevelMax" value="FATAL"/> + </filter--> + + </appender> + + <category name="NOTIFY"> + <priority value="info" /> + <appender-ref ref="EMAIL" /> + </category> + + <root> + <priority value="info" /> + <appender-ref ref="DEFAULT" /> + </root> + +</log4j:configuration> diff --git a/datastore_server/dist/etc/openBIS.keystore b/datastore_server/dist/etc/openBIS.keystore new file mode 100644 index 0000000000000000000000000000000000000000..b727bd0fb777fddb3463c81cb56963a7541f7b45 GIT binary patch literal 1426 zcmezO_TO6u1_mY|W&~r_+{*0KN+9!5EW=$#pv*3VCZ=r$d~96WY>X_7T1<kBjI0bS zO-zd#wG~&$%iMm`c71|$Y5$YitEL_J$ZnqF`)W$d-o2eMs`U?M6zI7t-(A`+-cpqw zHFxi9-Rrrt%0A4!7X5I`p&hRtn0Nl1W1MMabV=o@O##>LX-9cnmv_92vyZY^nksPD zdgGz_%YMiA%<+G`?zc{_^OTmKsam}GdFt7neK!{P|Bco8Yq|9PvTGT0HqQC!@x9l1 z+kfrLjk$U4PfrzX4>?lEY_X)NU`c_upzVKwLzZV_Z=OB7;%H;~#q=-kB7C15mGAK^ zvQDWpwV1Q=%|)J*vS+h8eD_X&KBLa;y!Ym4fd?y?u6<@yiQRcgW4YSWJ1qI-Q@4qE za4m1Vxao>O&#^E1)!s>&+m1~AFk#ErRgE9c$R|1RHZn>ST)F2PCX`h;GftH|P3C*s zWzB^ZGW^U2Io6G9<Cm+dn@b%$@cxkDi?0v*4*&Xhu{qw<UFz0*cKw3~R!=THb>jl> z6zzz+mrM40Funf&SL@T49sAWnZvUNcc-}kaM`;+(`I*O}e=OTwz2vdB%C^t0KV2dN z8|5bLIJC=m`3p1S>EdUM&gly9o_)k?62R=&u_P~(KYzPEv+V7u$GBF7Yt*targsEg zTQzgVqnKmgxtO%H)!0fto_uWX^v1c;QdM?#toT{0*tgvCKBx=r`VraapZ2`+cgggB z-Pvqw9={ifyv<wt>(DLz=%jkh_cpEkNBn#fQtx!K-V#22ApgPb-K(wNrdO3Et=(zk zH%r#4Uj9|=lZlRA5;d`xvpUaban7~tb)L?vqO<o@r$c({0U_Q-$DE%pD%yDqwYKp- ze(+{JQ~%8mo`p*G>(?31t6P_KXXUmI|NTdEW^*XD%W$kc6J6C`ZJL<Zk@PWEN6V*F zkgcZ1#>_TRc(ux#yKj=Ue_0-Rb!XA^BcIEU<sO}THR1C69iJfyIzrFXz!I2{Cjk?3 zpFtDjcP19gPahu`@Un4gwRyCC=VfGMVP!CA>@^esCUfRc7B*q_(7f!t{PH}Q2nU7; zJ3>UpKnkRbOIX-9zo<mPDX~()IX|zsG^ZppFWpeVKn^6!EiCR|kea6uoL^d$oT}iG zT9%rVUyz%cS7In?APiE$EX?DTT2fM}5S&_6mRe*WC(dhNU}#`uWN2(^YGM)v<Qf_n z8W}*jgU-E;^N~XoSVk~6_5wq-lc}+hVe40umt9-5IP9-{sGrmj5_UQxbn3f#Rhcal zb$96Tc-UWFAhSV0y0D>s$!bwcyY3l3?6uy+&YLB9AjrXwMJ9=TVxnh^gTTRrgQDAx z_HixoIbPSC|G?I^w5omSz6+<W?mi^z$(h6X=Z@>C2WR&Q742Af!tZ{F_B0c(^#Lh& zza|JWF*7nSB0CQl;mkmH*$SFDWnY}}>VU(o3#<5V+huGG`Dn2??1_qdd2)==i--F? zs#KSqOx?Wrm7jRA(!0Yuu5HVhrWZZce5Lb}5Bq8!uvR=V-JM%7`INM|?JE9h{&PR9 zPS5OYv=7|qeAeX{<0}8%Th2N96wN*x(D1*r!0!3JEkDAd&tKmoUZ_-IpH*z9SS<f# VnWy8+Ul;a#sHi_<p_OCR1^^CaPjmnP literal 0 HcmV?d00001 diff --git a/datastore_server/dist/etc/service.properties b/datastore_server/dist/etc/service.properties new file mode 100644 index 00000000000..3392d3b2f92 --- /dev/null +++ b/datastore_server/dist/etc/service.properties @@ -0,0 +1,110 @@ +# The root directory of the data store +storeroot-dir = data/store + +# The check interval (in seconds) +check-interval = 60 + +# The time-out for clean up work in the shutdown sequence (in seconds). +# Note that that the maximal time for the shutdown sequence to complete can be as large +# as twice this time. +# Remark: On a network file system, it is not recommended to turn this value to something +# lower than 180. +shutdown-timeout = 180 + +# If free disk space goes below value defined here, a notification email will be sent. +# Value must be specified in kilobytes (1048576 = 1024 * 1024 = 1GB). If no high water mark is +# specified or if value is negative, the system will not be watching. +highwater-mark = 1048576 + +# If a data set is successfully registered it sends out an email to the registrator. +# If this property is not specified, no email is sent to the registrator. This property +# does not affect the mails which are sent, when the data set could not be registered. +notify-successful-registration = false + +# The URL of the openBIS server +server-url = https://localhost:8443/openbis + +# The username to use when contacting the openBIS server +username = etlserver + +# The password to use when contacting the openBIS server +password = <change this> + +# SMTP properties (must start with 'mail' to be considered). +# mail.smtp.host = localhost +# mail.from = etlserver@localhost + +# ---------------- Timing parameters for file system operations on remote shares. + +# Time (in seconds) to wait for any file system operation to finish. Operations exceeding this +# timeout will be terminated. +timeout = 60 +# Number of times that a timed out operation will be tried again (0 means: every file system +# operation will only ever be performed once). +max-retries = 11 +# Time (in seconds) to wait after an operation has been timed out before re-trying. +failure-interval = 10 + +# Globally used separator character which separates entities in a data set file name +data-set-file-name-entity-separator = _ + +# Prefixes for processing paths for all procedure types. +# default-prefix-for-absolute-paths is the key for paths starting with '/'. +# default-prefix-for-relative-paths is the key for paths not starting with '/'. +# +default-prefix-for-absolute-paths = + +# Processors of processing instructions. +# +# processors: comma separated list of procedure type codes +# processor.<procedure type code>.prefix-for-absolute-paths: Key for a processing path starting with '/'. +# processor.<procedure type code>.prefix-for-relative-paths: Key for a processing path not starting with '/'. +# processor.<procedure type code>.parameters-file: Name of the file containing the processing parameters. +# processor.<procedure type code>.finished-file-template: Name of the marker file which finishes processing. + +processors = DATA_ACQUISITION +processor.DATA_ACQUISITION.prefix-for-absolute-paths = ${default-prefix-for-absolute-paths} +processor.DATA_ACQUISITION.prefix-for-relative-paths = targets/processing +processor.DATA_ACQUISITION.parameters-file = parameters +processor.DATA_ACQUISITION.data-set-code-prefix-glue = ${data-set-file-name-entity-separator} +processor.DATA_ACQUISITION.finished-file-template = .MARKER_is_finished_{0} +# Can be one of PROPRIETARY (the data as acquired from the measurement device) +# or BDS_DIRECTORY (the data in BDS format in a directory container). +processor.DATA_ACQUISITION.input-storage-format = PROPRIETARY + +# Comma separated names of processing threads. Each thread should have configuration properties prefixed with its name. +# E.g. 'code-extractor' property for the thread 'my-etl' should be specified as 'my-etl.code-extractor' +inputs=main-thread + +# --------------------------------------------------------------------------- +# 'main-thread' thread configuration +# --------------------------------------------------------------------------- + +# The directory to watch for incoming data. +main-thread.incoming-dir = data/incoming +# The group the samples extracted by this thread belong to. If commented out or empty, then samples +# are considered associated to a database instance (not group private). +# main-thread.group-code = <change this> + +# ---------------- Plugin properties + +# The extractor class to use for code extraction +main-thread.data-set-info-extractor = ch.systemsx.cisd.etlserver.DefaultDataSetInfoExtractor +# Separator used to extract the barcode in the data set file name +main-thread.data-set-info-extractor.entity-separator = ${data-set-file-name-entity-separator} + +# The extractor class to use for type extraction +main-thread.type-extractor = ch.systemsx.cisd.etlserver.SimpleTypeExtractor +main-thread.type-extractor.file-format-type = TIFF +main-thread.type-extractor.locator-type = RELATIVE_LOCATION +main-thread.type-extractor.data-set-type = HCS_IMAGE +main-thread.type-extractor.procedure-type = DATA_ACQUISITION + +# The storage processor (IStorageProcessor implementation) +main-thread.storage-processor = ch.systemsx.cisd.etlserver.DefaultStorageProcessor +# main-thread.storage-processor = ch.systemsx.cisd.etlserver.BDSStorageProcessor +# main-thread.storage-processor.version = 1.1 +# main-thread.storage-processor.sampleTypeCode = CELL_PLATE +# main-thread.storage-processor.sampleTypeDescription = Screening Plate +# main-thread.storage-processor.format = UNKNOWN V1.0 + diff --git a/datastore_server/dist/etlserver.bat b/datastore_server/dist/etlserver.bat new file mode 100755 index 00000000000..5da5824e65f --- /dev/null +++ b/datastore_server/dist/etlserver.bat @@ -0,0 +1,3 @@ +@echo off + +java -Djavax.net.ssl.trustStore=etc\openBIS.keystore -jar lib\etlserver.jar %1 %2 %3 %4 %5 %6 %7 diff --git a/datastore_server/dist/etlserver.sh b/datastore_server/dist/etlserver.sh new file mode 100755 index 00000000000..2b1199085d2 --- /dev/null +++ b/datastore_server/dist/etlserver.sh @@ -0,0 +1,250 @@ +#!/bin/bash +# +# Control script for CISD openBIS ETL Server on Unix / Linux systems +# ------------------------------------------------------------------------- + +awkBin() +{ + # We need a awk that accepts variable assignments with '-v' + case `uname -s` in + "SunOS") + echo "nawk" + return + ;; + esac + # default + echo "awk" +} + +isPIDRunning() +{ + if [ "$1" = "" ]; then + return 1 + fi + if [ "$1" = "fake" ]; then # for unit tests + return 0 + fi + # This will have a return value of 0 on BSDish systems + isBSD="`ps aux > /dev/null 2>&1; echo $?`" + AWK=`awkBin` + if [ "$isBSD" = "0" ]; then + if [ "`ps aux | $AWK -v PID=$1 '{if ($2==PID) {print "FOUND"}}'`" = "FOUND" ]; then + return 0 + else + return 1 + fi + else + if [ "`ps -ef | $AWK -v PID=$1 '{if ($2==PID) {print "FOUND"}}'`" = "FOUND" ]; then + return 0 + else + return 1 + fi + fi +} + +rotateLogFiles() +{ + logfile=$1 + max=$2 + if [ -z "$logfile" ]; then + echo "Error: rotateLogFiles: logfile argument missing" + return 1 + fi + if [ -z "$max" ]; then + echo "Error: rotateLogFiles: max argument missing" + return 1 + fi + test -f $logfile.$max && rm $logfile.$max + n=$max + while [ $n -gt 1 ]; do + nnew=$(($n-1)) + test -f $logfile.$nnew && mv $logfile.$nnew $logfile.$n + n=$nnew + done + test -f $logfile && mv $logfile $logfile.1 +} + +getStatus() +{ + if [ -f $PIDFILE ]; then + PID=`cat $PIDFILE` + isPIDRunning $PID + if [ $? -eq 0 ]; then + return 0 + else + return 1 + fi + else + return 2 + fi +} + +printStatus() +{ + if [ -f $PIDFILE ]; then + PID=`cat $PIDFILE` + isPIDRunning $PID + if [ $? -eq 0 ]; then + echo "ETL Server is running (pid $PID)" + return 0 + else + echo "ETL Server is dead (stale pid $PID)" + return 1 + fi + else + echo "ETL Server is not running." + return 2 + fi +} + +# +# definitions +# + +PIDFILE=${ETLSERVER_PID:-etlserver.pid} +CONFFILE=etc/etlserver.conf +LOGFILE=log/etlserver_log.txt +STARTUPLOG=log/startup_log.txt +SUCCESS_MSG="etlserver ready and waiting for data" +JAR_FILE=lib/etlserver.jar +MAX_LOOPS=10 + +# +# change to installation directory +# +bin=$0 +if [ -L $bin ]; then + bin=`dirname $bin`/`readlink $bin` +fi +WD=`dirname $bin` +cd $WD +SCRIPT=./`basename $0` + +# +# source configuration script, if any +# +test -f $CONFFILE && source $CONFFILE +if [ "$JAVA_HOME" != "" ]; then + JAVA_BIN="$JAVA_HOME/bin/java" +else + JAVA_BIN="java" +fi + +command=$1 +ALL_JAVA_OPTS="-Djavax.net.ssl.trustStore=etc/openBIS.keystore $JAVA_OPTS" +# ensure that we ignore a possible prefix "--" for any command +command="${command#--*}" +case "$command" in + start) + getStatus + EXIT_STATUS=$? + if [ $EXIT_STATUS -eq 0 ]; then + echo "Cannot start ETL Server: already running." + exit 100 + fi + + echo -n "Starting ETL Server " + rotateLogFiles $LOGFILE $MAXLOGS + shift 1 + ${JAVA_BIN} ${ALL_JAVA_OPTS} -jar $JAR_FILE "$@" > $STARTUPLOG 2>&1 & echo $! > $PIDFILE + if [ $? -eq 0 ]; then + # wait for initial self-test to finish + n=0 + while [ $n -lt $MAX_LOOPS ]; do + sleep 1 + if [ ! -f $PIDFILE ]; then + break + fi + if [ -s $STARTUPLOG ]; then + PID=`cat $PIDFILE 2> /dev/null` + isPIDRunning $PID + if [ $? -ne 0 ]; then + break + fi + fi + grep "$SUCCESS_MSG" $LOGFILE > /dev/null 2>&1 + if [ $? -eq 0 ]; then + break + fi + n=$(($n+1)) + done + PID=`cat $PIDFILE 2> /dev/null` + isPIDRunning $PID + if [ $? -eq 0 ]; then + grep "$SUCCESS_MSG" $LOGFILE > /dev/null 2>&1 + if [ $? -ne 0 ]; then + echo "(pid $PID - WARNING: SelfTest not yet finished)" + else + echo "(pid $PID)" + fi + else + echo "FAILED" + if [ -s $STARTUPLOG ]; then + echo "startup log says:" + cat $STARTUPLOG + else + echo "log file says:" + tail $LOGFILE + fi + fi + else + echo "FAILED" + fi + ;; + stop) + echo -n "Stopping ETL Server " + if [ -f $PIDFILE ]; then + PID=`cat $PIDFILE 2> /dev/null` + isPIDRunning $PID + if [ $? -eq 0 ]; then + kill $PID + n=0 + while [ $n -lt $MAX_LOOPS ]; do + isPIDRunning $PID + if [ $? -ne 0 ]; then + break + fi + sleep 1 + n=$(($n+1)) + done + isPIDRunning $PID + if [ $? -ne 0 ]; then + echo "(pid $PID)" + test -f $PIDFILE && rm $PIDFILE 2> /dev/null + else + echo "FAILED" + fi + else + if [ -f $PIDFILE ]; then + rm $PIDFILE 2> /dev/null + echo "(was dead - cleaned up pid file)" + fi + fi + else + echo "(not running - nothing to do)" + fi + ;; + status) + printStatus + EXIT_STATUS=$? + exit $EXIT_STATUS + ;; + restart) + $SCRIPT stop + $SCRIPT start + ;; + help) + ${JAVA_BIN} ${ALL_JAVA_OPTS} -jar $JAR_FILE --help + ;; + version) + ${JAVA_BIN} ${ALL_JAVA_OPTS} -jar $JAR_FILE --version + ;; + show-shredder) + ${JAVA_BIN} ${ALL_JAVA_OPTS} -jar $JAR_FILE --show-shredder + ;; + *) + echo $"Usage: $0 {start|stop|restart|status|help|version|show-shredder}" + exit 200 + ;; +esac +exit 0 diff --git a/datastore_server/dist/lib/.gitignore b/datastore_server/dist/lib/.gitignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/datastore_server/dist/log/.gitignore b/datastore_server/dist/log/.gitignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/datastore_server/etc/log.xml b/datastore_server/etc/log.xml new file mode 100644 index 00000000000..5cee0a68436 --- /dev/null +++ b/datastore_server/etc/log.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd"> + +<log4j:configuration xmlns:log4j='http://jakarta.apache.org/log4j/'> + + <appender name="STDOUT" class="org.apache.log4j.ConsoleAppender"> + <layout class="org.apache.log4j.PatternLayout"> + <param name="ConversionPattern" value="%d %-5p [%t] %c - %m%n"/> + </layout> + </appender> + + <appender name="NULL" class="org.apache.log4j.varia.NullAppender" /> + + <root> + <priority value ="info" /> + <appender-ref ref="STDOUT" /> + </root> + +</log4j:configuration> diff --git a/datastore_server/etc/service.properties b/datastore_server/etc/service.properties new file mode 100644 index 00000000000..71b9a000811 --- /dev/null +++ b/datastore_server/etc/service.properties @@ -0,0 +1,111 @@ +# The root directory of the data store +storeroot-dir = targets/store + +# The check interval (in seconds) +check-interval = 5 + +# The time-out for clean up work in the shutdown sequence (in seconds). +# Note that that the maximal time for the shutdown sequence to complete can be as large +# as twice this time. +shutdown-timeout = 2 + +# If free disk space goes below value defined here, a notification email will be sent. +# Value must be specified in kilobytes (1048576 = 1024 * 1024 = 1GB). If no high water mark is +# specified or if value is negative, the system will not be watching. +highwater-mark = 1048576 + +# If a data set is successfully registered it sends out an email to the registrator. +# If this property is not specified, no email is sent to the registrator. This property +# does not affect the mails which are sent, when the data set could not be registered. +notify-successful-registration = false + +# The URL of the openBIS server +server-url = http://localhost:8080/openbis + +# The username to use when contacting the openBIS server +username = etlserver + +# The password to use when contacting the openBIS server +password = doesnotmatter + +# SMTP properties (must start with 'mail' to be considered). +# mail.smtp.host = localhost +# mail.from = etlserver@localhost +# mail.smtp.user = +# mail.smtp.password = + +# Maximum number of retries if renaming failed. +# renaming.failure.max-retries = 12 + +# The number of milliseconds to wait before retrying to execute the renaming process. +# renaming.failure.millis-to-sleep = 5000 + +# Globally used separator character which separates entities in a data set file name +data-set-file-name-entity-separator = _ + +# Prefixes for processing paths for all procedure types. +# default-prefix-for-absolute-paths is the key for paths starting with '/'. +# default-prefix-for-relative-paths is the key for paths not starting with '/'. +# +default-prefix-for-absolute-paths = + +# Processors of processing instructions. +# +# processors: comma separated list of procedure type codes +# processor.<procedure type code>.prefix-for-absolute-paths: Key for a processing path starting with '/'. +# processor.<procedure type code>.prefix-for-relative-paths: Key for a processing path not starting with '/'. +# processor.<procedure type code>.parameters-file: Name of the file containing the processing parameters. +# processor.<procedure type code>.finished-file-template: Name of the marker file which finishes processing. + +processors = DATA_ACQUISITION +processor.DATA_ACQUISITION.prefix-for-absolute-paths = ${default-prefix-for-absolute-paths} +processor.DATA_ACQUISITION.prefix-for-relative-paths = targets/processing +processor.DATA_ACQUISITION.parameters-file = parameters +processor.DATA_ACQUISITION.data-set-code-prefix-glue = ${data-set-file-name-entity-separator} +processor.DATA_ACQUISITION.finished-file-template = .MARKER_is_finished_{0} +processor.DATA_ACQUISITION.input-storage-format = BDS_DIRECTORY +# time after which the copy of a single file for processing should complete. +# If that will not happen, operation will be terminated and relaunched. +processor.DATA_ACQUISITION.data-copy-timeout = 10 + +# Comma separated names of processing threads. Each thread should have configuration properties prefixed with its name. +# E.g. 'code-extractor' property for the thread 'my-etl' should be specified as 'my-etl.code-extractor' +inputs=main-thread + +# --------------------------------------------------------------------------- +# 'main-thread' thread configuration +# --------------------------------------------------------------------------- + +# The directory to watch for incoming data. +main-thread.incoming-dir = targets/incoming +# The group the samples extracted by this thread belong to. If commented out or empty, then samples +# are considered associated to a database instance (not group private). +# main-thread.group-code = CISD + +# The store format that should be applied in the incoming directory. +main-thread.incoming-dir.format = + +# ---------------- Plugin properties + +# The extractor plugin class to use for code extraction +main-thread.data-set-info-extractor = ch.systemsx.cisd.etlserver.DefaultDataSetInfoExtractor +# Separator used to extract the barcode in the data set file name +main-thread.data-set-info-extractor.entity-separator = ${data-set-file-name-entity-separator} + +main-thread.type-extractor = ch.systemsx.cisd.etlserver.SimpleTypeExtractor +main-thread.type-extractor.file-format-type = TIFF +main-thread.type-extractor.locator-type = RELATIVE_LOCATION +main-thread.type-extractor.data-set-type = HCS_IMAGE +main-thread.type-extractor.procedure-type = DATA_ACQUISITION + +# The storage processor (IStorageProcessor implementation) +#main-thread.storage-processor = ch.systemsx.cisd.etlserver.DefaultStorageProcessor +main-thread.storage-processor = ch.systemsx.cisd.etlserver.BDSStorageProcessor +main-thread.storage-processor.version = 1.1 +main-thread.storage-processor.sampleTypeCode = CELL_PLATE +main-thread.storage-processor.sampleTypeDescription = Screening Plate +main-thread.storage-processor.format = HCS_IMAGE V1.0 +main-thread.storage-processor.number_of_channels = 2 +main-thread.storage-processor.contains_original_data = TRUE +main-thread.storage-processor.well_geometry = 3x3 +main-thread.storage-processor.file-extractor = ch.systemsx.cisd.etlserver.imsb.HCSImageFileExtractor diff --git a/datastore_server/resource/dependency-structure.ddf b/datastore_server/resource/dependency-structure.ddf new file mode 100644 index 00000000000..f05d29b3583 --- /dev/null +++ b/datastore_server/resource/dependency-structure.ddf @@ -0,0 +1,18 @@ +# +# +# +#show allResults + +{root} = ch.systemsx.cisd +{etlserver} = ${root}.etlserver + +###################################################################### +# Check dependencies to openbis + +{openbis} = ${root}.openbis +[etlserver] = ${etlserver}.* +[private_openbis] = ${openbis}.* excluding ${openbis}.generic.shared.dto.* ${openbis}.generic.shared.IETLLIMSService + +check sets [etlserver] + +check [etlserver] independentOf [private_openbis] diff --git a/datastore_server/resource/eclipse/ETL Server Fast Suite.launch b/datastore_server/resource/eclipse/ETL Server Fast Suite.launch new file mode 100644 index 00000000000..1a5b7c04520 --- /dev/null +++ b/datastore_server/resource/eclipse/ETL Server Fast Suite.launch @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<launchConfiguration type="org.testng.eclipse.launchconfig"> +<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS"> +<listEntry value="/libraries/testng/testng-jdk15.jar"/> +</listAttribute> +<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES"> +<listEntry value="1"/> +</listAttribute> +<booleanAttribute key="org.eclipse.debug.core.appendEnvironmentVariables" value="true"/> +<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="org.testng.remote.RemoteTestNG"/> +<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="datastore_server"/> +<mapAttribute key="org.testng.eclipse.ALL_CLASS_METHODS"/> +<listAttribute key="org.testng.eclipse.CLASS_TEST_LIST"/> +<stringAttribute key="org.testng.eclipse.COMPLIANCE_LEVEL" value="JDK"/> +<listAttribute key="org.testng.eclipse.GROUP_LIST"/> +<listAttribute key="org.testng.eclipse.GROUP_LIST_CLASS"/> +<stringAttribute key="org.testng.eclipse.LOG_LEVEL" value="2"/> +<listAttribute key="org.testng.eclipse.SUITE_TEST_LIST"> +<listEntry value="sourceTest/java/tests_fast.xml"/> +</listAttribute> +<intAttribute key="org.testng.eclipse.TYPE" value="3"/> +</launchConfiguration> diff --git a/datastore_server/resource/eclipse/ETL Server.launch b/datastore_server/resource/eclipse/ETL Server.launch new file mode 100644 index 00000000000..afdd321a796 --- /dev/null +++ b/datastore_server/resource/eclipse/ETL Server.launch @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<launchConfiguration type="org.eclipse.jdt.launching.localJavaApplication"> +<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS"> +<listEntry value="/datastore_server/source/java/ch/systemsx/cisd/etlserver/Main.java"/> +</listAttribute> +<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES"> +<listEntry value="1"/> +</listAttribute> +<booleanAttribute key="org.eclipse.debug.core.appendEnvironmentVariables" value="true"/> +<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="ch.systemsx.cisd.etlserver.Main"/> +<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="datastore_server"/> +<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-ea"/> +</launchConfiguration> diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/AbstractDataSetInfoExtractor.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/AbstractDataSetInfoExtractor.java new file mode 100644 index 00000000000..d6fcec47156 --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/AbstractDataSetInfoExtractor.java @@ -0,0 +1,57 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import java.util.Properties; + +import ch.systemsx.cisd.common.utilities.ExtendedProperties; +import ch.systemsx.cisd.common.utilities.PropertyUtils; + +/** + * An abstract <code>ICodeExtractor</code> implementation. + * + * @author Christian Ribeaud + */ +public abstract class AbstractDataSetInfoExtractor implements IDataSetInfoExtractor +{ + + /** The name of the property to get the experiment separator from. */ + protected static final String ENTITY_SEPARATOR_PROPERTY_NAME = "entity-separator"; + + /** The default entity separator. */ + protected static final char DEFAULT_ENTITY_SEPARATOR = '.'; + + protected static final String STRIP_EXTENSION = "strip-file-extension"; + + protected final Properties properties; + + /** Separator character that divides entities in a data set name. */ + protected final char entitySeparator; + + protected final boolean stripExtension; + + protected AbstractDataSetInfoExtractor(final Properties globalProperties) + { + assert globalProperties != null : "Global properties can not be null."; + properties = ExtendedProperties.getSubset(globalProperties, EXTRACTOR_KEY + '.', true); + stripExtension = PropertyUtils.getBoolean(properties, STRIP_EXTENSION, false); + entitySeparator = + PropertyUtils.getChar(properties, ENTITY_SEPARATOR_PROPERTY_NAME, + DEFAULT_ENTITY_SEPARATOR); + } + +} diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/AbstractStorageProcessor.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/AbstractStorageProcessor.java new file mode 100644 index 00000000000..49eff0618ed --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/AbstractStorageProcessor.java @@ -0,0 +1,65 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import java.io.File; +import java.util.Properties; + +import ch.systemsx.cisd.common.utilities.PropertyUtils; + +/** + * An <code>abtract</code> implementation of <code>IStorageProcessor</code>. + * + * @author Christian Ribeaud + */ +public abstract class AbstractStorageProcessor implements IStorageProcessor +{ + protected final Properties properties; + + private File storeRootDir; + + protected AbstractStorageProcessor(final Properties properties) + { + this.properties = properties; + } + + protected final String getMandatoryProperty(final String propertyKey) + { + return PropertyUtils.getMandatoryProperty(properties, propertyKey); + } + + protected static final void checkParameters(final File incomingDataSetPath, + final File targetPath) + { + assert incomingDataSetPath != null : "Given incoming data set path can not be null."; + assert targetPath != null : "Given target path can not be null."; + } + + // + // IStorageProcessor + // + + public final File getStoreRootDirectory() + { + return storeRootDir; + } + + public final void setStoreRootDirectory(final File storeRootDirectory) + { + this.storeRootDir = storeRootDirectory; + } +} diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/BDSStorageProcessor.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/BDSStorageProcessor.java new file mode 100644 index 00000000000..031f801a490 --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/BDSStorageProcessor.java @@ -0,0 +1,598 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Properties; + +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.time.DurationFormatUtils; +import org.apache.log4j.Logger; + +import ch.systemsx.cisd.bds.Constants; +import ch.systemsx.cisd.bds.DataSet; +import ch.systemsx.cisd.bds.DataStructureFactory; +import ch.systemsx.cisd.bds.DataStructureLoader; +import ch.systemsx.cisd.bds.ExperimentRegistrationTimestamp; +import ch.systemsx.cisd.bds.ExperimentRegistrator; +import ch.systemsx.cisd.bds.Format; +import ch.systemsx.cisd.bds.FormatParameter; +import ch.systemsx.cisd.bds.IFormatParameterFactory; +import ch.systemsx.cisd.bds.IFormattedData; +import ch.systemsx.cisd.bds.Reference; +import ch.systemsx.cisd.bds.ReferenceType; +import ch.systemsx.cisd.bds.Sample; +import ch.systemsx.cisd.bds.UnknownFormatV1_0; +import ch.systemsx.cisd.bds.Version; +import ch.systemsx.cisd.bds.IDataStructure.Mode; +import ch.systemsx.cisd.bds.hcs.Geometry; +import ch.systemsx.cisd.bds.hcs.HCSImageAnnotations; +import ch.systemsx.cisd.bds.hcs.IHCSImageFormattedData; +import ch.systemsx.cisd.bds.hcs.Location; +import ch.systemsx.cisd.bds.hcs.PlateGeometry; +import ch.systemsx.cisd.bds.hcs.IHCSImageFormattedData.NodePath; +import ch.systemsx.cisd.bds.storage.IDirectory; +import ch.systemsx.cisd.bds.storage.IFile; +import ch.systemsx.cisd.bds.storage.ILink; +import ch.systemsx.cisd.bds.storage.INode; +import ch.systemsx.cisd.bds.storage.filesystem.FileStorage; +import ch.systemsx.cisd.bds.storage.filesystem.NodeFactory; +import ch.systemsx.cisd.bds.v1_1.ExperimentIdentifierWithUUID; +import ch.systemsx.cisd.bds.v1_1.IDataStructureV1_1; +import ch.systemsx.cisd.bds.v1_1.SampleWithOwner; +import ch.systemsx.cisd.common.collections.CollectionUtils; +import ch.systemsx.cisd.common.exceptions.ConfigurationFailureException; +import ch.systemsx.cisd.common.exceptions.EnvironmentFailureException; +import ch.systemsx.cisd.common.exceptions.UserFailureException; +import ch.systemsx.cisd.common.filesystem.FileOperations; +import ch.systemsx.cisd.common.filesystem.FileUtilities; +import ch.systemsx.cisd.common.filesystem.IFileOperations; +import ch.systemsx.cisd.common.logging.LogCategory; +import ch.systemsx.cisd.common.logging.LogFactory; +import ch.systemsx.cisd.common.mail.IMailClient; +import ch.systemsx.cisd.common.utilities.ClassUtils; +import ch.systemsx.cisd.etlserver.HCSImageCheckList.FullLocation; +import ch.systemsx.cisd.openbis.generic.shared.dto.DataSetType; +import ch.systemsx.cisd.openbis.generic.shared.dto.ExperimentPE; +import ch.systemsx.cisd.openbis.generic.shared.dto.PersonPE; +import ch.systemsx.cisd.openbis.generic.shared.dto.SamplePropertyPE; +import ch.systemsx.cisd.openbis.generic.shared.dto.StorageFormat; +import ch.systemsx.cisd.openbis.generic.shared.dto.types.SampleTypeCode; + +/** + * The <code>AbstractStorageAdapter</code> extension for <i>BDS</i> (Biological Data Standards). + * <p> + * When declared in <code>service.properties</code> file, it must specify a property called + * <code>storage-adapter.version</code>. Otherwise instantiation will fail. + * </p> + * + * @author Christian Ribeaud + */ +public final class BDSStorageProcessor extends AbstractStorageProcessor implements + IHCSImageFileAccepter +{ + private static final Logger operationLog = + LogFactory.getLogger(LogCategory.OPERATION, BDSStorageProcessor.class); + + private static final Logger notificationLog = + LogFactory.getLogger(LogCategory.OPERATION, BDSStorageProcessor.class); + + private static final String PROPERTY_PREFIX = "Property '%s': "; + + private static final String NO_FORMAT_FORMAT = + PROPERTY_PREFIX + "no valid and known format could be extracted from text '%s'."; + + static final String VERSION_KEY = "version"; + + static final String SAMPLE_TYPE_DESCRIPTION_KEY = "sampleTypeDescription"; + + static final String SAMPLE_TYPE_CODE_KEY = "sampleTypeCode"; + + static final String FORMAT_KEY = "format"; + + static final String FILE_EXTRACTOR_KEY = "file-extractor"; + + static final String NO_VERSION_FORMAT = + PROPERTY_PREFIX + "no version could be extracted from text '%s'."; + + private final Format format; + + private final SampleTypeCode sampleType; + + private final String sampleTypeDescription; + + private IDataStructureV1_1 dataStructure; + + private File imageFileRootDirectory; + + private File dataStructureDir; + + private IHCSImageFileExtractor imageFileExtractor; + + private List<FormatParameter> formatParameters; + + private IHCSImageFormattedData imageFormattedData; + + private HCSImageCheckList imageCheckList; + + private final Version version; + + public BDSStorageProcessor(final Properties properties) + { + super(properties); + version = parseVersion(getMandatoryProperty(VERSION_KEY)); + if (version.equals(new Version(1, 1)) == false) + { + throw new ConfigurationFailureException("Invalid version: " + version); + } + format = parseFormat(getMandatoryProperty(FORMAT_KEY)); + createFormatParameters(); + sampleTypeDescription = getMandatoryProperty(SAMPLE_TYPE_DESCRIPTION_KEY); + if (needsImageFileExtractor()) + { + final String property = getMandatoryProperty(FILE_EXTRACTOR_KEY); + imageFileExtractor = + ClassUtils.create(IHCSImageFileExtractor.class, property, properties); + } + try + { + sampleType = + SampleTypeCode.getSampleTypeCode(getMandatoryProperty(SAMPLE_TYPE_CODE_KEY)); + } catch (final IllegalArgumentException ex) + { + throw new ConfigurationFailureException(ex.getMessage()); + } + } + + private void createFormatParameters() + { + final List<String> parameterNames = format.getParameterNames(); + final IFormatParameterFactory formatParameterFactory = format.getFormatParameterFactory(); + formatParameters = new ArrayList<FormatParameter>(); + for (final String parameterName : parameterNames) + { + final String value = properties.getProperty(parameterName); + if (value == null) + { + throw ConfigurationFailureException.fromTemplate( + "No value has been defined for parameter '%s'", parameterName); + } + final FormatParameter formatParameter = + formatParameterFactory.createFormatParameter(parameterName, value); + if (formatParameter == null) + { + throw ConfigurationFailureException.fromTemplate( + "Given value '%s' is not understandable for parameter '%s'", value, + parameterName); + } + formatParameters.add(formatParameter); + } + } + + final static Version parseVersion(final String versionString) + { + final Version version = Version.createVersionFromString(versionString); + if (version == null) + { + throw ConfigurationFailureException.fromTemplate(NO_VERSION_FORMAT, VERSION_KEY, + versionString); + } + return version; + } + + final static Format parseFormat(final String formatString) + { + final Format format = Format.tryToCreateFormatFromString(formatString); + if (format == null) + { + throw ConfigurationFailureException.fromTemplate(NO_FORMAT_FORMAT, FORMAT_KEY, + formatString); + } + return format; + } + + private final ExperimentIdentifierWithUUID createExperimentIdentifier( + final DataSetInformation dataSetInformation) + { + final ch.systemsx.cisd.openbis.generic.shared.dto.identifier.ExperimentIdentifier experimentIdentifier = + dataSetInformation.getExperimentIdentifier(); + final String projectCode = experimentIdentifier.getProjectCode(); + final String experimentCode = experimentIdentifier.getExperimentCode(); + final String groupCode = experimentIdentifier.getGroupCode(); + final String instanceCode = dataSetInformation.getInstanceCode(); + return new ExperimentIdentifierWithUUID(instanceCode, dataSetInformation.getInstanceUUID(), + groupCode, projectCode, experimentCode); + } + + private final static void checkDataSetInformation(final DataSetInformation dataSetInformation) + { + assert dataSetInformation != null : "Unspecified data set information"; + assert dataSetInformation.getSampleIdentifier() != null : "Unspecified sample identifier"; + final ch.systemsx.cisd.openbis.generic.shared.dto.identifier.ExperimentIdentifier experimentIdentifier = + dataSetInformation.getExperimentIdentifier(); + assert experimentIdentifier != null : "Unspecified experiment identifier"; + checkExperimentIdentifier(experimentIdentifier); + } + + private final static void checkExperimentIdentifier( + final ch.systemsx.cisd.openbis.generic.shared.dto.identifier.ExperimentIdentifier experimentIdentifier) + { + assert experimentIdentifier.getGroupCode() != null : "Group code is null"; + assert experimentIdentifier.getExperimentCode() != null : "Experiment code is null"; + assert experimentIdentifier.getProjectCode() != null : "Project code is null"; + } + + private final IDataStructureV1_1 createDataStructure(final ExperimentPE experiment, + final DataSetInformation dataSetInformation, + final IProcedureAndDataTypeExtractor typeExtractor, final File incomingDataSetPath, + final File rootDir) + { + final FileStorage storage = new FileStorage(rootDir); + final IDataStructureV1_1 structure = + (IDataStructureV1_1) DataStructureFactory.createDataStructure(storage, version); + structure.create(); + structure.setFormat(format); + structure.setExperimentIdentifier(createExperimentIdentifier(dataSetInformation)); + structure.setExperimentRegistrationTimestamp(new ExperimentRegistrationTimestamp(experiment + .getRegistrationDate())); + final PersonPE registrator = experiment.getRegistrator(); + final String firstName = registrator.getFirstName(); + final String lastName = registrator.getLastName(); + final String email = registrator.getEmail(); + structure.setExperimentRegistrator(new ExperimentRegistrator(firstName, lastName, email)); + structure.setSample(createSample(dataSetInformation)); + structure.setDataSet(createDataSet(dataSetInformation, typeExtractor, incomingDataSetPath)); + for (final FormatParameter formatParameter : formatParameters) + { + structure.addFormatParameter(formatParameter); + } + final SamplePropertyPE[] sampleProperties = dataSetInformation.getProperties(); + final PlateDimension plateDimension = + PlateDimensionParser.tryToGetPlateDimension(sampleProperties); + if (plateDimension == null) + { + throw new EnvironmentFailureException( + "Missing plate geometry for the plate registered for sample identifier '" + + dataSetInformation.getSampleIdentifier() + "'."); + } + final Geometry plateGeometry = + new PlateGeometry(plateDimension.getRowsNum(), plateDimension.getColsNum()); + structure.addFormatParameter(new FormatParameter(PlateGeometry.PLATE_GEOMETRY, + plateGeometry)); + return structure; + } + + private final Sample createSample(final DataSetInformation dataSetInformation) + { + final String groupCode = StringUtils.defaultString(dataSetInformation.getGroupCode()); + final String instanceCode = dataSetInformation.getInstanceCode(); + final String instanceUUID = dataSetInformation.getInstanceUUID(); + assert instanceCode != null : "Unspecified database instance code"; + assert instanceUUID != null : "Unspecified database instance UUID"; + return new SampleWithOwner(dataSetInformation.getSampleIdentifier().getSampleCode(), + sampleType.getCode(), sampleTypeDescription, instanceUUID, instanceCode, groupCode); + } + + private final static DataSet createDataSet(final DataSetInformation dataSetInformation, + final IProcedureAndDataTypeExtractor typeExtractor, final File incomingDataSetPath) + { + final String dataSetCode = dataSetInformation.getDataSetCode(); + final String parentDataSetCode = dataSetInformation.getParentDataSetCode(); + final List<String> parentCodes = getParentCodeList(parentDataSetCode); + final boolean isMeasured = + typeExtractor.getProcedureType(incomingDataSetPath).isDataAcquisition(); + final DataSetType dataSetType = typeExtractor.getDataSetType(incomingDataSetPath); + final DataSet dataSet = + new DataSet(dataSetCode, dataSetType.getCode(), + ch.systemsx.cisd.bds.Utilities.Boolean.fromBoolean(isMeasured), + dataSetInformation.getProductionDate(), dataSetInformation + .getProducerCode(), parentCodes); + return dataSet; + } + + private final static List<String> getParentCodeList(final String parentDataSetCode) + { + if (parentDataSetCode == null) + { + return Collections.<String> emptyList(); + } else + { + return Collections.singletonList(parentDataSetCode); + } + } + + private final static String getPathOf(final INode node) + { + final StringBuilder builder = new StringBuilder(node.getName()); + IDirectory parent = node.tryGetParent(); + while (parent != null) + { + builder.insert(0, '/'); + builder.insert(0, parent.getName()); + parent = parent.tryGetParent(); + } + return builder.toString(); + } + + // TODO 2007-12-09, Christian Ribeaud: It will be a better choice to make two different + // implementations here: one for 'UnknownFormat1_0' and one for 'HCSImageFormat1_0'. + private final boolean needsImageFileExtractor() + { + return format.getCode().equals(UnknownFormatV1_0.UNKNOWN_1_0.getCode()) == false; + } + + // Although this check should be performed in the BDS library when closing is performed, we set + // the complete flag here as we want to inform the registrator about the incompleteness. + private void checkCompleteness(final DataSetInformation dataSetInformation, + final String dataSetFileName, final IMailClient mailClientOrNull) + { + final List<FullLocation> fullLocations = imageCheckList.getCheckedOnFullLocations(); + final boolean complete = fullLocations.size() == 0; + final IDataStructureV1_1 thisStructure = getDataStructure(dataStructureDir); + final DataSet dataSet = thisStructure.getDataSet(); + dataSet.setComplete(complete); + thisStructure.setDataSet(dataSet); + dataSetInformation.setComplete(complete); + if (complete == false) + { + final String message = + String.format("Incomplete data set '%s': %d image file(s) " + + "are missing (locations: %s)", dataSetFileName, fullLocations.size(), + CollectionUtils.abbreviate(fullLocations, 10)); + operationLog.warn(message); + if (mailClientOrNull != null) + { + final ExperimentRegistrator registrator = thisStructure.getExperimentRegistrator(); + final String email = registrator.getEmail(); + if (StringUtils.isBlank(email) == false) + { + try + { + mailClientOrNull.sendMessage("Incomplete data set '" + dataSetFileName + + "'", message, null, email); + } catch (final EnvironmentFailureException e) + { + notificationLog.error("Couldn't send the following e-mail to '" + email + + "': " + message, e); + } + } else + { + notificationLog.error("Unspecified e-mail address of experiment registrator " + + registrator); + } + } + } + } + + /** + * For given <var>storedDataDirectory</var> returns the {@link IDataStructureV1_1}. + * <p> + * In ideal case returns internally saved <code>dataStructureDir</code> but when given + * <var>storedDataDirectory</var> changed (meaning no longer equal to storage root), then we + * have to reload the data structure. + * </p> + */ + private final IDataStructureV1_1 getDataStructure(final File storedDataDirectory) + { + final IDataStructureV1_1 thisStructure; + if (storedDataDirectory.equals(dataStructureDir) == false) + { + final DataStructureLoader dataStructureLoader = + new DataStructureLoader(storedDataDirectory.getParentFile()); + thisStructure = + (IDataStructureV1_1) dataStructureLoader.load(storedDataDirectory.getName()); + } else + { + thisStructure = dataStructure; + if (thisStructure.isOpenOrCreated() == false) + { + thisStructure.open(Mode.READ_ONLY); + } + } + return thisStructure; + } + + // + // AbstractStorageProcessor + // + + public final File storeData(final ExperimentPE experiment, + final DataSetInformation dataSetInformation, + final IProcedureAndDataTypeExtractor typeExtractor, final IMailClient mailClient, + final File incomingDataSetDirectory, final File rootDirectory) + { + checkDataSetInformation(dataSetInformation); + assert rootDirectory != null : "Root directory can not be null."; + assert incomingDataSetDirectory != null : "Incoming data set directory can not be null."; + assert typeExtractor != null : "Unspecified IProcedureAndDataTypeExtractor implementation."; + + dataStructureDir = rootDirectory; + dataStructureDir.mkdirs(); + dataStructure = + createDataStructure(experiment, dataSetInformation, typeExtractor, + incomingDataSetDirectory, dataStructureDir); + final IFormattedData formattedData = dataStructure.getFormattedData(); + if (formattedData instanceof IHCSImageFormattedData) + { + imageFormattedData = (IHCSImageFormattedData) formattedData; + final int channels = imageFormattedData.getChannelCount(); + final Geometry plateGeometry = imageFormattedData.getPlateGeometry(); + final Geometry wellGeometry = imageFormattedData.getWellGeometry(); + imageCheckList = new HCSImageCheckList(channels, plateGeometry, wellGeometry); + } + if (needsImageFileExtractor()) + { + imageFileRootDirectory = incomingDataSetDirectory; + final HCSImageFileExtractionResult result = + imageFileExtractor.process(NodeFactory + .createDirectoryNode(incomingDataSetDirectory), dataSetInformation, + this); + if (operationLog.isInfoEnabled()) + { + operationLog.info(String.format("Extraction of %d files took %s.", result + .getTotalFiles(), DurationFormatUtils.formatDurationHMS(result + .getDuration()))); + } + if (result.getInvalidFiles().size() > 0) + { + throw UserFailureException.fromTemplate( + "Following invalid files %s have been found.", CollectionUtils.abbreviate( + result.getInvalidFiles(), 10)); + } + if (result.getTotalFiles() == 0) + { + throw UserFailureException.fromTemplate( + "No extractable files were found inside a dataset '%s'." + + " Have you changed your naming convention?", + incomingDataSetDirectory.getAbsolutePath()); + } + dataStructure.setAnnotations(new HCSImageAnnotations(result.getChannels())); + checkCompleteness(dataSetInformation, incomingDataSetDirectory.getName(), mailClient); + } else + { + dataStructure.getOriginalData().addFile(incomingDataSetDirectory, null, true); + if (operationLog.isInfoEnabled()) + { + operationLog.info(String.format("File '%s' added to original data.", + incomingDataSetDirectory)); + } + } + dataStructure.close(); + return dataStructureDir; + } + + public final void unstoreData(final File incomingDataSetDirectory, + final File storedDataDirectory) + { + checkParameters(incomingDataSetDirectory, storedDataDirectory); + + if (dataStructure == null) + { + // Nothing to do here. + return; + } + final IDirectory originalData = getDataStructure(dataStructureDir).getOriginalData(); + final INode node = originalData.tryGetNode(incomingDataSetDirectory.getName()); + // If the 'incoming' data have been moved to 'original' directory. This only happens if + // 'containsOriginalData' returns 'true'. + if (node != null) + { + final File incomingDirectory = incomingDataSetDirectory.getParentFile(); + try + { + node.moveTo(incomingDirectory); + if (operationLog.isInfoEnabled()) + { + operationLog.info(String.format( + "Node '%s' has moved to incoming directory '%s'.", node, + incomingDirectory.getAbsolutePath())); + } + } catch (final EnvironmentFailureException ex) + { + notificationLog.error(String.format( + "Could not move '%s' to incoming directory '%s'.", node, incomingDirectory + .getAbsolutePath()), ex); + return; + } + } + final IFileOperations fileOps = FileOperations.getMonitoredInstanceForCurrentThread(); + if (fileOps.exists(incomingDataSetDirectory)) + { + if (fileOps.removeRecursivelyQueueing(storedDataDirectory) == false) + { + operationLog + .error("Cannot delete '" + storedDataDirectory.getAbsolutePath() + "'."); + } + } else + { + notificationLog.error(String.format("Incoming data set directory '%s' does not " + + "exist, keeping store directory '%s'.", incomingDataSetDirectory, + storedDataDirectory)); + } + } + + public final File tryGetProprietaryData(final File storedDataDirectory) + { + assert storedDataDirectory != null : "Unspecified stored data directory."; + if (dataStructure == null) + { + operationLog.error("No data structure defined."); + return null; + } + if (imageFormattedData != null) + { + if (imageFormattedData.containsOriginalData() == false) + { + operationLog.warn("Original data are not available."); + return null; + } + } + final IDataStructureV1_1 thisStructure = getDataStructure(storedDataDirectory); + final IDirectory originalData = thisStructure.getOriginalData(); + final Iterator<INode> iterator = originalData.iterator(); + if (iterator.hasNext() == false) + { + return null; + } + final INode node = iterator.next(); + final String path = getPathOf(node); + final File originalDataFile = new File(path); + if (originalDataFile.exists() == false) + { + operationLog.error("Original data set file '" + originalDataFile.getAbsolutePath() + + "' does not exist."); + return null; + } + return originalDataFile; + } + + public final StorageFormat getStorageFormat() + { + return StorageFormat.BDS_DIRECTORY; + } + + // + // IHCSImageFileAccepter + // + + public final void accept(final int channel, final Location wellLocation, + final Location tileLocation, final IFile imageFile) + { + assert imageFileRootDirectory != null : "Incoming data set directory has not been set."; + final String imageRelativePath = + FileUtilities + .getRelativeFile(imageFileRootDirectory, new File(imageFile.getPath())); + assert imageRelativePath != null : "Image relative path should not be null."; + final NodePath nodePath = + imageFormattedData.addStandardNode(imageFileRootDirectory, imageRelativePath, + channel, wellLocation, tileLocation); + imageCheckList.checkOff(channel, wellLocation, tileLocation); + if (nodePath.getNode() instanceof ILink) + { + // We made a link in the 'standard' directory to the 'original' directory image file + // name. The image file did not change during the operation. + final Reference reference = + new Reference(nodePath.getPath(), imageFileRootDirectory.getName() + + Constants.PATH_SEPARATOR + imageRelativePath, ReferenceType.IDENTICAL); + getDataStructure(dataStructureDir).addReference(reference); + } + } +} diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/BaseDirectoryHolder.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/BaseDirectoryHolder.java new file mode 100644 index 00000000000..fbe4029b62d --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/BaseDirectoryHolder.java @@ -0,0 +1,68 @@ +/* + * Copyright 2008 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import java.io.File; + +/** + * A tiny class which holds the <i>base directory</i> and ensures that the <i>target file</i> + * lazily gets computed only once. + * + * @author Christian Ribeaud + */ +final class BaseDirectoryHolder +{ + + private final File baseDirectory; + + private final IDataStoreStrategy dataStoreStrategy; + + private final File incomingDataSetPath; + + private File targetFile; + + BaseDirectoryHolder(final IDataStoreStrategy dataStoreStrategy, final File baseDirectory, + final File incomingDataSetPath) + { + assert dataStoreStrategy != null : "Data store strategy can not be null."; + assert baseDirectory != null : "Base directory can not be null"; + assert incomingDataSetPath != null : "Incoming data set can not be null."; + this.dataStoreStrategy = dataStoreStrategy; + this.baseDirectory = baseDirectory; + this.incomingDataSetPath = incomingDataSetPath; + } + + private final File createTargetFile() + { + return dataStoreStrategy.getTargetPath(baseDirectory, incomingDataSetPath); + } + + final File getBaseDirectory() + { + return baseDirectory; + } + + final synchronized File getTargetFile() + { + if (targetFile == null) + { + targetFile = createTargetFile(); + } + return targetFile; + } + +} diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/ChannelSetHelper.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/ChannelSetHelper.java new file mode 100644 index 00000000000..beaf87b8ee2 --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/ChannelSetHelper.java @@ -0,0 +1,106 @@ +/* + * Copyright 2008 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; + +import ch.systemsx.cisd.bds.hcs.Channel; + +/** + * Helps to construct a sorted set of {@link Channel}. + * + * @author Christian Ribeaud + */ +public final class ChannelSetHelper +{ + private final SortedSet<Integer> wavelengths; + + private Set<Channel> channels; + + private boolean locked = false; + + private Map<Integer, Channel> channelsByWavelength; + + public ChannelSetHelper() + { + wavelengths = new TreeSet<Integer>(); + } + + /** + * Adds given <var>wavelength</var> to the internal set of wavelengths. + * <p> + * Wavelengths are ensured to be unique and are internally sorted. + * </p> + */ + public final void addWavelength(final int wavelength) + { + assert locked == false : "You can no longer change the state of this class."; + wavelengths.add(wavelength); + } + + /** + * Returns an unmodifiable sorted (based on {@link Channel#getCounter()}) set of channels. + * <p> + * This is typically called after having added all the wavelengths. + * </p> + */ + public final Set<Channel> getChannelSet() + { + locked = true; + if (channels == null) + { + final Set<Channel> set = new TreeSet<Channel>(); + final Iterator<Integer> iter = wavelengths.iterator(); + for (int i = 0; iter.hasNext(); i++) + { + set.add(new Channel(i + 1, iter.next())); + } + channels = Collections.unmodifiableSet(set); + } + return channels; + } + + /** + * For given wavelength returns corresponding <code>Channel</code>. + * <p> + * Never returns <code>null</code> and prefers to throw an exception if given <var>wavelength</var> + * can not be found. + * </p> + */ + public final Channel getChannelForWavelength(final int wavelength) + { + locked = true; + if (channelsByWavelength == null) + { + final Map<Integer, Channel> map = new HashMap<Integer, Channel>(); + for (final Channel channel : getChannelSet()) + { + map.put(channel.getWavelength(), channel); + } + channelsByWavelength = Collections.unmodifiableMap(map); + } + final Channel channel = channelsByWavelength.get(wavelength); + assert channel != null : String.format("Given wavelength %d can not be found.", wavelength); + return channel; + } +} \ No newline at end of file diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/DataSetInformation.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/DataSetInformation.java new file mode 100644 index 00000000000..1713d9768c9 --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/DataSetInformation.java @@ -0,0 +1,274 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import java.io.Serializable; +import java.util.Date; + +import org.apache.commons.lang.builder.ToStringBuilder; + +import ch.systemsx.cisd.common.types.BooleanOrUnknown; +import ch.systemsx.cisd.common.utilities.ModifiedShortPrefixToStringStyle; +import ch.systemsx.cisd.openbis.generic.shared.IWebService; +import ch.systemsx.cisd.openbis.generic.shared.dto.ExperimentPE; +import ch.systemsx.cisd.openbis.generic.shared.dto.ExternalData; +import ch.systemsx.cisd.openbis.generic.shared.dto.ExtractableData; +import ch.systemsx.cisd.openbis.generic.shared.dto.SamplePropertyPE; +import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.DatabaseInstanceIdentifier; +import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.ExperimentIdentifier; +import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.GroupIdentifier; +import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.SampleIdentifier; + +/** + * Container class for data extracted from the data set directory. + * + * @author Bernd Rinn + */ +public final class DataSetInformation implements Serializable +{ + + private static final long serialVersionUID = IWebService.VERSION; + + /** The sample code (aka <i>barcode</i>). <b>CAN NOT</b> be <code>null</code>. */ + private String sampleCode; + + private SamplePropertyPE[] properties = SamplePropertyPE.EMPTY_ARRAY; + + /** + * The database instance <i>UUID</i>. + */ + private String instanceUUID; + + /** + * The database instance code. + */ + private String instanceCode; + + /** + * The group code (set by the {@link IDataSetInfoExtractor} or as specified in the + * <code>service.properties</code> file). + */ + private String groupCode; + + /** An object that uniquely identifies the experiment. Can be <code>null</code>. */ + private ExperimentIdentifier experimentIdentifier; + + /** Required information about the experiment. */ + private transient ExperimentPE experiment; + + private BooleanOrUnknown isCompleteFlag = BooleanOrUnknown.U; + + /** + * A subset of {@link ExternalData} which gets set by the code extractor. + * <p> + * Initialized with <code>new ExtractableData()</code>. + * </p> + */ + private ExtractableData extractableData = new ExtractableData(); + + /** This constructor is for serialization. */ + public DataSetInformation() + { + } + + public final BooleanOrUnknown getIsCompleteFlag() + { + return isCompleteFlag; + } + + public final void setComplete(final boolean complete) + { + isCompleteFlag = BooleanOrUnknown.resolve(complete); + } + + /** + * Returns the sample properties. + * + * @return never <code>null</code> but could return an empty array. + */ + public final SamplePropertyPE[] getProperties() + { + return properties; + } + + public final void setProperties(final SamplePropertyPE[] properties) + { + this.properties = properties; + } + + public final String getInstanceCode() + { + return instanceCode; + } + + public final void setInstanceCode(final String instanceCode) + { + this.instanceCode = instanceCode; + } + + public final String getInstanceUUID() + { + return instanceUUID; + } + + public final void setInstanceUUID(final String instanceUUID) + { + this.instanceUUID = instanceUUID; + } + + /** Sets <code>experimentIdentifier</code>. */ + public final void setExperimentIdentifier(final ExperimentIdentifier experimentIdentifier) + { + this.experimentIdentifier = experimentIdentifier; + } + + /** + * Returns the identifier of experiment which makes it unique. + * + * @return <code>null</code> if no <code>ExperimentIdentifier</code> has been set. + */ + public final ExperimentIdentifier getExperimentIdentifier() + { + return experimentIdentifier; + } + + /** + * Returns the basic information about the experiment. + */ + public ExperimentPE getExperiment() + { + return experiment; + } + + /** + * Sets the basic information about the experiment. + */ + public void setExperiment(final ExperimentPE experiment) + { + this.experiment = experiment; + } + + /** + * Returns the sample identifier. + * + * @return <code>null</code> if <code>sampleCode</code> has not been set. + */ + public final SampleIdentifier getSampleIdentifier() + { + if (sampleCode == null) + { + return null; + } + final DatabaseInstanceIdentifier databaseInstanceIdentifier = + new DatabaseInstanceIdentifier(instanceCode); + if (groupCode == null) + { + return new SampleIdentifier(databaseInstanceIdentifier, sampleCode); + } + return new SampleIdentifier(new GroupIdentifier(databaseInstanceIdentifier, groupCode), + sampleCode); + } + + public final void setSampleCode(final String sampleCode) + { + this.sampleCode = sampleCode; + } + + public final String getDataSetCode() + { + return extractableData.getCode(); + } + + public final void setDataSetCode(final String dataSetCode) + { + extractableData.setCode(dataSetCode); + } + + public final String getProducerCode() + { + return extractableData.getDataProducerCode(); + } + + public final void setProducerCode(final String producerCode) + { + extractableData.setDataProducerCode(producerCode); + } + + public final Date getProductionDate() + { + return extractableData.getProductionDate(); + } + + public final void setProductionDate(final Date productionDate) + { + extractableData.setProductionDate(productionDate); + } + + public final ExtractableData getExtractableData() + { + return extractableData; + } + + public final void setExtractableData(final ExtractableData extractableData) + { + this.extractableData = extractableData; + } + + public final String getParentDataSetCode() + { + return extractableData.getParentDataSetCode(); + } + + public final void setParentDataSetCode(final String parentDataSetCode) + { + extractableData.setParentDataSetCode(parentDataSetCode); + } + + public final void setGroupCode(final String groupCode) + { + this.groupCode = groupCode; + } + + public final String getGroupCode() + { + return groupCode; + } + + public final String describe() + { + if (experimentIdentifier == null) + { + return String.format("CODE('%s') SAMPLE_CODE('%s')", extractableData.getCode(), + sampleCode); + } else + { + return String.format("CODE('%s') SAMPLE_CODE('%s') EXPERIMENT('%s')", extractableData + .getCode(), sampleCode, experimentIdentifier.describe()); + } + } + + // + // Object + // + + @Override + public final String toString() + { + return ToStringBuilder.reflectionToString(this, + ModifiedShortPrefixToStringStyle.MODIFIED_SHORT_PREFIX_STYLE); + } +} \ No newline at end of file diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/DataSetNameEntitiesProvider.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/DataSetNameEntitiesProvider.java new file mode 100644 index 00000000000..43357913ee3 --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/DataSetNameEntitiesProvider.java @@ -0,0 +1,101 @@ +/* + * Copyright 2008 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import java.io.File; + +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang.StringUtils; + +import ch.systemsx.cisd.common.exceptions.UserFailureException; + +/** + * Class which splits a data set name into entities and makes them accessible. + * + * @author Franz-Josef Elmer + */ +public class DataSetNameEntitiesProvider +{ + private final char entitySeparatorCharacter; + + private final String[] entities; + + private final String errorMessagePrefix; + + /** + * Creates an instance based on the name of the specified file using the specified character + * which separates entities. + */ + public DataSetNameEntitiesProvider(File dataSetFile, char entitySeparatorCharacter, + boolean stripExtension) + { + this(dataSetFile.getName(), entitySeparatorCharacter, stripExtension); + } + + /** + * Creates an instance for the specified name using the specified character which separates + * entities. + */ + public DataSetNameEntitiesProvider(String dataSetName, char entitySeparatorCharacter, + boolean stripExtension) + { + assert dataSetName != null : "Unspecified data set name."; + + this.entitySeparatorCharacter = entitySeparatorCharacter; + final String name; + if (stripExtension) + { + name = FilenameUtils.getBaseName(dataSetName); + } else + { + name = dataSetName; + } + entities = StringUtils.split(name, entitySeparatorCharacter); + errorMessagePrefix = "Invalid data set name '" + dataSetName + "'. "; + } + + /** + * Returns the entity of specified index. Negative arguments can also be used. They are + * interpreted as an index counting from the end of the sequence of entities. For example, -1 + * denotes the last entity. + */ + public String getEntity(int index) + { + if (index >= entities.length) + { + throwUserFailureException(index + 1); + } + int actualIndex = index; + if (index < 0) + { + actualIndex = entities.length + index; + if (actualIndex < 0) + { + throwUserFailureException(-index); + } + } + return entities[actualIndex]; + } + + private void throwUserFailureException(int expectedNumberOfEntities) + { + throw new UserFailureException(errorMessagePrefix + "We need " + expectedNumberOfEntities + + " entities, separated by '" + entitySeparatorCharacter + "', but got only " + + entities.length + "."); + } + +} diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/DataStoreStrategyKey.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/DataStoreStrategyKey.java new file mode 100644 index 00000000000..ba26e78155a --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/DataStoreStrategyKey.java @@ -0,0 +1,50 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +/** + * Key associated with each {@link IDataStoreStrategy}. + * + * @author Christian Ribeaud + */ +enum DataStoreStrategyKey +{ + /** + * This <code>IDataStoreStrategy</code> implementation if for data set that has been + * identified as <i>unidentified</i>, meaning that, for instance, no experiment could be mapped + * to the one found in given {@link DataSetInformation} (if we try to find out the sample to + * which this data set should be registered through the experiment). + */ + UNIDENTIFIED, + /** + * This <code>IDataStoreStrategy</code> implementation if for data set that has been + * <i>identified</i>, meaning that kind of connection to this data set could be found in the + * database (through the derived <i>Master Plate</i> or through the experiment specified). + */ + IDENTIFIED, + /** + * This <code>IDataStoreStrategy</code> implementation if for data set that has been + * identified as <i>invalid</i>, meaning that the data set itself or its + * <code>Master Plate</code> code is not registered in the database. So there is no + * possibility to link the data set to an already existing sample. + */ + INVALID, + /** + * States that the transformation part could not be processed correctly. + */ + ERROR; +} \ No newline at end of file diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/DataStrategyStore.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/DataStrategyStore.java new file mode 100644 index 00000000000..66abc5ce46c --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/DataStrategyStore.java @@ -0,0 +1,192 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import java.io.File; +import java.util.EnumMap; +import java.util.Map; + +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; + +import ch.systemsx.cisd.common.exceptions.EnvironmentFailureException; +import ch.systemsx.cisd.common.logging.LogCategory; +import ch.systemsx.cisd.common.logging.LogFactory; +import ch.systemsx.cisd.common.mail.IMailClient; +import ch.systemsx.cisd.openbis.generic.shared.dto.ExperimentPE; +import ch.systemsx.cisd.openbis.generic.shared.dto.GroupPE; +import ch.systemsx.cisd.openbis.generic.shared.dto.PersonPE; +import ch.systemsx.cisd.openbis.generic.shared.dto.ProjectPE; +import ch.systemsx.cisd.openbis.generic.shared.dto.SamplePropertyPE; +import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.ExperimentIdentifier; +import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.SampleIdentifier; + +/** + * Default <code>IDataStrategyStore</code> implementation. + * <p> + * Decides which {@link IDataStoreStrategy} will be applied for an incoming data set. + * </p> + * + * @author Christian Ribeaud + */ +final class DataStrategyStore implements IDataStrategyStore +{ + static final String SUBJECT_FORMAT = "ATTENTION: experiment '%s'"; + + private static final Logger notificationLog = + LogFactory.getLogger(LogCategory.NOTIFY, DataStrategyStore.class); + + private static final Logger operationLog = + LogFactory.getLogger(LogCategory.OPERATION, DataStrategyStore.class); + + private final IEncapsulatedLimsService limsService; + + private final IMailClient mailClient; + + private final Map<DataStoreStrategyKey, IDataStoreStrategy> dataStoreStrategies; + + DataStrategyStore(final IEncapsulatedLimsService limsService, final IMailClient mailClient) + { + this.mailClient = mailClient; + dataStoreStrategies = createDataStoreStrategies(); + this.limsService = limsService; + } + + private final static void putDataStoreStrategy( + final Map<DataStoreStrategyKey, IDataStoreStrategy> map, + final IDataStoreStrategy dataStoreStrategy) + { + map.put(dataStoreStrategy.getKey(), dataStoreStrategy); + } + + private final static Map<DataStoreStrategyKey, IDataStoreStrategy> createDataStoreStrategies() + { + final Map<DataStoreStrategyKey, IDataStoreStrategy> map = + new EnumMap<DataStoreStrategyKey, IDataStoreStrategy>(DataStoreStrategyKey.class); + putDataStoreStrategy(map, new IdentifiedDataStrategy()); + putDataStoreStrategy(map, new NamedDataStrategy(DataStoreStrategyKey.UNIDENTIFIED)); + putDataStoreStrategy(map, new NamedDataStrategy(DataStoreStrategyKey.INVALID)); + return map; + } + + final static String createInvalidSampleCodeMessage(final DataSetInformation dataSetInfo) + { + return "ETL server: Sample '" + dataSetInfo.getSampleIdentifier() + + "' is not valid for experiment '" + dataSetInfo.getExperimentIdentifier() + + "' (it has maybe been invalidated?)."; + } + + private static String createNotificationMessage(final DataSetInformation dataSetInfo, + final File incomingDataSetPathForLogging) + { + final SampleIdentifier sampleIdentifier = dataSetInfo.getSampleIdentifier(); + return String.format("Directory '%s', sample identifier '%s': unknown to openBIS", + incomingDataSetPathForLogging, sampleIdentifier); + } + + private final static ExperimentIdentifier createExperimentIdentifier( + final ExperimentPE experiment) + { + ExperimentIdentifier experimentIdentifier; + experimentIdentifier = new ExperimentIdentifier(); + experimentIdentifier.setExperimentCode(experiment.getCode()); + final ProjectPE project = experiment.getProject(); + assert project != null : "Unspecified project"; + experimentIdentifier.setProjectCode(project.getCode()); + final GroupPE group = project.getGroup(); + assert group != null : "Unspecified group"; + experimentIdentifier.setGroupCode(group.getCode()); + return experimentIdentifier; + } + + // + // IDataStrategyStore + // + + public final IDataStoreStrategy getDataStoreStrategy(final DataSetInformation dataSetInfo, + final File incomingDataSetPath) + { + + assert incomingDataSetPath != null : "Incoming data set path can not be null."; + if (dataSetInfo == null) + { + return dataStoreStrategies.get(DataStoreStrategyKey.UNIDENTIFIED); + } + final SampleIdentifier sampleIdentifier = dataSetInfo.getSampleIdentifier(); + final ExperimentPE experiment = limsService.getBaseExperiment(sampleIdentifier); + if (experiment == null) + { + notificationLog.error(createNotificationMessage(dataSetInfo, incomingDataSetPath)); + return dataStoreStrategies.get(DataStoreStrategyKey.UNIDENTIFIED); + } else if (experiment.getInvalidation() != null) + { + notificationLog.error("Data set for sample '" + sampleIdentifier + + "' can not be registered because experiment '" + experiment.getCode() + + "' has been invalidated."); + return dataStoreStrategies.get(DataStoreStrategyKey.UNIDENTIFIED); + } + dataSetInfo.setExperiment(experiment); + final ExperimentIdentifier experimentIdentifier = createExperimentIdentifier(experiment); + dataSetInfo.setExperimentIdentifier(experimentIdentifier); + + final SamplePropertyPE[] properties = + limsService.getPropertiesOfTopSampleRegisteredFor(sampleIdentifier); + if (properties == null) + { + final PersonPE registrator = experiment.getRegistrator(); + assert registrator != null : "Registrator must be known"; + final String message = createInvalidSampleCodeMessage(dataSetInfo); + final String recipientMail = registrator.getEmail(); + if (StringUtils.isNotBlank(recipientMail)) + { + sendEmail(message, experimentIdentifier, recipientMail); + } else + { + notificationLog.error("The registrator '" + registrator + + "' has a blank email, sending the following email failed:\n" + message); + } + operationLog.error(String.format("Incoming data set '%s' claims to " + + "belong to experiment '%s' and sample" + + " identifier '%s', but according to the openBIS server " + + "there is no such sample for this " + + "experiment (it has maybe been invalidated?). We thus consider it invalid.", + incomingDataSetPath, experimentIdentifier, sampleIdentifier)); + return dataStoreStrategies.get(DataStoreStrategyKey.INVALID); + } + dataSetInfo.setProperties(properties); + + if (operationLog.isInfoEnabled()) + { + operationLog.info("Identified that database knows experiment '" + experimentIdentifier + + "' and sample '" + sampleIdentifier + "'."); + } + return dataStoreStrategies.get(DataStoreStrategyKey.IDENTIFIED); + } + + private void sendEmail(final String message, final ExperimentIdentifier experimentIdentifier, + final String recipientMail) + { + final String subject = String.format(SUBJECT_FORMAT, experimentIdentifier); + try + { + mailClient.sendMessage(subject, message, null, recipientMail); + } catch (final EnvironmentFailureException ex) + { + operationLog.error(ex.getMessage()); + } + } +} \ No newline at end of file diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/DefaultDataSetInfoExtractor.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/DefaultDataSetInfoExtractor.java new file mode 100644 index 00000000000..fea627bdb96 --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/DefaultDataSetInfoExtractor.java @@ -0,0 +1,246 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import java.io.File; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Properties; + +import ch.rinn.restrictions.Private; +import ch.systemsx.cisd.common.exceptions.EnvironmentFailureException; +import ch.systemsx.cisd.common.exceptions.UserFailureException; +import ch.systemsx.cisd.common.utilities.PropertyUtils; + +/** + * Default implementation which assumes that the information can be extracted from the file name. + * Following information can be extracted: + * <ul> + * <li>Sample code + * <li>Parent data set code + * <li>Data producer code + * <li>Data production date + * </ul> + * The name is split into entities separated by the property {@link #ENTITY_SEPARATOR_PROPERTY_NAME} + * . It is assumed that each of the above-mentioned pieces of information is one of these entities. + * The extractor can be configured by the following optional properties: + * <table border="1" * cellspacing="0" cellpadding="5"> + * <tr> + * <th>Property</th> + * <th>Default value</th> + * <th>Description</th> + * </tr> + * <tr> + * <td><code>entity-separator</code></td> + * <td><code>.</code></td> + * <td>Character which separates entities in the file name. Whitespace characters are not allowed.</td> + * </tr> + * <tr> + * <td><code>index-of-sample-code</code></td> + * <td><code>-1</code></td> + * <td>Index of the entity which is interpreted as the sample code.</td> + * </tr> + * <tr> + * <td><code>index-of-parent-data-set-code</code></td> + * <td> </td> + * <td>Index of the entity which is interpreted as the parent data set code. If not specified no + * parent data set code will be extracted.</td> + * </tr> + * <tr> + * <td><code>index-of-data-producer-code</code></td> + * <td> </td> + * <td>Index of the entity which is interpreted as the data producer code. If not specified no data + * producer code will be extracted.</td> + * </tr> + * <tr> + * <td><code>index-of-data-production-date</code></td> + * <td> </td> + * <td>Index of the entity which is interpreted as the data production date. If not specified no + * data production date will be extracted.</td> + * </tr> + * <tr> + * <td><code>data-production-date-format</code></td> + * <td><code>yyyyMMddHHmmss</code></td> + * <td>Format of the data production date. For the correct syntax see <a + * href="http://java.sun.com/j2se/1.5.0/docs/api/java/text/SimpleDateFormat.html" + * >SimpleDateFormat</a>.</td> + * </tr> + * </table> + * The first entity has index 0, the second 1, etc. Using negative numbers one can specify entities + * from the end. Thus, -1 means the last entity, -2 the second last entity, etc. + * + * @author Franz-Josef Elmer + */ +public class DefaultDataSetInfoExtractor extends AbstractDataSetInfoExtractor +{ + + /** + * Name of the property specifying the index of the entity which should be interpreted as the + * sample code. + * <p> + * Use a negative number to count from the end, e.g. <code>-1</code> to use the last entity as + * the sample code. + * </p> + */ + @Private + static final String INDEX_OF_SAMPLE_CODE = "index-of-sample-code"; + + /** + * Name of the property specifying the index of the entity which should be interpreted as the + * parent data set code. + * <p> + * Use a negative number to count from the end, e.g. <code>-1</code> to use the last entity as + * the sample code. + * </p> + */ + @Private + static final String INDEX_OF_PARENT_DATA_SET_CODE = "index-of-parent-data-set-code"; + + /** Default index of sample code. */ + private static final int DEFAULT_INDEX_OF_SAMPLE_CODE = -1; + + /** + * Name of the property specifying the index of the entity which should be interpreted as the + * data producer code. + * <p> + * Use a negative number to count from the end, e.g. <code>-1</code> to use the last entity as + * the data producer code. + * </p> + */ + @Private + static final String INDEX_OF_DATA_PRODUCER_CODE = "index-of-data-producer-code"; + + /** + * Name of the property specifying the index of the entity which should be interpreted as the + * data production date. + * <p> + * Use a negative number to count from the end, e.g. <code>-1</code> to use the last entity as + * the data production date. + * </p> + */ + @Private + static final String INDEX_OF_DATA_PRODUCTION_DATE = "index-of-data-production-date"; + + /** + * Name of the property specifying the format of the data production date. + */ + @Private + static final String DATA_PRODUCTION_DATE_FORMAT = "data-production-date-format"; + + /** Default data production date format. */ + private static final String DEFAULT_DATA_PRODUCTION_DATE_FORMAT = "yyyyMMddHHmmss"; + + private final int indexOfSampleCode; + + private final boolean noParentDataSetCode; + + private final int indexOfParentDataSetCode; + + private final boolean noDataProducerCode; + + private final int indexOfDataProducerCode; + + private final boolean noDataProductionDate; + + private final int indexOfDataProductionDate; + + private final SimpleDateFormat dateFormat; + + /** + * The <var>properties</var> are not used by this constructor but present to fulfill the + * contract. + */ + public DefaultDataSetInfoExtractor(final Properties globalProperties) + { + super(globalProperties); + indexOfSampleCode = + PropertyUtils + .getInt(properties, INDEX_OF_SAMPLE_CODE, DEFAULT_INDEX_OF_SAMPLE_CODE); + String indexAsString = properties.getProperty(INDEX_OF_PARENT_DATA_SET_CODE); + noParentDataSetCode = indexAsString == null; + indexOfParentDataSetCode = + PropertyUtils.getInt(properties, INDEX_OF_PARENT_DATA_SET_CODE, 0); + indexAsString = properties.getProperty(INDEX_OF_DATA_PRODUCER_CODE); + noDataProducerCode = indexAsString == null; + indexOfDataProducerCode = PropertyUtils.getInt(properties, INDEX_OF_DATA_PRODUCER_CODE, 0); + indexAsString = properties.getProperty(INDEX_OF_DATA_PRODUCTION_DATE); + noDataProductionDate = indexAsString == null; + indexOfDataProductionDate = + PropertyUtils.getInt(properties, INDEX_OF_DATA_PRODUCTION_DATE, 0); + dateFormat = + new SimpleDateFormat(properties.getProperty(DATA_PRODUCTION_DATE_FORMAT, + DEFAULT_DATA_PRODUCTION_DATE_FORMAT)); + } + + // + // ICodeExtractor + // + + public DataSetInformation getDataSetInformation(final File incomingDataSetPath) + throws EnvironmentFailureException, UserFailureException + { + assert incomingDataSetPath != null : "Incoming data set path can not be null."; + final DataSetNameEntitiesProvider entitiesProvider = + new DataSetNameEntitiesProvider(incomingDataSetPath, entitySeparator, + stripExtension); + final DataSetInformation dataSetInformation = new DataSetInformation(); + dataSetInformation.setSampleCode(entitiesProvider.getEntity(indexOfSampleCode)); + dataSetInformation.setParentDataSetCode(tryGetParentDataSetCode(entitiesProvider)); + dataSetInformation.setProducerCode(tryGetDataProducerCode(entitiesProvider)); + dataSetInformation.setProductionDate(tryGetDataProductionDate(entitiesProvider)); + return dataSetInformation; + } + + private String tryGetParentDataSetCode( + final DataSetNameEntitiesProvider dataSetNameEntitiesProvider) + { + if (noParentDataSetCode) + { + return null; + } + return dataSetNameEntitiesProvider.getEntity(indexOfParentDataSetCode); + } + + private String tryGetDataProducerCode( + final DataSetNameEntitiesProvider dataSetNameEntitiesProvider) + { + if (noDataProducerCode) + { + return null; + } + return dataSetNameEntitiesProvider.getEntity(indexOfDataProducerCode); + } + + private Date tryGetDataProductionDate( + final DataSetNameEntitiesProvider dataSetNameEntitiesProvider) + { + if (noDataProductionDate) + { + return null; + } + final String dateString = dataSetNameEntitiesProvider.getEntity(indexOfDataProductionDate); + try + { + return dateFormat.parse(dateString); + } catch (final ParseException e) + { + throw new UserFailureException("Could not parse data production date '" + dateString + + "' because it violates the following format: " + dateFormat.toPattern()); + } + } +} \ No newline at end of file diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/DefaultStorageProcessor.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/DefaultStorageProcessor.java new file mode 100644 index 00000000000..356a50764a1 --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/DefaultStorageProcessor.java @@ -0,0 +1,87 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import java.io.File; +import java.util.Properties; + +import ch.systemsx.cisd.common.exceptions.EnvironmentFailureException; +import ch.systemsx.cisd.common.mail.IMailClient; +import ch.systemsx.cisd.openbis.generic.shared.dto.ExperimentPE; +import ch.systemsx.cisd.openbis.generic.shared.dto.StorageFormat; + +/** + * A default {@link IStorageProcessor} implementation. + * + * @author Christian Ribeaud + */ +public class DefaultStorageProcessor extends AbstractStorageProcessor +{ + static final String NO_RENAME = "Couldn't rename '%s' to '%s'."; + + public DefaultStorageProcessor(final Properties properties) + { + super(properties); + } + + private final static File createTargetFile(final File incomingDataSetFile, + final File baseDirectory) + { + return new File(baseDirectory, incomingDataSetFile.getName()); + } + + // + // AbstractStorageProcessor + // + + public final File storeData(final ExperimentPE experiment, + final DataSetInformation dataSetInformation, + final IProcedureAndDataTypeExtractor typeExtractor, final IMailClient mailClient, + final File incomingDataSetDirectory, final File rootDir) + { + checkParameters(incomingDataSetDirectory, rootDir); + final File targetFile = createTargetFile(incomingDataSetDirectory, rootDir); + if (FileRenamer.renameAndLog(incomingDataSetDirectory, targetFile) == false) + { + throw new EnvironmentFailureException(String.format(NO_RENAME, + incomingDataSetDirectory, targetFile)); + } + return targetFile; + } + + public final void unstoreData(final File incomingDataSetDirectory, + final File storedDataDirectory) + { + checkParameters(incomingDataSetDirectory, storedDataDirectory); + // Note that this will move back <code>targetPath</code> to its original place but the + // directory structure will persist. Right now, we consider this is fine as these empty + // directories will not disturb the running application. + FileRenamer.renameAndLog(createTargetFile(incomingDataSetDirectory, storedDataDirectory), + incomingDataSetDirectory); + } + + public final StorageFormat getStorageFormat() + { + return StorageFormat.PROPRIETARY; + } + + public final File tryGetProprietaryData(final File storedDataDirectory) + { + assert storedDataDirectory != null : "Unspecified stored data directory."; + return storedDataDirectory; + } +} diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/ETLServerPlugin.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/ETLServerPlugin.java new file mode 100644 index 00000000000..8d48eb808d5 --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/ETLServerPlugin.java @@ -0,0 +1,67 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + + +/** + * ETL Server plugin as a bean. + * + * @author Franz-Josef Elmer + */ +public class ETLServerPlugin implements IETLServerPlugin +{ + private final IDataSetInfoExtractor codeExtractor; + + private final IProcedureAndDataTypeExtractor typeExtractor; + + private final IStorageProcessor storageProcessor; + + /** + * Creates an instance with the specified extractors. + */ + public ETLServerPlugin(final IDataSetInfoExtractor codeExtractor, + final IProcedureAndDataTypeExtractor typeExtractor, + final IStorageProcessor storageProcessor) + { + assert codeExtractor != null : "Missing code extractor"; + assert typeExtractor != null : "Missing type extractor"; + assert storageProcessor != null : "Missing storage processor"; + + this.codeExtractor = codeExtractor; + this.typeExtractor = typeExtractor; + this.storageProcessor = storageProcessor; + } + + // + // IETLServerPlugin + // + + public final IDataSetInfoExtractor getDataSetInfoExtractor() + { + return codeExtractor; + } + + public final IProcedureAndDataTypeExtractor getTypeExtractor() + { + return typeExtractor; + } + + public final IStorageProcessor getStorageProcessor() + { + return storageProcessor; + } +} diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/EncapsulatedLimsService.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/EncapsulatedLimsService.java new file mode 100644 index 00000000000..e326821a7eb --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/EncapsulatedLimsService.java @@ -0,0 +1,211 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import org.apache.log4j.Logger; + +import ch.systemsx.cisd.common.exceptions.ConfigurationFailureException; +import ch.systemsx.cisd.common.exceptions.InvalidSessionException; +import ch.systemsx.cisd.common.exceptions.UserFailureException; +import ch.systemsx.cisd.common.logging.LogCategory; +import ch.systemsx.cisd.common.logging.LogFactory; +import ch.systemsx.cisd.openbis.generic.shared.IETLLIMSService; +import ch.systemsx.cisd.openbis.generic.shared.dto.DatabaseInstancePE; +import ch.systemsx.cisd.openbis.generic.shared.dto.ExperimentPE; +import ch.systemsx.cisd.openbis.generic.shared.dto.ExternalData; +import ch.systemsx.cisd.openbis.generic.shared.dto.SamplePropertyPE; +import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.SampleIdentifier; + +/** + * A class that encapsulates the {@link IETLLIMSService} and handles (re-)authentication automatically + * as needed. + * <p> + * This class is thread safe (otherwise one thread can change the session and cause the other thread + * to use the invalid one). + * </p> + * + * @author Bernd Rinn + */ +final class EncapsulatedLimsService implements IEncapsulatedLimsService +{ + + private static final Logger operationLog = + LogFactory.getLogger(LogCategory.OPERATION, EncapsulatedLimsService.class); + + private final String username; + + private final String password; + + private final IETLLIMSService limsService; + + private String sessionToken; // NOTE: can be changed in parallel by different threads + + private Integer version; + + private DatabaseInstancePE homeDatabaseInstance; + + EncapsulatedLimsService(final IETLLIMSService limsService, final String username, + final String password) + { + assert limsService != null : "Given IETLLIMSService implementation can not be null."; + assert username != null : "Given username can not be null."; + assert password != null : "Given password can not be null."; + + this.limsService = limsService; + this.username = username; + this.password = password; + } + + private final void authenticate() + { + if (operationLog.isDebugEnabled()) + { + operationLog.debug("Authenticating to LIMS server as user '" + username + "'."); + } + sessionToken = limsService.authenticate(username, password); + if (sessionToken == null) + { + final String msg = + "Authentication failure to LIMS server. Most probable cause: user or password are invalid."; + throw new ConfigurationFailureException(msg); + } + } + + private final void checkSessionToken() + { + if (sessionToken == null) + { + authenticate(); + } + } + + private final ExperimentPE primGetBaseExperiment(final SampleIdentifier sampleIdentifier) + { + return limsService.tryToGetBaseExperiment(sessionToken, sampleIdentifier); + } + + private final void primRegisterDataSet(final DataSetInformation dataSetInformation, + final String procedureTypeCode, final ExternalData data) + { + limsService.registerDataSet(sessionToken, dataSetInformation.getSampleIdentifier(), + procedureTypeCode, data); + } + + private final SamplePropertyPE[] primGetPropertiesOfSampleRegisteredFor( + final SampleIdentifier sampleIdentifier) + { + return limsService.tryToGetPropertiesOfTopSampleRegisteredFor(sessionToken, sampleIdentifier); + } + + private final String primCreateDataSetCode() + { + return limsService.createDataSetCode(sessionToken); + } + + // + // IEncapsulatedLimsService + // + + synchronized public final ExperimentPE getBaseExperiment( + final SampleIdentifier sampleIdentifier) + { + assert sampleIdentifier != null : "Given sample identifier can not be null."; + + checkSessionToken(); + try + { + return primGetBaseExperiment(sampleIdentifier); + } catch (final InvalidSessionException ex) + { + authenticate(); + return primGetBaseExperiment(sampleIdentifier); + } + } + + synchronized public final void registerDataSet(final DataSetInformation dataSetInformation, + final String procedureTypeCode, final ExternalData data) + { + assert dataSetInformation != null : "missing sample identifier"; + assert procedureTypeCode != null : "missing procedure type"; + assert data != null : "missing data"; + + checkSessionToken(); + try + { + primRegisterDataSet(dataSetInformation, procedureTypeCode, data); + } catch (final InvalidSessionException ex) + { + authenticate(); + primRegisterDataSet(dataSetInformation, procedureTypeCode, data); + } + if (operationLog.isInfoEnabled()) + { + operationLog.info("Registered in openBIS: data set " + dataSetInformation.describe() + + " PROCEDURE_TYPE('" + procedureTypeCode + "')."); + } + } + + synchronized public final SamplePropertyPE[] getPropertiesOfTopSampleRegisteredFor( + final SampleIdentifier sampleIdentifier) throws UserFailureException + { + assert sampleIdentifier != null : "Given sample identifier can not be null."; + + checkSessionToken(); + try + { + return primGetPropertiesOfSampleRegisteredFor(sampleIdentifier); + } catch (final InvalidSessionException ex) + { + authenticate(); + return primGetPropertiesOfSampleRegisteredFor(sampleIdentifier); + } + } + + synchronized public final int getVersion() + { + checkSessionToken(); + if (version == null) + { + version = limsService.getVersion(); + } + return version; + } + + synchronized public final DatabaseInstancePE getHomeDatabaseInstance() + { + checkSessionToken(); + if (homeDatabaseInstance == null) + { + homeDatabaseInstance = limsService.getHomeDatabaseInstance(sessionToken); + } + return homeDatabaseInstance; + } + + synchronized public final String createDataSetCode() + { + checkSessionToken(); + try + { + return primCreateDataSetCode(); + } catch (final InvalidSessionException ex) + { + authenticate(); + return primCreateDataSetCode(); + } + } + +} \ No newline at end of file diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/FileBasedFile.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/FileBasedFile.java new file mode 100644 index 00000000000..bd025e258b1 --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/FileBasedFile.java @@ -0,0 +1,187 @@ +/* + * Copyright 2008 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import java.io.File; +import java.io.IOException; + +import org.apache.commons.io.FileUtils; + +import ch.systemsx.cisd.common.exceptions.CheckedExceptionTunnel; +import ch.systemsx.cisd.common.exceptions.ConfigurationFailureException; +import ch.systemsx.cisd.common.exceptions.EnvironmentFailureException; +import ch.systemsx.cisd.common.exceptions.StopException; +import ch.systemsx.cisd.common.filesystem.FileOperations; +import ch.systemsx.cisd.common.filesystem.FileUtilities; +import ch.systemsx.cisd.common.filesystem.IImmutableCopier; + +/** + * Adapter of {@link File}. Files are copies by creating hard links (if possible) if the parameter + * <var>hardLinkInsteadOfCopy</var> of the constructor is set to <code>true</code>. Otherwise files + * are always copied. + * + * @author Franz-Josef Elmer + */ +public class FileBasedFile implements IFile +{ + private final IImmutableCopier hardLinkCopierOrNull; + + private final File file; + + /** + * Creates a new instance for the specified file with the specified copy policy. + * + * @param file Real file wrapped by this adapter. + * @param hardLinkCopierOrNull If specified, will be used instead of the normal file system + * copier for copying files and directories. + */ + public FileBasedFile(final File file, final IImmutableCopier hardLinkCopierOrNull) + { + assert file != null : "Unspecified file."; + this.file = file; + this.hardLinkCopierOrNull = hardLinkCopierOrNull; + } + + public void copyFrom(final File sourceFile) + { + copy(sourceFile, file); + } + + public void copyTo(final File destinationFile) + { + copy(file, destinationFile); + } + + private void copy(final File sourceFile, final File destinationFile) + { + if (sourceFile.isDirectory()) + { + copyDirectory(sourceFile, destinationFile); + } else + { + copyFile(sourceFile, destinationFile); + } + } + + private void copyFile(final File sourceFile, final File destinationFile) + { + if (hardLinkCopierOrNull != null) + { + final File destinationDirectory = destinationFile.getParentFile(); + final boolean ok = + hardLinkCopierOrNull.copyImmutably(sourceFile, destinationDirectory, + destinationFile.getName()); + if (ok == false) + { + throw new EnvironmentFailureException("Couldn't copy '" + + sourceFile.getAbsolutePath() + "' using hard links to '" + + destinationFile.getAbsolutePath() + + "'. Maybe the destination already exists?"); + } + } else + { + try + { + StopException.check(); + FileUtils.copyFile(sourceFile, destinationFile, true); + } catch (final IOException ex) + { + throw CheckedExceptionTunnel.wrapIfNecessary(ex); + } + } + + } + + private void copyDirectory(final File sourceDirectory, final File destinationDirectory) + { + if (hardLinkCopierOrNull != null) + { + final File destinationParentDirectory = destinationDirectory.getParentFile(); + final boolean ok = + hardLinkCopierOrNull.copyImmutably(sourceDirectory, + destinationParentDirectory, destinationDirectory.getName()); + if (ok == false) + { + throw new EnvironmentFailureException("Couldn't copy '" + + sourceDirectory.getAbsolutePath() + "' using hard links to '" + + destinationDirectory.getAbsolutePath() + + "'. Maybe the destination already exists?"); + } + } else + { + try + { + StopException.check(); + FileUtils.copyDirectory(sourceDirectory, destinationDirectory, true); + } catch (final IOException ex) + { + throw CheckedExceptionTunnel.wrapIfNecessary(ex); + } + } + + } + + public void delete() + { + if (FileOperations.getMonitoredInstanceForCurrentThread().removeRecursivelyQueueing(file)) + { + throw new EnvironmentFailureException("Can not delete file '" + + file.getAbsolutePath() + "'."); + } + } + + public String getAbsolutePath() + { + return file.getAbsolutePath(); + } + + public byte[] read() + { + try + { + return FileUtils.readFileToByteArray(file); + } catch (final IOException ex) + { + throw CheckedExceptionTunnel.wrapIfNecessary(ex); + } + } + + public void write(final byte[] data) + { + try + { + FileUtils.writeByteArrayToFile(file, data); + } catch (final IOException ex) + { + throw CheckedExceptionTunnel.wrapIfNecessary(ex); + } + } + + public final void check() throws EnvironmentFailureException, ConfigurationFailureException + { + final String response = FileUtilities.checkDirectoryFullyAccessible(file, ""); + if (response != null) + { + throw new ConfigurationFailureException(response); + } + } + + public boolean isRemote() + { + return false; + } +} diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/FileBasedFileFactory.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/FileBasedFileFactory.java new file mode 100644 index 00000000000..659b123cedb --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/FileBasedFileFactory.java @@ -0,0 +1,98 @@ +/* + * Copyright 2008 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import java.io.File; + +import org.apache.commons.io.FilenameUtils; + +import ch.systemsx.cisd.common.TimingParameters; +import ch.systemsx.cisd.common.filesystem.FastRecursiveHardLinkMaker; +import ch.systemsx.cisd.common.filesystem.IImmutableCopier; + +/** + * File factory based on {@link File}. Files are copies by creating hard links (if possible) if the + * parameter <var>hardLinkInsteadOfCopy</var> of the constructor is set to <code>true</code>. + * Otherwise files are always copied. + * + * @author Franz-Josef Elmer + */ +public class FileBasedFileFactory implements IFileFactory +{ + private final IImmutableCopier hardLinkCopierOrNull; + + /** + * Creates a new instance with the specified copy policy. Uses default timing parameters. + * + * @param hardLinkInsteadOfCopy If <code>true</code> hard links instead of copies are created. + */ + public FileBasedFileFactory(final boolean hardLinkInsteadOfCopy) + { + this(hardLinkInsteadOfCopy, TimingParameters.getDefaultParameters()); + } + + /** + * Creates a new instance with the specified copy policy. + * + * @param hardLinkInsteadOfCopy If <code>true</code> hard links instead of copies are created. + * @param timingParameters The timing parameters to use for copy oprations. + */ + public FileBasedFileFactory(final boolean hardLinkInsteadOfCopy, + final TimingParameters timingParameters) + { + this.hardLinkCopierOrNull = + tryGetHardLinkCopier(hardLinkInsteadOfCopy, timingParameters); + } + + private static IImmutableCopier tryGetHardLinkCopier( + final boolean hardLinkInsteadOfCopy, final TimingParameters timingParameters) + { + if (hardLinkInsteadOfCopy) + { + return FastRecursiveHardLinkMaker.tryCreate(timingParameters); + } else + { + return null; + } + } + + private final IFile wrap(final File file) + { + return new FileBasedFile(file, hardLinkCopierOrNull); + } + + // + // IFileFactory + // + + public final IFile create(final String path) + { + assert path != null : "Unspecified path."; + final File file = new File(path); + return wrap(file); + } + + public final IFile create(final IFile baseDir, final String relativePath) + { + assert baseDir != null : "Unspecified base directory."; + assert relativePath != null : "Unspecified relative pate"; + assert FilenameUtils.getPrefixLength(relativePath) == 0 : String.format( + "Given relative path '%s' is not relative.", relativePath); + return wrap(new File(baseDir.getAbsolutePath(), relativePath)); + } + +} diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/FileRenamer.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/FileRenamer.java new file mode 100644 index 00000000000..f6a435d4236 --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/FileRenamer.java @@ -0,0 +1,83 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import java.io.File; + +import org.apache.log4j.Logger; + +import ch.systemsx.cisd.common.filesystem.FileOperations; +import ch.systemsx.cisd.common.logging.LogCategory; +import ch.systemsx.cisd.common.logging.LogFactory; + +/** + * A file renamer that logs it's operations. + * <p> + * Renames and logs the file renaming process. + * </p> + * + * @author Franz-Josef Elmer + */ +final class FileRenamer +{ + private final static Logger notificationLog = + LogFactory.getLogger(LogCategory.NOTIFY, FileRenamer.class); + + final static Logger operationLog = + LogFactory.getLogger(LogCategory.OPERATION, FileRenamer.class); + + /** + * Renames given <var>sourceFile</var> to given <var>destinationFile</var>. + * <p> + * Internally uses {@link FileOperations} and notifies the administrator if the process + * failed. + * </p> + */ + static final boolean renameAndLog(final File sourceFile, final File destinationFile) + { + final String absoluteTargetPath = destinationFile.getAbsolutePath(); + if (destinationFile.exists()) + { + notificationLog.error(String + .format("Destination file '%s' already exists. Won't overwrite it.", + absoluteTargetPath)); + return false; + } + boolean renamedOK = + FileOperations.getMonitoredInstanceForCurrentThread().rename(sourceFile, + destinationFile); + if (renamedOK) + { + if (operationLog.isInfoEnabled()) + { + final String entity = sourceFile.isDirectory() ? "directory" : "file"; + final String name = sourceFile.getName(); + final String parent = sourceFile.getParent(); + final String path = destinationFile.getParent(); + operationLog.info(String.format("Moving %s '%s' from '%s' to '%s'.", entity, name, + parent, path)); + } + return true; + } else + { + notificationLog.error(String.format("Moving '%s' to '%s' failed, giving up.", + sourceFile.getAbsolutePath(), absoluteTargetPath)); + return false; + } + } + +} diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/HCSImageCheckList.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/HCSImageCheckList.java new file mode 100644 index 00000000000..ec86d0e08a4 --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/HCSImageCheckList.java @@ -0,0 +1,173 @@ +/* + * Copyright 2008 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import ch.systemsx.cisd.bds.hcs.Geometry; +import ch.systemsx.cisd.bds.hcs.Location; +import ch.systemsx.cisd.common.utilities.AbstractHashable; + +/** + * Helper class to set the <code>is_complete</code> flag in the <i>BDS</i> library. + * <p> + * All the possible combinations are computed in the constructor. This class is also able to spot + * images which have already been handled. + * </p> + * + * @author Franz-Josef Elmer + */ +final class HCSImageCheckList +{ + + private final List<Map<FullLocation, Check>> list; + + HCSImageCheckList(final int numberOfChannels, final Geometry plateGeometry, + final Geometry wellGeometry) + { + if (numberOfChannels < 1) + { + throw new IllegalArgumentException("Number of channels smaller than one."); + } + if (plateGeometry == null) + { + throw new IllegalArgumentException("Unspecified plate geometry."); + } + if (wellGeometry == null) + { + throw new IllegalArgumentException("Unspecified well geometry."); + } + list = new ArrayList<Map<FullLocation, Check>>(); + for (int i = 0; i < numberOfChannels; i++) + { + final Map<FullLocation, Check> map = new HashMap<FullLocation, Check>(); + for (int plateX = 1; plateX <= plateGeometry.getColumns(); plateX++) + { + for (int plateY = 1; plateY <= plateGeometry.getRows(); plateY++) + { + final Location wellLocation = new Location(plateX, plateY); + for (int wellX = 1; wellX <= wellGeometry.getColumns(); wellX++) + { + for (int wellY = 1; wellY <= wellGeometry.getRows(); wellY++) + { + final Location tileLocation = new Location(wellX, wellY); + map.put(new FullLocation(wellLocation, tileLocation), new Check()); + } + } + } + } + assert map.size() == plateGeometry.getColumns() * plateGeometry.getRows() + * wellGeometry.getColumns() * wellGeometry.getRows() : "Wrong map size"; + list.add(map); + } + } + + final void checkOff(final int channel, final Location wellLocation, final Location tileLocation) + { + assert wellLocation != null : "Unspecified well location."; + assert tileLocation != null : "Unspecified tile location."; + if (channel < 1) + { + throw new IllegalArgumentException("Not a positive channel number: " + channel); + } + if (channel > list.size()) + { + throw new IllegalArgumentException("Channel number to large: " + channel + " > " + + list.size()); + } + final Map<FullLocation, Check> map = list.get(channel - 1); + final Check check = map.get(new FullLocation(wellLocation, tileLocation)); + if (check == null) + { + throw new IllegalArgumentException("Invalid well/tile location: " + wellLocation); + } + if (check.isCheckedOff()) + { + throw new IllegalArgumentException("Image already handle for channel" + channel + + ", well:" + wellLocation + " tile:" + tileLocation); + } + check.checkOff(); + } + + final List<FullLocation> getCheckedOnFullLocations() + { + final List<FullLocation> fullLocations = new ArrayList<FullLocation>(); + for (final Map<FullLocation, Check> map : list) + { + for (final Map.Entry<FullLocation, Check> entry : map.entrySet()) + { + if (entry.getValue().isCheckedOff() == false) + { + fullLocations.add(entry.getKey()); + } + } + } + return fullLocations; + } + + // + // Helper classes + // + + private static final class Check + { + private boolean checkedOff; + + final void checkOff() + { + checkedOff = true; + } + + final boolean isCheckedOff() + { + return checkedOff; + } + } + + final static class FullLocation extends AbstractHashable + { + + final Location wellLocation; + + final Location tileLocation; + + FullLocation(final Location wellLocation, final Location tileLocation) + { + this.wellLocation = wellLocation; + this.tileLocation = tileLocation; + } + + private final static String toString(final Location location, final String type) + { + return type + "=" + location; + } + + // + // AbstractHashable + // + + @Override + public final String toString() + { + return "[" + toString(wellLocation, "well") + "," + toString(tileLocation, "tile") + + "]"; + } + } +} diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/HCSImageFileExtractionResult.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/HCSImageFileExtractionResult.java new file mode 100644 index 00000000000..4294eaaa295 --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/HCSImageFileExtractionResult.java @@ -0,0 +1,82 @@ +/* + * Copyright 2008 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import java.util.List; +import java.util.Set; + +import ch.systemsx.cisd.bds.hcs.Channel; +import ch.systemsx.cisd.bds.storage.IFile; + +/** + * Class which contains the extraction process results. + */ +public final class HCSImageFileExtractionResult +{ + + /** The duration of the process. */ + private final long duration; + + /** The total number of files found. */ + private final int totalFiles; + + /** The invalid files found. */ + private final List<IFile> invalidFiles; + + /** The channels found. */ + private final Set<Channel> channels; + + public HCSImageFileExtractionResult(final long duration, final int totalFiles, + final List<IFile> invalidFiles, final Set<Channel> channels) + { + this.duration = duration; + this.totalFiles = totalFiles; + this.invalidFiles = invalidFiles; + this.channels = channels; + } + + /** + * Returns the duration of the process. + */ + public final long getDuration() + { + return duration; + } + + /** + * Returns the total number of files found. + */ + public final int getTotalFiles() + { + return totalFiles; + } + + /** + * Returns the invalid files found. + */ + public final List<IFile> getInvalidFiles() + { + return invalidFiles; + } + + /** + * Returns the channels found. + */ + public final Set<Channel> getChannels() + { + return channels; + } +} \ No newline at end of file diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/IDataSetInfoExtractor.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/IDataSetInfoExtractor.java new file mode 100644 index 00000000000..bbabefb64ba --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/IDataSetInfoExtractor.java @@ -0,0 +1,75 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import java.io.File; + +import ch.systemsx.cisd.common.exceptions.EnvironmentFailureException; +import ch.systemsx.cisd.common.exceptions.UserFailureException; + +/** + * A role to extract {@link DataSetInformation} from an incoming data set. Implementations of this + * interface are expected to have a constructor taking a {@link java.util.Properties} object as + * their only argument. The properties can be used to get further arguments that the extractor + * implementation requires to function. + * <p> + * The usage mode of implementations <var>extractorClassName</var>s is: + * + * <pre> + * Properties props = <get some props from somewhere> + * Class clazz = Class.forName(extractorClassName); + * IIDExtractor extractor = clazz.getConstructor(new Class[] { Properties.class } ).newInstance(new Object[] { props }); + * DataSetInformation info = extractor.getDataSetInformation(incomingDataSetPath); + * String experimentCode = info.getExperimentCode(); + * String dataSetCode = info.getSampleCode(); + * </pre> + * + * Implementations of this class are expected to be "re-usable". This is, calling the method + * {@link #getDataSetInformation(File)} multiple times for different data set on the same instance + * is expected to work. + * + * @author Bernd Rinn + */ +public interface IDataSetInfoExtractor +{ + + /** Properties key prefix for the extractor. */ + public static final String EXTRACTOR_KEY = "data-set-info-extractor"; + + /** + * Extracts data set information from the specified path of the incoming data set. + * <p> + * <i>Note that <code>incomingDataSetPath.getParent()</code> is arbitrary and the extracted id + * and code must not depend on it!</i> + * </p> + * + * @param incomingDataSetPath The path of the incoming data set. The path may be a file or + * directory. The caller needs to ensure that the path exists when this method is + * called. + * @return The information extracted about this data set. The code extractor <i>can</i>, but + * <i>doesn't have to</i> provide an group. If no group has been provided by the + * extractor then the one specified for the thread in the + * <code>service.properties</code> file (if any) will be taken. Never returns + * <code>null</code>. + * @throws UserFailureException If the incoming data set does not meet the expectations and thus + * the extractor can't extract either the experiment id or the data set code or + * both. + */ + public DataSetInformation getDataSetInformation(final File incomingDataSetPath) + throws UserFailureException, EnvironmentFailureException; + +} diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/IDataStoreStrategy.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/IDataStoreStrategy.java new file mode 100644 index 00000000000..1ecea806c76 --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/IDataStoreStrategy.java @@ -0,0 +1,55 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import java.io.File; + +import ch.systemsx.cisd.openbis.generic.shared.dto.DataSetType; + +/** + * This interface implements a strategy for storing data in the <i>store</i> directory. + * + * @author Christian Ribeaud + */ +interface IDataStoreStrategy +{ + + /** + * Returns the key associated with this <code>IDataStoreStrategy</code>. + * <p> + * This key uniquely identifies this <code>IDataStoreStrategy</code>. + * </p> + */ + public DataStoreStrategyKey getKey(); + + /** + * Returns the base directory where the data are going to be moved into. + */ + public File getBaseDirectory(final File baseDirectory, final DataSetInformation dataSetInfo, + final DataSetType dataSetType); + + /** + * Create the target path for given <var>baseDirectory</var> and given <var>incomingDataSetPath</var>. + * <p> + * Note that each call either produces a new <i>target path</i> or throws an exception if + * computed <i>target path</i> already exists. + * </p> + * + * @return The target path. + */ + public File getTargetPath(final File baseDirectory, final File incomingDataSetPath); +} diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/IDataStrategyStore.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/IDataStrategyStore.java new file mode 100644 index 00000000000..c88817c6ecb --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/IDataStrategyStore.java @@ -0,0 +1,43 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import java.io.File; + +/** + * The main purpose of this interface is to return a <code>IDataStoreStrategy</code> for a given + * <code>DataSetInformation</code>. + * <p> + * To perform its job it might use some helpers defined in the constructor. + * </p> + * + * @author Christian Ribeaud + */ +interface IDataStrategyStore +{ + + /** + * For given <var>dataSetInfo</var> and given <var>incomingDataSetPath</var> returns the + * corresponding <code>IDataStoreStrategy</code>. As a side effect sets also the sample + * properties, the experiment, and if not already set, the experiment identifier. + * + * @param dataSetInfo The data set information, gets enriched in the process. + * @param incomingDataSetPath mainly used for logging purposes. + */ + public IDataStoreStrategy getDataStoreStrategy(final DataSetInformation dataSetInfo, + final File incomingDataSetPath); +} \ No newline at end of file diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/IETLServerPlugin.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/IETLServerPlugin.java new file mode 100644 index 00000000000..e31d9dfe070 --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/IETLServerPlugin.java @@ -0,0 +1,41 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + + +/** + * Plugin interface of the ETL Server. + * + * @author Franz-Josef Elmer + */ +public interface IETLServerPlugin +{ + /** + * Returns the code extractor. + */ + public IDataSetInfoExtractor getDataSetInfoExtractor(); + + /** + * Returns the procedure and data type extractor. + */ + public IProcedureAndDataTypeExtractor getTypeExtractor(); + + /** + * Returns the {@link IStorageProcessor} implementation. + */ + public IStorageProcessor getStorageProcessor(); +} diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/IEncapsulatedLimsService.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/IEncapsulatedLimsService.java new file mode 100644 index 00000000000..e61b7a7958c --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/IEncapsulatedLimsService.java @@ -0,0 +1,75 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import ch.systemsx.cisd.common.exceptions.UserFailureException; +import ch.systemsx.cisd.openbis.generic.shared.IETLLIMSService; +import ch.systemsx.cisd.openbis.generic.shared.dto.DatabaseInstancePE; +import ch.systemsx.cisd.openbis.generic.shared.dto.ExperimentPE; +import ch.systemsx.cisd.openbis.generic.shared.dto.ExternalData; +import ch.systemsx.cisd.openbis.generic.shared.dto.SamplePropertyPE; +import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.SampleIdentifier; + +/** + * This interface is very similar to {@link IETLLIMSService} but <code>sessionToken</code> has + * been removed from each method. + * + * @see IETLLIMSService + * @author Christian Ribeaud + */ +interface IEncapsulatedLimsService +{ + /** + * For given <var>dataSetInfo</var> returns the <code>BaseExperiment</code> object. + */ + public ExperimentPE getBaseExperiment(final SampleIdentifier sampleIdentifier) + throws UserFailureException; + + /** + * Registers the specified data. + * <p> + * As side effect, sets <i>data set code</i> in {@link DataSetInformation#getExtractableData()}. + * </p> + */ + public void registerDataSet(final DataSetInformation dataSetInformation, + final String procedureTypeCode, final ExternalData data) throws UserFailureException; + + /** + * Tries to return the properties of the top sample (e.g. master plate) registered for the + * specified sample identifier. + * + * @return <code>null</code> if no appropriated sample found. Returns an empty array if a a + * sample found with no properties. + */ + public SamplePropertyPE[] getPropertiesOfTopSampleRegisteredFor( + final SampleIdentifier sampleIdentifier) throws UserFailureException; + + /** + * Creates and returns a unique code for a new data set. + */ + public String createDataSetCode(); + + /** + * Returns the version of the service. + */ + public int getVersion(); + + /** + * Returns the home database instance. + */ + public DatabaseInstancePE getHomeDatabaseInstance(); +} \ No newline at end of file diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/IFile.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/IFile.java new file mode 100644 index 00000000000..3e0afa9ee73 --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/IFile.java @@ -0,0 +1,58 @@ +/* + * Copyright 2008 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import java.io.File; + +import ch.systemsx.cisd.common.utilities.ISelfTestable; + +/** + * A file abstraction. + * + * @author Franz-Josef Elmer + */ +public interface IFile extends ISelfTestable +{ + /** + * Returns the absolute path of this file. + */ + public String getAbsolutePath(); + + /** + * Copies the file denoted by this abstract pathname to given <var>destinationFile</var>. + * <p> + * Note that, depending on the implementation, it effectively copies this abstract pathname or + * makes an hard link of it. + * </p> + */ + public void copyTo(File destinationFile); + + /** + * Copies given <code>sourceFile</code> to the file denoted by this abstract pathname. + * <p> + * Note that, depending on the implementation, it effectively copies the given <var>sourceFile</var> + * or makes an hard link of it. + * </p> + */ + public void copyFrom(File sourceFile); + + public byte[] read(); + + public void write(byte[] data); + + public void delete(); +} diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/IFileFactory.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/IFileFactory.java new file mode 100644 index 00000000000..6fe516bddf0 --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/IFileFactory.java @@ -0,0 +1,30 @@ +/* + * Copyright 2008 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +/** + * @author Franz-Josef Elmer + */ +public interface IFileFactory +{ + + /** Creates given <var>path</var>. */ + public IFile create(String path); + + /** Creates given <var>relativePath</var> in given <var>baseDir</var>. */ + public IFile create(IFile baseDir, String relativePath); +} diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/IHCSImageFileAccepter.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/IHCSImageFileAccepter.java new file mode 100644 index 00000000000..1dbeac69dc5 --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/IHCSImageFileAccepter.java @@ -0,0 +1,37 @@ +/* + * Copyright 2008 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import ch.systemsx.cisd.bds.hcs.Location; +import ch.systemsx.cisd.bds.storage.IFile; + +/** + * Role that is implemented by <i>ETL Server</i> core system. + * <p> + * It is called by the <code>IHCSImageFileExtractor</code> implementation as callback to register + * an image file at given coordinates. + * </p> + * + * @author Christian Ribeaud + */ +public interface IHCSImageFileAccepter +{ + /** + * Registers given <var>imageFile</var> at given <code>standard</code> coordinates. + */ + public void accept(final int channel, final Location wellLocation, final Location tileLocation, + final IFile imageFile); +} \ No newline at end of file diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/IHCSImageFileExtractor.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/IHCSImageFileExtractor.java new file mode 100644 index 00000000000..c412b147add --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/IHCSImageFileExtractor.java @@ -0,0 +1,44 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import java.util.Properties; + +import ch.systemsx.cisd.bds.storage.IDirectory; + +/** + * This role is supposed to be implemented by classes that can extract HCS image files from an + * incoming data set directory. Implementations of this interface need to have a constructor that + * takes {@link Properties} to initialize itself. + * + * @author Christian Ribeaud + */ +public interface IHCSImageFileExtractor +{ + + public final static String FILE_EXTRACTOR = "file-extractor"; + + /** + * Extracts <code>StandardCoordinates</code> in the given <var>incomingDataSetDirectory</var> + * and for the given <var>dataSetInfo</var> and hands the image files that it finds over to the + * specified <var>accepter</var>. + * + * @return the extraction result. Must not be <code>null</code>. + */ + public HCSImageFileExtractionResult process(final IDirectory incomingDataSetDirectory, + DataSetInformation dataSetInformation, final IHCSImageFileAccepter accepter); +} \ No newline at end of file diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/IProcedureAndDataTypeExtractor.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/IProcedureAndDataTypeExtractor.java new file mode 100644 index 00000000000..b305aeb45f3 --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/IProcedureAndDataTypeExtractor.java @@ -0,0 +1,55 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import java.io.File; + +import ch.systemsx.cisd.openbis.generic.shared.dto.DataSetType; +import ch.systemsx.cisd.openbis.generic.shared.dto.FileFormatType; +import ch.systemsx.cisd.openbis.generic.shared.dto.LocatorType; +import ch.systemsx.cisd.openbis.generic.shared.dto.ProcedureType; + +/** + * Extractor for procedure, data set, file format, and locator type. + * + * @author Franz-Josef Elmer + */ +public interface IProcedureAndDataTypeExtractor +{ + /** Properties key prefix for the type extractor. */ + public static final String TYPE_EXTRACTOR_KEY = "type-extractor"; + + /** + * Gets the procedure type from the specified path of the incoming data set. + */ + public ProcedureType getProcedureType(File incomingDataSetPath); + + /** + * Gets the data set type from the specified path of the incoming data set. + */ + public DataSetType getDataSetType(File incomingDataSetPath); + + /** + * Gets the file format type from the specified path of the incoming data set. + */ + public FileFormatType getFileFormatType(File incomingDataSetPath); + + /** + * Gets the locator type from the specified path of the incoming data set. + */ + public LocatorType getLocatorType(File incomingDataSetPath); +} diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/IProcessor.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/IProcessor.java new file mode 100644 index 00000000000..662d1a5ca49 --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/IProcessor.java @@ -0,0 +1,42 @@ +/* + * Copyright 2008 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import java.io.File; + +import ch.systemsx.cisd.openbis.generic.shared.dto.ProcessingInstructionDTO; +import ch.systemsx.cisd.openbis.generic.shared.dto.StorageFormat; + +/** + * Interface for processing a data set file or folder after registration. + * + * @author Franz-Josef Elmer + */ +public interface IProcessor +{ + /** + * Returns the required format of the input data used by this processor. + */ + public StorageFormat getRequiredInputDataFormat(); + + /** + * Initiates processing the <var>dataSet</var> with the specified processing instructions. + */ + public void initiateProcessing(final ProcessingInstructionDTO instructionOrNull, + final DataSetInformation dataSetInformation, final File dataSet); + +} diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/IProcessorFactory.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/IProcessorFactory.java new file mode 100644 index 00000000000..586cddae88d --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/IProcessorFactory.java @@ -0,0 +1,38 @@ +/* + * Copyright 2008 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import ch.systemsx.cisd.common.filesystem.PathPrefixPrepender; + +/** + * Factory for {@link IProcessor} instances. + * + * @author Franz-Josef Elmer + */ +public interface IProcessorFactory +{ + /** + * Creates a new processor. + */ + public IProcessor createProcessor(); + + /** + * Returns the {@link PathPrefixPrepender}. + */ + public PathPrefixPrepender getPathPrefixPrepender(); + +} diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/IStorageProcessor.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/IStorageProcessor.java new file mode 100644 index 00000000000..8d8dc3c4292 --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/IStorageProcessor.java @@ -0,0 +1,90 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import java.io.File; +import java.util.Properties; + +import ch.systemsx.cisd.common.mail.IMailClient; +import ch.systemsx.cisd.openbis.generic.shared.dto.ExperimentPE; +import ch.systemsx.cisd.openbis.generic.shared.dto.StorageFormat; + +/** + * Takes care of storing the data in the store root directory. + * <p> + * Implementations of this interface are expected to have a constructor taking a {@link Properties} + * object as their only argument. + * </p> + * + * @author Christian Ribeaud + */ +public interface IStorageProcessor extends IStoreRootDirectoryHolder +{ + /** Properties key prefix to find the {@link IStorageProcessor} implementation. */ + public static final String STORAGE_PROCESSOR_KEY = "storage-processor"; + + /** + * Stores the specified incoming data set file to the specified directory. In general some + * processing and/or transformation of the incoming data takes place. + * <p> + * Do not try/catch exceptions that could occur here. Preferably let the upper layer handle + * them. + * </p> + * + * @param experiment information about the related experiment. + * @param dataSetInformation Information about the data set. + * @param typeExtractor the {@link IProcedureAndDataTypeExtractor} implementation. + * @param mailClient mail client. + * @param incomingDataSetDirectory folder to store. Do not remove it after the implementation + * has finished processing. {@link TransferredDataSetHandler} takes care of this. + * @param rootDir directory to whom the data will be stored. + * @return folder which contains the stored data. This folder <i>must</i> be below the + * <var>rootDir</var>. Never returns <code>null</code> but prefers to throw an + * exception in case of unexpected behavior. + */ + public File storeData(final ExperimentPE experiment, + final DataSetInformation dataSetInformation, + final IProcedureAndDataTypeExtractor typeExtractor, final IMailClient mailClient, + final File incomingDataSetDirectory, final File rootDir); + + /** + * Performs a rollback of + * {@link #storeData(ExperimentPE, DataSetInformation, IProcedureAndDataTypeExtractor, IMailClient, File, File)} + * The data created in <code>directory</code> will also be removed. + * <p> + * Call to this method is safe as implementations should try/catch exceptions that could occur + * here. + * </p> + * + * @param incomingDataSetDirectory original folder to be restored. + * @param storedDataDirectory directory which contains the data to be restored. + */ + public void unstoreData(final File incomingDataSetDirectory, final File storedDataDirectory); + + /** + * Returns the format that this storage processor is storing data sets in. + */ + public StorageFormat getStorageFormat(); + + /** + * Returns the data set in the original proprietary format (before being processed) if + * available, or <code>null</code>, if the original data set is no longer available. + * <p> + * <strong>Consider the data in the returned file / directory read only!</strong> + */ + public File tryGetProprietaryData(final File storedDataDirectory); +} \ No newline at end of file diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/IStoreRootDirectoryHolder.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/IStoreRootDirectoryHolder.java new file mode 100644 index 00000000000..701da320a64 --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/IStoreRootDirectoryHolder.java @@ -0,0 +1,41 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import java.io.File; + +/** + * Implementations of this interface specifies the location of the store root directory. + * + * @author Christian Ribeaud + */ +public interface IStoreRootDirectoryHolder +{ + + /** + * Returns the store root directory. + * <p> + * Note that this method does not call {@link File#mkdirs()} on the returned path. + * </p> + */ + public File getStoreRootDirectory(); + + /** + * Sets the store root directory. + */ + public void setStoreRootDirectory(final File storeRootDirectory); +} \ No newline at end of file diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/IdentifiedDataStrategy.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/IdentifiedDataStrategy.java new file mode 100644 index 00000000000..98dc811072e --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/IdentifiedDataStrategy.java @@ -0,0 +1,180 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import java.io.File; + +import ch.systemsx.cisd.common.exceptions.EnvironmentFailureException; +import ch.systemsx.cisd.openbis.generic.shared.dto.DataSetType; +import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.ExperimentIdentifier; +import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.SampleIdentifier; + +/** + * This <code>IDataStoreStrategy</code> implementation if for data set that has been <i>identified</i>, + * meaning that kind of connection to this data set could be found in the database (through the + * derived <i>Master Plate</i> or through the experiment specified). + * + * @author Christian Ribeaud + */ +public final class IdentifiedDataStrategy implements IDataStoreStrategy +{ + static final String DATA_SET_TYPE_PREFIX = "DataSetType_"; + + static final String SAMPLE_PREFIX = "Sample_"; + + static final String EXPERIMENT_PREFIX = "Experiment_"; + + static final String PROJECT_PREFIX = "Project_"; + + static final String GROUP_PREFIX = "Group_"; + + static final String INSTANCE_PREFIX = "Instance_"; + + public static final String DATASET_PREFIX = "Dataset_"; + + static final String UNEXPECTED_PATHS_MSG_FORMAT = + "There are unexpected paths '%s' in data store '%s'. I'll proceed anyway."; + + static final String STORAGE_LAYOUT_ERROR_MSG_PREFIX = "Serious error in data store layout: "; + + IdentifiedDataStrategy() + { + + } + + private static String createInstanceDirectory(final DataSetInformation dataSetInfo) + { + final String instanceUUID = dataSetInfo.getInstanceUUID(); + assert instanceUUID != null : "Instance UUID can not be null."; + return INSTANCE_PREFIX + instanceUUID; + } + + private static String createGroupDirectory(final DataSetInformation dataSetInfo) + { + final ExperimentIdentifier identifier = dataSetInfo.getExperimentIdentifier(); + assert identifier != null : "Identifier can not be null."; + final String groupCode = identifier.getGroupCode(); + assert groupCode != null : "Group code can not be null."; + return GROUP_PREFIX + groupCode; + } + + private static String createProjectDirectory(final DataSetInformation dataSetInfo) + { + final ExperimentIdentifier identifier = dataSetInfo.getExperimentIdentifier(); + assert identifier != null : "Identifier can not be null."; + final String projectCode = identifier.getProjectCode(); + assert projectCode != null : "Project code can not be null."; + return PROJECT_PREFIX + projectCode; + } + + private static String createExperimentDirectory(final DataSetInformation dataSetInfo) + { + final ExperimentIdentifier identifier = dataSetInfo.getExperimentIdentifier(); + assert identifier != null : "Identifier can not be null."; + final String experimentCode = identifier.getExperimentCode(); + assert experimentCode != null : "Experiment code can not be null."; + return EXPERIMENT_PREFIX + experimentCode; + } + + private final static String createSampleDirectory(final DataSetInformation dataSetInfo) + { + final SampleIdentifier sampleIdentifier = dataSetInfo.getSampleIdentifier(); + assert sampleIdentifier != null : "Sample identifier can not be null."; + return SAMPLE_PREFIX + sampleIdentifier.getSampleCode(); + } + + private static String createDatasetDirectory(final DataSetInformation dataSetInfo) + { + final String dataSetCode = dataSetInfo.getDataSetCode(); + assert dataSetCode != null : "Dataset code con not be null."; + return DATASET_PREFIX + dataSetCode; + } + + final static String createDataSetTypeDirectory(final DataSetType dataSetType) + { + final String dataSetTypeCode = dataSetType.getCode(); + assert dataSetTypeCode != null : "Data set type code can not be null."; + return DATA_SET_TYPE_PREFIX + dataSetTypeCode; + } + + /** + * Computes the base directory with given <var>baseDir</var> and given <var>dataSetInfo</var> + * and returns it as <code>File</code>. + * <p> + * Note that this method does not call {@link File#mkdirs()} on returned <code>File</code>. + * </p> + */ + private final static File createBaseDirectory(final File baseDir, + final DataSetInformation dataSetInfo, final DataSetType dataSetType) + { + final File instanceDir = new File(baseDir, createInstanceDirectory(dataSetInfo)); + final File groupDir = new File(instanceDir, createGroupDirectory(dataSetInfo)); + final File projectDir = new File(groupDir, createProjectDirectory(dataSetInfo)); + final File experimentDir = new File(projectDir, createExperimentDirectory(dataSetInfo)); + final File dataSetTypeDir = + new File(experimentDir, createDataSetTypeDirectory(dataSetType)); + final File sampleDir = new File(dataSetTypeDir, createSampleDirectory(dataSetInfo)); + final File datasetDir = new File(sampleDir, createDatasetDirectory(dataSetInfo)); + return datasetDir; + + } + + // + // IDataStoreStrategy + // + + public final DataStoreStrategyKey getKey() + { + return DataStoreStrategyKey.IDENTIFIED; + } + + public final File getBaseDirectory(final File storeRoot, final DataSetInformation dataSetInfo, + final DataSetType dataSetType) + { + assert storeRoot != null : "Store root can not be null"; + assert dataSetInfo != null : "Data set information can not be null"; + assert dataSetType != null : "Data set type can not be null"; + final File baseDirectory = createBaseDirectory(storeRoot, dataSetInfo, dataSetType); + if (baseDirectory.exists()) + { + throw EnvironmentFailureException.fromTemplate(STORAGE_LAYOUT_ERROR_MSG_PREFIX + + "Data set directory '%s' exists but has been designed to be unique.", + baseDirectory.getPath()); + } + if (baseDirectory.isFile()) + { + throw EnvironmentFailureException.fromTemplate(STORAGE_LAYOUT_ERROR_MSG_PREFIX + + "Base directory '%s' is a file.", baseDirectory); + } + return baseDirectory; + } + + public final File getTargetPath(final File baseDirectory, final File incomingDataSetPath) + throws IllegalStateException + { + assert baseDirectory != null : "Base directory can not be null"; + assert incomingDataSetPath != null : "Incoming data set can not be null"; + final File targetPath = new File(baseDirectory, incomingDataSetPath.getName()); + if (targetPath.exists()) + { + throw new IllegalStateException(String.format( + "Target path '%s' of identified incoming data set already exists " + + "(which it shouldn't), bailing out.", targetPath.getAbsolutePath())); + } + return targetPath; + } +} diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/Main.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/Main.java new file mode 100644 index 00000000000..f573e571829 --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/Main.java @@ -0,0 +1,497 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import java.io.File; +import java.io.FilenameFilter; +import java.lang.Thread.UncaughtExceptionHandler; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Timer; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.io.filefilter.FileFilterUtils; +import org.apache.commons.io.filefilter.NameFileFilter; +import org.apache.log4j.Level; +import org.apache.log4j.Logger; + +import ch.rinn.restrictions.Private; +import ch.systemsx.cisd.common.Constants; +import ch.systemsx.cisd.common.TimingParameters; +import ch.systemsx.cisd.common.concurrent.TimerUtilities; +import ch.systemsx.cisd.common.exceptions.ConfigurationFailureException; +import ch.systemsx.cisd.common.exceptions.EnvironmentFailureException; +import ch.systemsx.cisd.common.exceptions.HighLevelException; +import ch.systemsx.cisd.common.exceptions.StopException; +import ch.systemsx.cisd.common.filesystem.DirectoryScanningTimerTask; +import ch.systemsx.cisd.common.filesystem.FaultyPathDirectoryScanningHandler; +import ch.systemsx.cisd.common.filesystem.FileUtilities; +import ch.systemsx.cisd.common.filesystem.IDirectoryScanningHandler; +import ch.systemsx.cisd.common.filesystem.PathPrefixPrepender; +import ch.systemsx.cisd.common.filesystem.QueueingPathRemoverService; +import ch.systemsx.cisd.common.highwatermark.HighwaterMarkDirectoryScanningHandler; +import ch.systemsx.cisd.common.highwatermark.HighwaterMarkWatcher; +import ch.systemsx.cisd.common.highwatermark.HostAwareFileWithHighwaterMark; +import ch.systemsx.cisd.common.logging.LogCategory; +import ch.systemsx.cisd.common.logging.LogFactory; +import ch.systemsx.cisd.common.logging.LogInitializer; +import ch.systemsx.cisd.common.spring.HttpInvokerUtils; +import ch.systemsx.cisd.common.utilities.BuildAndEnvironmentInfo; +import ch.systemsx.cisd.common.utilities.IExitHandler; +import ch.systemsx.cisd.common.utilities.ISelfTestable; +import ch.systemsx.cisd.common.utilities.IStopSignaler; +import ch.systemsx.cisd.common.utilities.PropertyUtils; +import ch.systemsx.cisd.common.utilities.SystemExit; +import ch.systemsx.cisd.openbis.generic.shared.IETLLIMSService; +import ch.systemsx.cisd.openbis.generic.shared.IWebService; +import ch.systemsx.cisd.openbis.generic.shared.dto.DatabaseInstancePE; + +/** + * The main class of the ETL server. + * + * @author Bernd Rinn + */ +public final class Main +{ + static final String STOREROOT_DIR_KEY = "storeroot-dir"; + + static final String NOTIFY_SUCCESSFUL_REGISTRATION = "notify-successful-registration"; + + private static final Logger operationLog = + LogFactory.getLogger(LogCategory.OPERATION, Main.class); + + private static final Logger notificationLog = + LogFactory.getLogger(LogCategory.NOTIFY, Main.class); + + private static final File shredderQueueFile = new File(".shredder"); + + private static final UncaughtExceptionHandler loggingExceptionHandler = + new UncaughtExceptionHandler() + { + + // + // UncaughtExceptionHandler + // + + public final void uncaughtException(final Thread t, final Throwable e) + { + notificationLog.error("An exception has occurred [thread: '" + t.getName() + + "'].", e); + } + }; + + @Private + static IExitHandler exitHandler = SystemExit.SYSTEM_EXIT; + + private static void initLog() + { + LogInitializer.init(); + Thread.setDefaultUncaughtExceptionHandler(loggingExceptionHandler); + } + + private static void printInitialLogMessage(final Parameters parameters) + { + operationLog.info("Etlserver is starting up."); + for (final String line : BuildAndEnvironmentInfo.INSTANCE.getEnvironmentInfo()) + { + operationLog.info(line); + } + parameters.log(); + } + + private static boolean checkListShredder(final String[] args) + { + if (args.length > 0 && args[0].equals("--show-shredder")) + { + final List<File> shredderItems = + QueueingPathRemoverService.listShredderItems(shredderQueueFile); + if (shredderItems.isEmpty()) + { + System.out.println("Shredder is empty."); + } else + { + System.out.println("Found " + shredderItems.size() + " items in shredder:"); + for (final File f : shredderItems) + { + System.out.println(f.getAbsolutePath()); + } + } + return true; + } else + { + return false; + } + } + + private static void selfTest(final File incomingDirectory, + final IEncapsulatedLimsService service, final ISelfTestable... selfTestables) + { + final String msgStart = "Etlserver self test failed:"; + ISelfTestable currentSelfTestableOrNull = null; + try + { + checkFullyAccesible(incomingDirectory); + final int serviceVersion = service.getVersion(); + if (IWebService.VERSION != serviceVersion) + { + throw new ConfigurationFailureException( + "This client has the wrong service version for the server (client: " + + IWebService.VERSION + ", server: " + serviceVersion + ")."); + } + for (final ISelfTestable selfTestableOrNull : selfTestables) + { + if (selfTestableOrNull != null) + { + currentSelfTestableOrNull = selfTestableOrNull; + selfTestableOrNull.check(); + } + } + } catch (final HighLevelException e) + { + if (currentSelfTestableOrNull != null && currentSelfTestableOrNull.isRemote()) + { + notificationLog.error("Self test on self-testable " + + currentSelfTestableOrNull.getClass().getSimpleName() + + " failed. This it relies on a remote resource which might become " + + "available at at later time, we keep the server running anyway.", e); + } else + { + System.err.printf(msgStart + " [%s: %s]\n", e.getClass().getSimpleName(), e + .getMessage()); + exitHandler.exit(1); + } + } catch (final RuntimeException e) + { + System.err.println(msgStart); + e.printStackTrace(); + exitHandler.exit(1); + } + if (TimerUtilities.isOperational()) + { + if (operationLog.isInfoEnabled()) + { + operationLog.info("Timer task interruption is operational."); + } + } else + { + operationLog.warn("Timer task interruption is not operational. " + + "No clean up can be performed on extraordinary shutdown."); + } + + } + + private static void checkFullyAccesible(final File directory) + throws ConfigurationFailureException + { + if (operationLog.isDebugEnabled()) + { + operationLog.debug("Checking source directory '" + directory.getAbsolutePath() + "'."); + } + final String errorMessage = + FileUtilities.checkDirectoryFullyAccessible(directory, "source"); + if (errorMessage != null) + { + throw new ConfigurationFailureException(errorMessage); + } + } + + private static IETLLIMSService getETLLIMSService(final Parameters parameters) + { + final String serviceURL = getServiceURL(parameters) + "/rmi-etl"; + final IETLLIMSService service = HttpInvokerUtils.createServiceStub(IETLLIMSService.class, serviceURL, 5); + return service; + } + + private static String getServiceURL(final Parameters parameters) + { + final String serverURL = parameters.getServerURL(); + if (serverURL == null) + { + throw new EnvironmentFailureException("Application Server URL is not defined."); + } + return serverURL; + } + + private static void startupServer(final Parameters parameters) + { + final Map<String, IProcessorFactory> processorFactories = + new LinkedHashMap<String, IProcessorFactory>(); + final Map<String, Properties> processorProperties = parameters.getProcessorProperties(); + for (final Map.Entry<String, Properties> entry : processorProperties.entrySet()) + { + processorFactories.put(entry.getKey(), StandardProcessorFactory + .create(entry.getValue())); + } + final ThreadParameters[] threads = parameters.getThreads(); + final IEncapsulatedLimsService authorizedLimsService = + createAuthorizedLimsService(parameters); + final Properties properties = parameters.getProperties(); + final boolean notifySuccessfulRegistration = getNotifySuccessfulRegistration(properties); + final HighwaterMarkWatcher highwaterMarkWatcher = + new HighwaterMarkWatcher(getHighwaterMark(properties)); + for (final ThreadParameters threadParameters : threads) + { + createProcessingThread(parameters, threadParameters, authorizedLimsService, + processorFactories, highwaterMarkWatcher, notifySuccessfulRegistration); + } + } + + private static IEncapsulatedLimsService createAuthorizedLimsService(final Parameters parameters) + { + final String username = parameters.getUsername(); + final String password = parameters.getPassword(); + + final IETLLIMSService limsService = getETLLIMSService(parameters); + return new EncapsulatedLimsService(limsService, username, password); + } + + private final static File getStoreRootDir(final Properties properties) + { + return FileUtilities.normalizeFile(new File(PropertyUtils.getMandatoryProperty(properties, + STOREROOT_DIR_KEY))); + } + + @Private + final static void migrateStoreRootDir(final File storeRootDir, + final DatabaseInstancePE databaseInstancePE) + { + final File[] instanceDirs = + storeRootDir.listFiles((FilenameFilter) new NameFileFilter( + IdentifiedDataStrategy.INSTANCE_PREFIX + databaseInstancePE.getCode())); + final int size = instanceDirs.length; + assert size == 0 || size == 1 : "Wrong size of instance directories."; + final String absolutePath = storeRootDir.getAbsolutePath(); + if (size == 0) + { + operationLog.info(String.format("No instance directory has been renamed " + + "in store root directory '%s'.", absolutePath)); + } else + { + final File instanceDir = instanceDirs[0]; + final File newName = + new File(storeRootDir, IdentifiedDataStrategy.INSTANCE_PREFIX + + databaseInstancePE.getUuid()); + instanceDir.renameTo(newName); + operationLog.info(String.format("Following instance directory '%s' has been " + + "renamed to '%s' in store root directory '%s'.", instanceDir.getName(), + newName.getName(), absolutePath)); + } + } + + @Private + final static List<File> findFiles(final File root, String prefix, int maxDepth) + { + ArrayList<File> files = new ArrayList<File>(); + if (maxDepth == 0) + { + if (root.getName().startsWith(prefix)) + { + files.add(root); + } + } else + { + if (root.isDirectory()) + { + for (File file : root.listFiles()) + { + files.addAll(findFiles(file, prefix, maxDepth - 1)); + } + } + } + return files; + } + + @Private + final static void migrateDataStoreByRenamingObservableTypeToDataSetType(final File root) + { + final String observableTypeDirPrefix = "ObservableType_"; + final String dataSetTypeDirPrefix = "DataSetType_"; + final String observableTypeFilePrefix = "observable_type"; + final String dataSetTypeFilePrefix = "data_set_type"; + + for (File file : findFiles(root, observableTypeDirPrefix, 5)) + { + final File newName = + new File(file.getAbsolutePath().replaceFirst(observableTypeDirPrefix, + dataSetTypeDirPrefix)); + file.renameTo(newName); + } + for (File file : findFiles(root, observableTypeFilePrefix, 10)) + { + final File newName = + new File(file.getAbsolutePath().replaceFirst(observableTypeFilePrefix, + dataSetTypeFilePrefix)); + file.renameTo(newName); + } + } + + private final static long getHighwaterMark(final Properties properties) + { + return PropertyUtils.getLong(properties, + HostAwareFileWithHighwaterMark.HIGHWATER_MARK_PROPERTY_KEY, -1L); + } + + private final static boolean getNotifySuccessfulRegistration(final Properties properties) + { + return PropertyUtils.getBoolean(properties, NOTIFY_SUCCESSFUL_REGISTRATION, false); + } + + private final static void createProcessingThread(final Parameters parameters, + final ThreadParameters threadParameters, + final IEncapsulatedLimsService authorizedLimsService, + final Map<String, IProcessorFactory> processorFactories, + final HighwaterMarkWatcher highwaterMarkWatcher, + final boolean notifySuccessfulRegistration) + { + final File incomingDataDirectory = threadParameters.getIncomingDataDirectory(); + final IETLServerPlugin plugin = threadParameters.getPlugin(); + final Properties properties = parameters.getProperties(); + final File storeRootDir = getStoreRootDir(properties); + migrateStoreRootDir(storeRootDir, authorizedLimsService.getHomeDatabaseInstance()); + migrateDataStoreByRenamingObservableTypeToDataSetType(storeRootDir); + plugin.getStorageProcessor().setStoreRootDirectory(storeRootDir); + final Properties mailProperties = parameters.getMailProperties(); + final TransferredDataSetHandler pathHandler = + new TransferredDataSetHandler(threadParameters.tryGetGroupCode(), plugin, + authorizedLimsService, mailProperties, highwaterMarkWatcher, + notifySuccessfulRegistration); + pathHandler.setProcessorFactories(processorFactories); + final HighwaterMarkDirectoryScanningHandler directoryScanningHandler = + createDirectoryScanningHandler(pathHandler, highwaterMarkWatcher, + incomingDataDirectory, storeRootDir, processorFactories.values()); + final DirectoryScanningTimerTask dataMonitorTask = + new DirectoryScanningTimerTask(incomingDataDirectory, FileFilterUtils + .prefixFileFilter(Constants.IS_FINISHED_PREFIX), pathHandler, + directoryScanningHandler); + selfTest(incomingDataDirectory, authorizedLimsService, pathHandler); + final String timerThreadName = + threadParameters.getThreadName() + " - Incoming Data Monitor"; + final Timer workerTimer = new Timer(timerThreadName); + workerTimer.schedule(dataMonitorTask, 0L, parameters.getCheckIntervalMillis()); + addShutdownHookForCleanup(workerTimer, pathHandler, parameters.getShutdownTimeOutMillis(), + threadParameters.getThreadName()); + } + + private static void addShutdownHookForCleanup(final Timer workerTimer, + final TransferredDataSetHandler mover, final long timeoutMillis, final String threadName) + { + Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() + { + public void run() + { + try + { + if (operationLog.isInfoEnabled()) + { + operationLog.info("Requesting shutdown lock of thread '" + threadName + + "'."); + } + final long startTimeMillis = System.currentTimeMillis(); + final boolean lockOK = + mover.getRegistrationLock().tryLock(timeoutMillis, + TimeUnit.MILLISECONDS); + final long timeoutLeftMillis = + Math.max(timeoutMillis / 2, timeoutMillis + - (System.currentTimeMillis() - startTimeMillis)); + if (lockOK == false) + { + operationLog.error("Failed to get lock for shutdown of thread '" + + threadName + "'."); + } + try + { + if (operationLog.isInfoEnabled()) + { + operationLog.info(String.format("Initiating shutdown sequence " + + "[maximal shutdown time: %ds].", + 2 * timeoutLeftMillis / 1000)); + } + final boolean shutdownOK = + TimerUtilities.tryShutdownTimer(workerTimer, timeoutLeftMillis); + operationLog.log(shutdownOK ? Level.INFO : Level.ERROR, + "Worker thread shutdown, status=" + + (shutdownOK ? "OK" : "FAILED")); + } finally + { + if (lockOK) + { + mover.getRegistrationLock().unlock(); + } + } + operationLog.warn("Shutting down shredder(s)"); + QueueingPathRemoverService.stopAndWait(timeoutMillis); + } catch (final InterruptedException ex) + { + throw new StopException(ex); + } finally + { + if (operationLog.isInfoEnabled()) + { + operationLog.info("Shutting down now."); + } + } + } + }, threadName + " - Shutdown Handler")); + } + + private final static HighwaterMarkDirectoryScanningHandler createDirectoryScanningHandler( + final IStopSignaler stopSignaler, final HighwaterMarkWatcher highwaterMarkWatcher, + final File incomingDataDirectory, final File storeRootDir, + final Iterable<IProcessorFactory> processorFactories) + { + final IDirectoryScanningHandler faultyPathHandler = + new FaultyPathDirectoryScanningHandler(incomingDataDirectory, stopSignaler); + final List<File> list = new ArrayList<File>(); + list.add(incomingDataDirectory); + for (final IProcessorFactory processorFactory : processorFactories) + { + final PathPrefixPrepender pathPrefixPrepender = + processorFactory.getPathPrefixPrepender(); + File file = pathPrefixPrepender.tryGetDirectoryForAbsolutePaths(); + if (file != null) + { + list.add(file); + } + file = pathPrefixPrepender.tryGetDirectoryForRelativePaths(); + if (file != null) + { + list.add(file); + } + } + return new HighwaterMarkDirectoryScanningHandler(faultyPathHandler, highwaterMarkWatcher, + list.toArray(new File[0])); + } + + public final static void main(final String[] args) + { + if (checkListShredder(args)) + { + System.exit(0); + } + initLog(); + final Parameters parameters = new Parameters(args); + TimingParameters.setDefault(parameters.getTimingParameters()); + QueueingPathRemoverService.start(shredderQueueFile); + printInitialLogMessage(parameters); + startupServer(parameters); + operationLog.info("etlserver ready and waiting for data."); + } + +} diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/NamedDataStrategy.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/NamedDataStrategy.java new file mode 100644 index 00000000000..2fb39810001 --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/NamedDataStrategy.java @@ -0,0 +1,94 @@ +/* + * Copyright 2008 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import java.io.File; +import java.util.regex.Pattern; + +import ch.systemsx.cisd.common.filesystem.FileUtilities; +import ch.systemsx.cisd.openbis.generic.shared.dto.DataSetType; + +/** + * A <code>IDataStoreStrategy</code> implementation that creates a named directory and put the + * candidates (data sets) using numbered subdirectories. + * + * @author Christian Ribeaud + */ +final class NamedDataStrategy implements IDataStoreStrategy +{ + /** + * A pattern used in {@link FileUtilities#createNextNumberedFile(File, Pattern, String)} to + * number the files in a given directory. + */ + private final static Pattern multipleFilesPatterns = Pattern.compile("_\\[([0-9]+)\\]"); + + private final DataStoreStrategyKey key; + + final static File createTargetPath(final File targetPath) + { + assert targetPath != null : "Given target path can not be null."; + final String defaultFileName = targetPath.getName() + "_[1]"; + return FileUtilities.createNextNumberedFile(targetPath, multipleFilesPatterns, + defaultFileName); + } + + NamedDataStrategy(final DataStoreStrategyKey key) + { + super(); + this.key = key; + } + + static final String getDirectoryName(final DataStoreStrategyKey key) + { + return key.name().toLowerCase(); + } + + private final String getDirectoryName() + { + return getDirectoryName(key); + } + + private final static void assertBaseDirectory(final File baseDirectory) + { + assert baseDirectory != null : "Missing base directory."; + } + + // + // IDataStoreStrategy + // + + public final DataStoreStrategyKey getKey() + { + return key; + } + + public final File getBaseDirectory(final File baseDirectory, + final DataSetInformation dataSetInfo, final DataSetType dataSetType) + { + assertBaseDirectory(baseDirectory); + assert dataSetType != null : "Missing data set type."; + return new File(new File(baseDirectory, getDirectoryName()), IdentifiedDataStrategy + .createDataSetTypeDirectory(dataSetType)); + } + + public final File getTargetPath(final File baseDirectory, final File incomingDataSetPath) + { + assertBaseDirectory(baseDirectory); + assert incomingDataSetPath != null : "Missing incoming data set path"; + return createTargetPath(new File(baseDirectory, incomingDataSetPath.getName())); + } +} diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/Parameters.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/Parameters.java new file mode 100644 index 00000000000..3deee7fbff6 --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/Parameters.java @@ -0,0 +1,403 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import org.apache.commons.lang.time.DateUtils; +import org.apache.log4j.Logger; + +import ch.systemsx.cisd.args4j.CmdLineException; +import ch.systemsx.cisd.args4j.CmdLineParser; +import ch.systemsx.cisd.args4j.ExampleMode; +import ch.systemsx.cisd.args4j.Option; +import ch.systemsx.cisd.common.TimingParameters; +import ch.systemsx.cisd.common.exceptions.ConfigurationFailureException; +import ch.systemsx.cisd.common.exceptions.UserFailureException; +import ch.systemsx.cisd.common.logging.LogCategory; +import ch.systemsx.cisd.common.logging.LogFactory; +import ch.systemsx.cisd.common.mail.JavaMailProperties; +import ch.systemsx.cisd.common.utilities.BuildAndEnvironmentInfo; +import ch.systemsx.cisd.common.utilities.ExtendedProperties; +import ch.systemsx.cisd.common.utilities.IExitHandler; +import ch.systemsx.cisd.common.utilities.PropertyUtils; +import ch.systemsx.cisd.common.utilities.SystemExit; + +/** + * The class to process the command line parameters and service properties. + * + * @author Bernd Rinn + */ +public class Parameters +{ + private static final String SERVICE_PROPERTIES_FILE = "etc/service.properties"; + + private static final Logger operationLog = + LogFactory.getLogger(LogCategory.OPERATION, Parameters.class); + + private static final Logger notificationLog = + LogFactory.getLogger(LogCategory.NOTIFY, Parameters.class); + + /** property with thread names separated by {@link #ITEMS_DELIMITER} */ + private static final String INPUT_THREAD_NAMES = "inputs"; + + private static final String ITEMS_DELIMITER = ","; + + @Option(name = "s", longName = "server-url", metaVar = "URL", usage = "URL of the server") + private String serverURL; + + /** + * The interval to wait between to checks for activity (in milliseconds). + */ + @Option(name = "c", longName = "check-interval", usage = "The interval to wait between two checks (in seconds) " + + "[default: 120]") + private long checkIntervalSeconds; + + /** + * The time-out for clean up work in the shutdown sequence (in seconds). + * <p> + * Note that that the maximal time for the shutdown sequence to complete can be as large as + * twice this time. + */ + @Option(name = "t", longName = "shutdown-timeout", usage = "The time-out for clean up work " + + "in the shutdown sequence (in seconds) [default: 30]") + private long shutdownTimeOutSeconds; + + /** + * The username to access the LIMS server with. + */ + @Option(name = "u", longName = "username", usage = "User login name") + private String username; + + /** + * The password to access the LIMS server with. + */ + @Option(name = "p", longName = "password", usage = "User login password") + private String password; + + /** A subset of <code>service.properties</code> that are reserved for the <i>JavaMail API</i>. */ + private Properties mailProperties; + + /** + * The command line parser. + */ + private final CmdLineParser parser = new CmdLineParser(this); + + private TimingParameters timingParameters; + + private Properties serviceProperties; + + private ThreadParameters[] threads; + + private Map<String, Properties> processorProperties; + + @Option(longName = "help", skipForExample = true, usage = "Prints out a description of the options.") + void printHelp(final boolean exit) + { + parser.printHelp("etlserver", "<required options> [option [...]]", "", ExampleMode.ALL); + if (exit) + { + System.exit(0); + } + } + + @Option(longName = "version", skipForExample = true, usage = "Prints out the version information.") + void printVersion(final boolean exit) + { + System.err + .println("etlserver version " + BuildAndEnvironmentInfo.INSTANCE.getFullVersion()); + if (exit) + { + System.exit(0); + } + } + + @Option(longName = "test-notify", skipForExample = true, usage = "Tests the notify log (i.e. that an email is " + + "sent out).") + void sendTestNotification(final boolean exit) + { + notificationLog + .error("This is a test notification given due to specifying the --test-notify option."); + if (exit) + { + System.exit(0); + } + } + + Parameters(final String[] args) + { + this(args, SystemExit.SYSTEM_EXIT); + } + + Parameters(final String[] args, final IExitHandler systemExitHandler) + { + try + { + initParametersFromProperties(); + + parser.parseArgument(args); + for (final ThreadParameters thread : threads) + { + thread.check(); + } + if (serverURL == null) + { + throw new ConfigurationFailureException("No 'server-url' defined."); + } + } catch (final Exception ex) + { + outputException(ex); + systemExitHandler.exit(1); + // Only reached in unit tests. + throw new AssertionError(ex.getMessage()); + } + } + + private void initParametersFromProperties() + { + serviceProperties = PropertyUtils.loadProperties(SERVICE_PROPERTIES_FILE); + PropertyUtils.trimProperties(serviceProperties); + processorProperties = extractProcessorProperties(serviceProperties); + threads = createThreadParameters(serviceProperties); + serverURL = serviceProperties.getProperty("server-url"); + username = serviceProperties.getProperty("username"); + password = serviceProperties.getProperty("password"); + checkIntervalSeconds = + Long.parseLong(serviceProperties.getProperty("check-interval", "120")); + shutdownTimeOutSeconds = + Long.parseLong(serviceProperties.getProperty("shutdown-timeout", "30")); + mailProperties = createMailProperties(serviceProperties); + timingParameters = TimingParameters.create(serviceProperties); + } + + private static Map<String, Properties> extractProcessorProperties(final Properties properties) + { + final LinkedHashMap<String, Properties> map = new LinkedHashMap<String, Properties>(); + final String processors = properties.getProperty("processors"); + if (processors != null) + { + final String[] procedureTypes = processors.split(ITEMS_DELIMITER); + for (final String procedureType : procedureTypes) + { + final String prefix = "processor." + procedureType + "."; + map.put(procedureType, ExtendedProperties.getSubset(properties, prefix, true)); + } + } + return map; + } + + private static ThreadParameters[] createThreadParameters(final Properties serviceProperties) + { + final String threadNames = serviceProperties.getProperty(INPUT_THREAD_NAMES); + if (threadNames == null) + { + // backward compatibility mode - no prefixes before thread properties, one thread only + return new ThreadParameters[] + { new ThreadParameters(serviceProperties, "default") }; + } else + { + final String[] names = threadNames.split(ITEMS_DELIMITER); + validateThreadNames(names); + return createThreadParameters(names, serviceProperties); + } + } + + private static ThreadParameters[] createThreadParameters(final String[] names, + final Properties serviceProperties) + { + final ThreadParameters[] threadParameters = new ThreadParameters[names.length]; + final Properties generalProperties = + removeThreadSpecificProperties(names, serviceProperties); + for (int i = 0; i < names.length; i++) + { + final String name = names[i].trim(); + if (operationLog.isInfoEnabled()) + { + operationLog.info("Create parameters for thread '" + name + "'."); + } + // extract thread specific properties, remove prefix + final ExtendedProperties threadProperties = + ExtendedProperties.getSubset(serviceProperties, getPropertyPrefix(name), true); + threadProperties.putAll(generalProperties); // add all general properties + threadParameters[i] = new ThreadParameters(threadProperties, name); + } + return threadParameters; + } + + private static Properties removeThreadSpecificProperties(final String[] names, + final Properties properties) + { + final ExtendedProperties generalProperties = ExtendedProperties.createWith(properties); + for (final String name : names) + { + generalProperties.removeSubset(getPropertyPrefix(name)); + } + return generalProperties; + } + + private static String getPropertyPrefix(final String name) + { + return name + "."; + } + + private static void validateThreadNames(final String[] names) + { + final Set<String> processed = new HashSet<String>(); + for (final String name : names) + { + if (processed.contains(name)) + { + throw new ConfigurationFailureException("Duplicated thread name: " + name); + } + if (name.length() == 0) + { + throw new ConfigurationFailureException("Thread name:cannot be empty!"); + } + processed.add(name); + } + } + + private final static Properties createMailProperties(final Properties serviceProperties) + { + final Properties properties = + ExtendedProperties.getSubset(serviceProperties, "mail", false); + if (properties.getProperty(JavaMailProperties.MAIL_SMTP_HOST) == null) + { + properties.setProperty(JavaMailProperties.MAIL_SMTP_HOST, "localhost"); + } + if (properties.getProperty(JavaMailProperties.MAIL_FROM) == null) + { + properties.setProperty(JavaMailProperties.MAIL_FROM, "etlserver@localhost"); + } + return properties; + } + + private void outputException(final Exception ex) + { + if (ex instanceof UserFailureException || ex instanceof CmdLineException) + { + System.err.println(ex.getMessage()); + } else + { + System.err.println("An exception occurred."); + ex.printStackTrace(); + } + if (ex instanceof CmdLineException) + { + printHelp(false); + } + } + + /** + * Returns The interval to wait between to checks for activity (in milliseconds). + */ + public long getCheckIntervalMillis() + { + return checkIntervalSeconds * DateUtils.MILLIS_PER_SECOND; + } + + /** + * Returns the time-out for clean up work in the shutdown sequence (in seconds). + * <p> + * Note that that the maximal time for the shutdown sequence to complete can be as large as + * twice this time. + */ + public long getShutdownTimeOutMillis() + { + return shutdownTimeOutSeconds * DateUtils.MILLIS_PER_SECOND; + } + + /** + * Returns the password to access the LIMS server with. + */ + public String getPassword() + { + return password; + } + + /** + * Returns the username to access the LIMS server with. + */ + public String getUsername() + { + return username; + } + + /** + * Returns all properties. + */ + public final Properties getProperties() + { + return serviceProperties; + } + + /** + * Returns a map of all processor properties with the procedure type code as the key. + */ + public final Map<String, Properties> getProcessorProperties() + { + return processorProperties; + } + + /** Returns <code>mailProperties</code>. */ + public final Properties getMailProperties() + { + return mailProperties; + } + + /** + * Returns the timing parameters for monitored operations. + */ + public TimingParameters getTimingParameters() + { + return timingParameters; + } + + /** + * Logs the current parameters to the {@link LogCategory#OPERATION} log. + */ + public void log() + { + if (operationLog.isInfoEnabled()) + { + for (final ThreadParameters threadParameters : threads) + { + threadParameters.log(); + } + operationLog.info(String.format("Check intervall: %d s.", + getCheckIntervalMillis() / 1000)); + } + } + + /** + * Returns the URL of the LIMS server. + */ + public String getServerURL() + { + return serverURL; + } + + public ThreadParameters[] getThreads() + { + return threads; + } + +} diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/PlateDimension.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/PlateDimension.java new file mode 100644 index 00000000000..6321c839dd4 --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/PlateDimension.java @@ -0,0 +1,73 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import java.io.Serializable; + +import ch.systemsx.cisd.common.utilities.AbstractHashable; +import ch.systemsx.cisd.openbis.generic.shared.IWebService; + +/** + * @author Tomasz Pylak + */ +public class PlateDimension extends AbstractHashable implements Serializable +{ + private static final long serialVersionUID = IWebService.VERSION; + + private int rowsNum; + + private int colsNum; + + // for internal use only + public PlateDimension() + { + this(0, 0); + } + + public PlateDimension(int rowsNum, int colsNum) + { + super(); + this.rowsNum = rowsNum; + this.colsNum = colsNum; + } + + public int getRowsNum() + { + return rowsNum; + } + + public void setRowsNum(int rowsNum) + { + this.rowsNum = rowsNum; + } + + public int getColsNum() + { + return colsNum; + } + + public void setColsNum(int colsNum) + { + this.colsNum = colsNum; + } + + @Override + public String toString() + { + return "(" + rowsNum + ", " + colsNum + ")"; + } +} diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/PlateDimensionParser.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/PlateDimensionParser.java new file mode 100644 index 00000000000..1e237a59cf6 --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/PlateDimensionParser.java @@ -0,0 +1,120 @@ +/* + * Copyright 2008 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import ch.systemsx.cisd.openbis.generic.shared.dto.EntityPropertyPE; +import ch.systemsx.cisd.openbis.generic.shared.dto.PropertyTypePE; + +/** + * Extractor and parser of the plate geometry from an array of properties. + * + * @author Tomasz Pylak + */ +public class PlateDimensionParser +{ + public static final String PLATE_GEOMETRY_PROPERTY_NAME = "PLATE_GEOMETRY"; + + /** + * Returns the plate geometry from the specified properties. + * + * @throws IllegalArgumentException if either their isn't such a property or it has an invalid + * value. + */ + public static PlateDimension getPlateDimension(final EntityPropertyPE[] properties) + { + final PlateDimension plateDimension = tryToGetPlateDimension(properties); + if (plateDimension == null) + { + throw new IllegalArgumentException("Cannot find property " + + PLATE_GEOMETRY_PROPERTY_NAME); + } + return plateDimension; + } + + /** + * Tries to get the plate geometry from the specified properties. + * + * @return <code>null</code> if their isn't such a property. + * @throws IllegalArgumentException if the property for the plate geometry has an invalid value. + */ + public static PlateDimension tryToGetPlateDimension(final EntityPropertyPE[] properties) + { + assert properties != null : "Unspecified properties"; + final String plateGeometryString = + tryFindProperty(properties, PLATE_GEOMETRY_PROPERTY_NAME); + if (plateGeometryString == null) + { + return null; + } + final PlateDimension dimension = tryParsePlateDimension(plateGeometryString); + if (dimension == null) + { + throw new IllegalArgumentException("Cannot parse plate geometry " + plateGeometryString); + } + return dimension; + + } + + // parses plate geometry - takes the token after the last "_" sign and assumes that the number + // of rows is separated + // from number of columns by the 'X' sign, e.g. XXX_YYY_16x24 + private static PlateDimension tryParsePlateDimension(final String plateGeometryString) + { + final String[] tokens = plateGeometryString.split("_"); + final String sizeToken = tokens[tokens.length - 1]; + final String[] dims = sizeToken.split("X"); + if (dims.length != 2) + { + return null; + } + final Integer rows = tryParseInteger(dims[0]); + final Integer cols = tryParseInteger(dims[1]); + if (rows == null || cols == null) + { + return null; + } + return new PlateDimension(rows, cols); + } + + private static Integer tryParseInteger(final String value) + { + try + { + return Integer.parseInt(value); + } catch (final NumberFormatException e) + { + return null; + } + } + + private static String tryFindProperty(final EntityPropertyPE[] properties, + final String propertyCode) + { + for (final EntityPropertyPE property : properties) + { + final PropertyTypePE propertyType = + property.getEntityTypePropertyType().getPropertyType(); + if (propertyType.getCode().equals(propertyCode)) + { + return property.tryGetUntypedValue(); + } + } + return null; + } + + +} diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/PropertiesBasedETLServerPlugin.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/PropertiesBasedETLServerPlugin.java new file mode 100644 index 00000000000..6feb4ffde9d --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/PropertiesBasedETLServerPlugin.java @@ -0,0 +1,123 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import static ch.systemsx.cisd.etlserver.IDataSetInfoExtractor.EXTRACTOR_KEY; +import static ch.systemsx.cisd.etlserver.IProcedureAndDataTypeExtractor.TYPE_EXTRACTOR_KEY; +import static ch.systemsx.cisd.etlserver.IStorageProcessor.STORAGE_PROCESSOR_KEY; + +import java.util.Properties; + +import ch.systemsx.cisd.common.exceptions.ConfigurationFailureException; +import ch.systemsx.cisd.common.utilities.ClassUtils; +import ch.systemsx.cisd.common.utilities.ExtendedProperties; + +/** + * An implementation of {@link IETLServerPlugin} which is based on a <code>Properties</code> + * object. The objects delivered by this implementation are created only once. For creation the + * properties are used. For each object a specific property has to be defined which specifies the + * fully-qualified class name of the object. The class has to implement a specific interface and it + * should have a constructor with a single argument of type <code>Properties</code>. The argmunt + * is derived from the original properties by extracting all properties where the key starts with + * the prefix <code><i><class name key></i> + '.'</code>. The prefix is removed from the + * key for the derived properties. The following table shows all class name keys and interfaces: + * <table cellspacing="0" cellpadding="5" border="1"> + * <tr> + * <th>Class name key</th> + * <th>Interface</th> + * </tr> + * <tr> + * <td><code>code-extractor</code></td> + * <td>{@link IDataSetInfoExtractor}</td> + * </tr> + * <tr> + * <td><code>type-extractor</code></td> + * <td>{@link IProcedureAndDataTypeExtractor}</td> + * </tr> + * </table> Example of a properties file: + * + * <pre><tt> + * data-set-info-extractor = ch.systemsx.cisd.etlserver.DefaultDataSetInfoExtractor + * data-set-info-extractor.entity-separator = == + * + * type-extractor = ch.systemsx.cisd.etlserver.SimpleTypeExtractor + * type-extractor.file-format-type = TIFF + * type-extractor.locator-type = RELATIVE_LOCATION + * type-extractor.data-set-type = HCS_IMAGE + * type-extractor.procedure-type = DATA_ACQUISITION + * </tt></pre> + * + * @author Franz-Josef Elmer + */ +public class PropertiesBasedETLServerPlugin extends ETLServerPlugin +{ + + private static final Properties EMPTY_PROPERTIES = new Properties(); + + private final static <T> T create(final Class<T> superClazz, final Properties properties, + final String keyPrefix, final boolean withSubset) + { + final String className = properties.getProperty(keyPrefix); + if (className == null) + { + throw new ConfigurationFailureException("Missing property '" + keyPrefix + "'."); + } + try + { + return ClassUtils.create(superClazz, className, withSubset ? createSubsetProperties( + properties, keyPrefix) : properties); + } catch (IllegalArgumentException ex) + { + throw new ConfigurationFailureException(ex.getMessage()); + } + } + + public PropertiesBasedETLServerPlugin(final Properties properties) + { + super(createDataSetInfoExtractor(properties), + createProcedureAndDataTypeExtractor(properties), createStorageProcessor(properties)); + } + + private final static Properties createSubsetProperties(final Properties properties, + final String prefix) + { + if (prefix == null) + { + return properties; + } + return ExtendedProperties.getSubset(properties == null ? EMPTY_PROPERTIES : properties, + prefix + '.', true); + } + + private final static IStorageProcessor createStorageProcessor(final Properties properties) + { + return create(IStorageProcessor.class, properties, STORAGE_PROCESSOR_KEY, true); + } + + private final static IProcedureAndDataTypeExtractor createProcedureAndDataTypeExtractor( + final Properties properties) + { + return create(IProcedureAndDataTypeExtractor.class, properties, TYPE_EXTRACTOR_KEY, true); + } + + private final static IDataSetInfoExtractor createDataSetInfoExtractor( + final Properties properties) + { + return create(IDataSetInfoExtractor.class, properties, EXTRACTOR_KEY, false); + } + +} diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/SimpleTypeExtractor.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/SimpleTypeExtractor.java new file mode 100644 index 00000000000..ad4867f7896 --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/SimpleTypeExtractor.java @@ -0,0 +1,93 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import java.io.File; +import java.util.Properties; + +import ch.systemsx.cisd.openbis.generic.shared.dto.DataSetType; +import ch.systemsx.cisd.openbis.generic.shared.dto.FileFormatType; +import ch.systemsx.cisd.openbis.generic.shared.dto.LocatorType; +import ch.systemsx.cisd.openbis.generic.shared.dto.ProcedureType; +import ch.systemsx.cisd.openbis.generic.shared.dto.types.DataSetTypeCode; +import ch.systemsx.cisd.openbis.generic.shared.dto.types.ProcedureTypeCode; + +/** + * Implementation of {@link IProcedureAndDataTypeExtractor} which gets the types from the properties + * argument of the constructor. + * + * @author Franz-Josef Elmer + */ +public class SimpleTypeExtractor implements IProcedureAndDataTypeExtractor +{ + public static final String FILE_FORMAT_TYPE_KEY = "file-format-type"; + + public static final String LOCATOR_TYPE_KEY = "locator-type"; + + public static final String DATA_SET_TYPE_KEY = "data-set-type"; + + public static final String PROCEDURE_TYPE_KEY = "procedure-type"; + + private FileFormatType fileFormatType; + + private LocatorType locatorType; + + private DataSetType dataSetType; + + private ProcedureType procedureType; + + public SimpleTypeExtractor(final Properties properties) + { + String code = + properties.getProperty(FILE_FORMAT_TYPE_KEY, + FileFormatType.DEFAULT_FILE_FORMAT_TYPE_CODE); + fileFormatType = new FileFormatType(code); + code = properties.getProperty(LOCATOR_TYPE_KEY, LocatorType.DEFAULT_LOCATOR_TYPE_CODE); + locatorType = new LocatorType(code); + code = properties.getProperty(DATA_SET_TYPE_KEY, DataSetTypeCode.HCS_IMAGE.getCode()); + dataSetType = new DataSetType(code); + code = + properties.getProperty(PROCEDURE_TYPE_KEY, ProcedureTypeCode.DATA_ACQUISITION + .getCode()); + procedureType = new ProcedureType(code); + } + + // + // IProcedureAndDataTypeExtractor + // + + public final FileFormatType getFileFormatType(final File incomingDataSetPath) + { + return fileFormatType; + } + + public final LocatorType getLocatorType(final File incomingDataSetPath) + { + return locatorType; + } + + public final DataSetType getDataSetType(final File incomingDataSetPath) + { + return dataSetType; + } + + public final ProcedureType getProcedureType(final File incomingDataSetPath) + { + return procedureType; + } + +} diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/StandardProcessor.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/StandardProcessor.java new file mode 100644 index 00000000000..20e54118205 --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/StandardProcessor.java @@ -0,0 +1,187 @@ +/* + * Copyright 2008 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import java.io.File; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.time.StopWatch; +import org.apache.log4j.Logger; + +import ch.systemsx.cisd.common.exceptions.StopException; +import ch.systemsx.cisd.common.filesystem.PathPrefixPrepender; +import ch.systemsx.cisd.common.logging.LogCategory; +import ch.systemsx.cisd.common.logging.LogFactory; +import ch.systemsx.cisd.openbis.generic.shared.dto.ProcessingInstructionDTO; +import ch.systemsx.cisd.openbis.generic.shared.dto.StorageFormat; + +/** + * Standard implementation of <code>IProcessor</code>. + * + * @author Christian Ribeaud + */ +final class StandardProcessor implements IProcessor +{ + private static final Logger operationLog = + LogFactory.getLogger(LogCategory.OPERATION, StandardProcessor.class); + + private static final Logger notificationLog = + LogFactory.getLogger(LogCategory.NOTIFY, StandardProcessor.class); + + private final IFileFactory fileFactory; + + private final PathPrefixPrepender pathPrefixPrepender; + + private final String parametersFileName; + + private final MessageFormat finishedFileFormat; + + private final StorageFormat inputDataFormat; + + private final String dataSetCodePrefixGlueCharacter; + + public StandardProcessor(final IFileFactory fileFactory, final StorageFormat inputDataFormat, + final PathPrefixPrepender pathPrefixPrepender, final String parametersFileName, + final String finishedFileNameTemplate, final String dataSetCodePrefixGlueCharacter) + { + assert fileFactory != null : "Unspecified IFileFactory."; + assert inputDataFormat != null : "Unspecified StorageFormat."; + assert pathPrefixPrepender != null : "Unspecified PathPrefixPrepender."; + assert parametersFileName != null : "Unspecified parameters file name."; + assert finishedFileNameTemplate != null : "Unspecified finished file name template."; + assert dataSetCodePrefixGlueCharacter != null : "Unspecified data set code prefix glue character."; + this.fileFactory = fileFactory; + this.inputDataFormat = inputDataFormat; + this.pathPrefixPrepender = pathPrefixPrepender; + this.parametersFileName = parametersFileName; + this.finishedFileFormat = new MessageFormat(finishedFileNameTemplate); + this.dataSetCodePrefixGlueCharacter = dataSetCodePrefixGlueCharacter; + } + + private void createDataSetForProcessing(final File dataSet, final IFile dataSetForProcessing) + { + final StopWatch watch = new StopWatch(); + watch.start(); + dataSetForProcessing.copyFrom(dataSet); + if (operationLog.isInfoEnabled()) + { + operationLog.info("Data set '" + dataSet.getName() + "' copied into '" + + dataSetForProcessing.getAbsolutePath() + "', took " + watch + "."); + } + } + + private void createProcessingParameters(final ProcessingInstructionDTO instruction, + final IFile processingDirectory, final List<IFile> itemsToRemoveInCaseOfError) + { + final byte[] instructionDataOrNull = instruction.getParameters(); + if (instructionDataOrNull != null) + { + final IFile parametersFile = + fileFactory.create(processingDirectory, parametersFileName); + itemsToRemoveInCaseOfError.add(parametersFile); + parametersFile.write(instructionDataOrNull); + if (operationLog.isInfoEnabled()) + { + operationLog.info("Processing parameters written into '" + + parametersFile.getAbsolutePath() + "'."); + } + } + } + + private void createFinishedFile(final IFile processingDirectory, final String dataSetName) + { + final String finishedFileName = finishedFileFormat.format(new String[] + { dataSetName }); + final IFile finishedFile = fileFactory.create(processingDirectory, finishedFileName); + finishedFile.write(new byte[0]); + } + + private final String getDataSetName(final DataSetInformation dataSetInformation, + final File dataSet) + { + final String dataSetName = dataSet.getName(); + final String parentDataSetCode = dataSetInformation.getDataSetCode(); + if (StringUtils.isNotEmpty(parentDataSetCode)) + { + return parentDataSetCode + dataSetCodePrefixGlueCharacter + dataSetName; + } + return dataSetName; + } + + // + // IProcessor + // + + public final StorageFormat getRequiredInputDataFormat() + { + return inputDataFormat; + } + + public final void initiateProcessing(final ProcessingInstructionDTO instruction, + final DataSetInformation dataSetInformation, final File dataSet) + { + assert instruction != null : "Unspecified instruction."; + assert dataSet != null : "Unspecified data set."; + assert dataSetInformation != null : "Unspecified data set information."; + if (operationLog.isInfoEnabled()) + { + operationLog.info("Start initialization of processing."); + } + final String processingPath = pathPrefixPrepender.addPrefixTo(instruction.getPath()); + final IFile processingDirectory = fileFactory.create(processingPath); + processingDirectory.check(); + final String dataSetName = getDataSetName(dataSetInformation, dataSet); + final IFile dataSetForProcessing = fileFactory.create(processingDirectory, dataSetName); + final List<IFile> itemsToRemoveInCaseOfError = new ArrayList<IFile>(2); + itemsToRemoveInCaseOfError.add(dataSetForProcessing); + try + { + StopException.check(); + createDataSetForProcessing(dataSet, dataSetForProcessing); + StopException.check(); + createProcessingParameters(instruction, processingDirectory, itemsToRemoveInCaseOfError); + StopException.check(); + createFinishedFile(processingDirectory, dataSetName); + if (operationLog.isInfoEnabled()) + { + operationLog.info("Processing initiated."); + } + } catch (final Exception ex) + { + for (final IFile item : itemsToRemoveInCaseOfError) + { + item.delete(); + } + if (ex instanceof StopException) + { + operationLog + .warn(String + .format( + "Requested to stop initiation of processing, rolled back: [data set: '%s'].", + dataSetForProcessing.getAbsolutePath())); + } else + { + notificationLog.error(String.format( + "Error when initiating processing, rolled back: [data set: '%s'].", + dataSetForProcessing.getAbsolutePath()), ex); + } + } + } +} \ No newline at end of file diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/StandardProcessorFactory.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/StandardProcessorFactory.java new file mode 100644 index 00000000000..a6c38d0a634 --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/StandardProcessorFactory.java @@ -0,0 +1,135 @@ +/* + * Copyright 2008 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import java.util.Properties; + +import ch.systemsx.cisd.common.exceptions.ConfigurationFailureException; +import ch.systemsx.cisd.common.filesystem.PathPrefixPrepender; +import ch.systemsx.cisd.common.utilities.PropertyUtils; +import ch.systemsx.cisd.openbis.generic.shared.dto.ProcessingInstructionDTO; +import ch.systemsx.cisd.openbis.generic.shared.dto.StorageFormat; + +/** + * Factory for standard post processors. A standard post processor does the following: + * <ol> + * <li>Copies the data set file or folder to the processing directory. The path of the processing + * directory is a combination of the processing code (i.e. {@link ProcessingInstructionDTO#getPath()}) + * and the mandatory property <code>root-directory</code>. This copy action is done during the + * preparation step. + * <li>Stores the processing parameters (i.e. {@link ProcessingInstructionDTO#getParameters()}) in a + * file in the processing directory who's name is specified by the mandatory property + * <code>paramaters-file</code>. + * <li>Creates an empty file in the processing directory who's name is specified by the mandatory + * property <code>finished-file</code>. + * </ol> + * + * @author Franz-Josef Elmer + */ +public class StandardProcessorFactory implements IProcessorFactory +{ + private static final String HARD_LINK_INSTEAD_OF_COPY_KEY = "hard-link-instead-of-copy"; + + private static final String PATH_PREFIX_ABSOLUTE_KEY = "prefix-for-absolute-paths"; + + private static final String PATH_PREFIX_RELATIVE_KEY = "prefix-for-relative-paths"; + + private static final String PARAMETERS_FILE_KEY = "parameters-file"; + + private static final String INPUT_STORAGE_FORMAT_KEY = "input-storage-format"; + + private static final String DATA_SET_CODE_PREFIX_GLUE_KEY = "data-set-code-prefix-glue"; + + private static final String FINISHED_FILE_TEMPLATE_KEY = "finished-file-template"; + + private final String parametersFileName; + + private final String finishedFileNameTemplate; + + private final IFileFactory fileFactory; + + private final StorageFormat inputStorageFormat; + + final PathPrefixPrepender pathPrefixPrepender; + + private final String dataSetCodePrefixGlueCharacter; + + /** + * Creates a new instances for the specified properties. Uses the default timing parameters. + * + * @throws ConfigurationFailureException if one of the mandatory properties is missing or the + * property <code>root-directory</code> is not the path of an existing directory. + */ + public static StandardProcessorFactory create(final Properties properties) + throws ConfigurationFailureException + { + final FileBasedFileFactory fileBasedFileFactory = createFileFactory(properties); + return new StandardProcessorFactory(properties, fileBasedFileFactory); + } + + private static FileBasedFileFactory createFileFactory(final Properties properties) + throws ConfigurationFailureException + { + final boolean useHardLinks = + PropertyUtils.getBoolean(properties, HARD_LINK_INSTEAD_OF_COPY_KEY, true); + final FileBasedFileFactory fileBasedFileFactory = + new FileBasedFileFactory(useHardLinks); + return fileBasedFileFactory; + } + + private StandardProcessorFactory(final Properties properties, final IFileFactory fileFactory) + throws ConfigurationFailureException + { + this.fileFactory = fileFactory; + assert properties != null : "Undefined properties."; + final String prefixForAbsolutePathsOrNull = + properties.getProperty(PATH_PREFIX_ABSOLUTE_KEY); + final String prefixForRelativePathsOrNull = + properties.getProperty(PATH_PREFIX_RELATIVE_KEY); + final String inputDataFormatCode = + PropertyUtils.getMandatoryProperty(properties, INPUT_STORAGE_FORMAT_KEY); + inputStorageFormat = StorageFormat.tryGetFromCode(inputDataFormatCode); + if (inputStorageFormat == null) + { + throw ConfigurationFailureException.fromTemplate(INPUT_STORAGE_FORMAT_KEY + + " property has illegal value '%s'.", inputDataFormatCode); + } + pathPrefixPrepender = + new PathPrefixPrepender(prefixForAbsolutePathsOrNull, prefixForRelativePathsOrNull); + parametersFileName = PropertyUtils.getMandatoryProperty(properties, PARAMETERS_FILE_KEY); + finishedFileNameTemplate = + PropertyUtils.getMandatoryProperty(properties, FINISHED_FILE_TEMPLATE_KEY); + dataSetCodePrefixGlueCharacter = + PropertyUtils.getMandatoryProperty(properties, DATA_SET_CODE_PREFIX_GLUE_KEY); + } + + // + // IProcessorFactory + // + + public final PathPrefixPrepender getPathPrefixPrepender() + { + return pathPrefixPrepender; + } + + public final IProcessor createProcessor() + { + return new StandardProcessor(fileFactory, inputStorageFormat, pathPrefixPrepender, + parametersFileName, finishedFileNameTemplate, dataSetCodePrefixGlueCharacter); + } + +} diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/ThreadParameters.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/ThreadParameters.java new file mode 100644 index 00000000000..6caa7f5af41 --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/ThreadParameters.java @@ -0,0 +1,146 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import java.io.File; +import java.util.Properties; + +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; + +import ch.rinn.restrictions.Private; +import ch.systemsx.cisd.common.exceptions.ConfigurationFailureException; +import ch.systemsx.cisd.common.filesystem.FileUtilities; +import ch.systemsx.cisd.common.logging.LogCategory; +import ch.systemsx.cisd.common.logging.LogFactory; +import ch.systemsx.cisd.common.utilities.PropertyUtils; + +/** + * <i>ETL</i> thread specific parameters. + * + * @author Tomasz Pylak + */ +public final class ThreadParameters +{ + @Private + static final String GROUP_CODE_KEY = "group-code"; + + private static final Logger operationLog = + LogFactory.getLogger(LogCategory.OPERATION, ThreadParameters.class); + + private static final String INCOMING_DIR = "incoming-dir"; + + /** + * The (local) directory to monitor for new files and directories to move to the remote side. + * The directory where data to be processed by the ETL server become available. + */ + private final File incomingDataDirectory; + + private final IETLServerPlugin plugin; + + private final String threadName; + + private final String groupCode; + + /** + * @param threadProperties parameters for one processing thread together with general + * parameters. + */ + public ThreadParameters(final Properties threadProperties, final String threadName) + { + this.incomingDataDirectory = extractIncomingDataDir(threadProperties); + this.plugin = new PropertiesBasedETLServerPlugin(threadProperties); + groupCode = tryGetGroupCode(threadProperties); + this.threadName = threadName; + } + + final void check() + { + if (incomingDataDirectory.isDirectory() == false) + { + throw new ConfigurationFailureException("Incoming directory '" + incomingDataDirectory + + "' is not a directory."); + } + } + + @Private + static File extractIncomingDataDir(final Properties threadProperties) + { + final String incomingDir = threadProperties.getProperty(INCOMING_DIR); + if (StringUtils.isNotBlank(incomingDir)) + { + return FileUtilities.normalizeFile(new File(incomingDir)); + } else + { + throw new ConfigurationFailureException("No '" + INCOMING_DIR + "' defined."); + } + } + + @Private + static final String tryGetGroupCode(final Properties properties) + { + return StringUtils.defaultIfEmpty(PropertyUtils.getProperty(properties, GROUP_CODE_KEY), + null); + } + + /** + * Returns the <code>group-code</code> property specified for this thread. + */ + final String tryGetGroupCode() + { + return groupCode; + } + + /** + * Returns The directory to monitor for incoming data. + */ + final File getIncomingDataDirectory() + { + return incomingDataDirectory; + } + + final IETLServerPlugin getPlugin() + { + return plugin; + } + + /** + * Logs the current parameters to the {@link LogCategory#OPERATION} log. + */ + final void log() + { + if (operationLog.isInfoEnabled()) + { + operationLog.info(String.format("[%s] Code extractor: '%s'", threadName, plugin + .getDataSetInfoExtractor().getClass().getName())); + operationLog.info(String.format("[%s] Type extractor: '%s'", threadName, plugin + .getTypeExtractor().getClass().getName())); + operationLog.info(String.format("[%s] Incoming data directory: '%s'.", threadName, + getIncomingDataDirectory().getAbsolutePath())); + if (groupCode != null) + { + operationLog.info(String.format("[%s] Group code: '%s'.", threadName, groupCode)); + } + } + } + + public String getThreadName() + { + return threadName; + } + +} diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/TransferredDataSetHandler.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/TransferredDataSetHandler.java new file mode 100644 index 00000000000..29057ff6381 --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/TransferredDataSetHandler.java @@ -0,0 +1,743 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import static ch.systemsx.cisd.common.Constants.IS_FINISHED_PREFIX; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Collections; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.time.StopWatch; +import org.apache.log4j.Logger; + +import ch.rinn.restrictions.Private; +import ch.systemsx.cisd.common.Constants; +import ch.systemsx.cisd.common.exceptions.ConfigurationFailureException; +import ch.systemsx.cisd.common.exceptions.EnvironmentFailureException; +import ch.systemsx.cisd.common.exceptions.HighLevelException; +import ch.systemsx.cisd.common.exceptions.StopException; +import ch.systemsx.cisd.common.exceptions.WrappedIOException; +import ch.systemsx.cisd.common.filesystem.FileOperations; +import ch.systemsx.cisd.common.filesystem.FileUtilities; +import ch.systemsx.cisd.common.filesystem.IFileOperations; +import ch.systemsx.cisd.common.filesystem.IPathHandler; +import ch.systemsx.cisd.common.highwatermark.HighwaterMarkWatcher; +import ch.systemsx.cisd.common.logging.LogCategory; +import ch.systemsx.cisd.common.logging.LogFactory; +import ch.systemsx.cisd.common.mail.IMailClient; +import ch.systemsx.cisd.common.mail.MailClient; +import ch.systemsx.cisd.common.types.BooleanOrUnknown; +import ch.systemsx.cisd.common.utilities.BeanUtils; +import ch.systemsx.cisd.common.utilities.ISelfTestable; +import ch.systemsx.cisd.common.utilities.OSUtilities; +import ch.systemsx.cisd.openbis.generic.shared.dto.DataSetType; +import ch.systemsx.cisd.openbis.generic.shared.dto.DatabaseInstancePE; +import ch.systemsx.cisd.openbis.generic.shared.dto.ExperimentPE; +import ch.systemsx.cisd.openbis.generic.shared.dto.ExternalData; +import ch.systemsx.cisd.openbis.generic.shared.dto.ExtractableData; +import ch.systemsx.cisd.openbis.generic.shared.dto.ProcessingInstructionDTO; +import ch.systemsx.cisd.openbis.generic.shared.dto.StorageFormat; + +/** + * The class that handles the incoming data set. + * + * @author Bernd Rinn + */ +public final class TransferredDataSetHandler implements IPathHandler, ISelfTestable +{ + + private static final String TARGET_NOT_RELATIVE_TO_STORE_ROOT = + "Target path '%s' is not relative to store root directory '%s'."; + + @Private + static final String DATA_SET_STORAGE_FAILURE_TEMPLATE = "Storing data set '%s' failed."; + + @Private + static final String DATA_SET_REGISTRATION_FAILURE_TEMPLATE = + "Registration of data set '%s' failed."; + + @Private + static final String SUCCESSFULLY_REGISTERED_TEMPLATE = + "Successfully registered data set '%s' for sample '%s', data set type '%s', " + + "experiment '%s' with openBIS service."; + + @Private + static final String EMAIL_SUBJECT_TEMPLATE = "Success: data set for experiment '%s"; + + private static final Logger notificationLog = + LogFactory.getLogger(LogCategory.NOTIFY, TransferredDataSetHandler.class); + + private static final Logger operationLog = + LogFactory.getLogger(LogCategory.OPERATION, TransferredDataSetHandler.class); + + private static final NamedDataStrategy ERROR_DATA_STRATEGY = + new NamedDataStrategy(DataStoreStrategyKey.ERROR); + + private final IStoreRootDirectoryHolder storeRootDirectoryHolder; + + private final IEncapsulatedLimsService limsService; + + private final IDataStrategyStore dataStrategyStore; + + private final IDataSetInfoExtractor dataSetInfoExtractor; + + private final IFileOperations fileOperations; + + private final Lock registrationLock; + + private final IProcedureAndDataTypeExtractor typeExtractor; + + private final IStorageProcessor storageProcessor; + + private final IMailClient mailClient; + + private final String groupCode; + + private final boolean notifySuccessfulRegistration; + + private boolean stopped = false; + + private Map<String, IProcessorFactory> processorFactories = + Collections.<String, IProcessorFactory> emptyMap(); + + private DatabaseInstancePE homeDatabaseInstance; + + public TransferredDataSetHandler(final String groupCode, final IETLServerPlugin plugin, + final IEncapsulatedLimsService limsService, final Properties mailProperties, + final HighwaterMarkWatcher highwaterMarkWatcher, + final boolean notifySuccessfulRegistration) + + { + this(groupCode, plugin.getStorageProcessor(), plugin, limsService, new MailClient( + mailProperties), notifySuccessfulRegistration); + } + + TransferredDataSetHandler(final String groupCode, + final IStoreRootDirectoryHolder storeRootDirectoryHolder, + final IETLServerPlugin plugin, final IEncapsulatedLimsService limsService, + final IMailClient mailClient, final boolean notifySuccessfulRegistration) + + { + assert storeRootDirectoryHolder != null : "Given store root directory holder can not be null."; + assert plugin != null : "IETLServerPlugin implementation can not be null."; + assert limsService != null : "IEncapsulatedLimsService implementation can not be null."; + assert mailClient != null : "IMailClient implementation can not be null."; + + this.groupCode = groupCode; + this.storeRootDirectoryHolder = storeRootDirectoryHolder; + this.dataSetInfoExtractor = plugin.getDataSetInfoExtractor(); + this.typeExtractor = plugin.getTypeExtractor(); + this.storageProcessor = plugin.getStorageProcessor(); + this.limsService = limsService; + this.mailClient = mailClient; + this.dataStrategyStore = new DataStrategyStore(this.limsService, mailClient); + this.notifySuccessfulRegistration = notifySuccessfulRegistration; + this.registrationLock = new ReentrantLock(); + this.fileOperations = FileOperations.getMonitoredInstanceForCurrentThread(); + } + + public final void setProcessorFactories(final Map<String, IProcessorFactory> processorFactories) + { + assert processorFactories != null : "Unspecified processor factory map."; + this.processorFactories = processorFactories; + } + + /** + * Returns the lock one needs to hold before one interrupts a data set registration. + */ + public Lock getRegistrationLock() + { + return registrationLock; + } + + // + // IPathHandler + // + + public final void handle(final File isFinishedFile) + { + if (stopped) + { + return; + } + final RegistrationHelper registrationHelper = new RegistrationHelper(isFinishedFile); + registrationHelper.prepare(); + if (registrationHelper.hasDataSetBeenIdentified()) + { + registrationHelper.registerDataSet(); + } else + { + registrationHelper.moveDataSet(); + } + } + + public boolean isStopped() + { + return stopped; + } + + // + // ISelfTestable + // + + public final void check() throws ConfigurationFailureException, EnvironmentFailureException + { + final File storeRootDirectory = storeRootDirectoryHolder.getStoreRootDirectory(); + storeRootDirectory.mkdirs(); + if (operationLog.isDebugEnabled()) + { + operationLog.debug("Checking store root directory '" + + storeRootDirectory.getAbsolutePath() + "'."); + } + final String errorMessage = + fileOperations.checkDirectoryFullyAccessible(storeRootDirectory, "store root"); + if (errorMessage != null) + { + if (fileOperations.exists(storeRootDirectory) == false) + { + throw EnvironmentFailureException.fromTemplate( + "Store root directory '%s' does not exist.", storeRootDirectory + .getAbsolutePath()); + } else + { + throw new ConfigurationFailureException(errorMessage); + } + } + } + + public boolean isRemote() + { + return true; + } + + private DatabaseInstancePE getHomeDatabaseInstance() + { + if (homeDatabaseInstance == null) + { + homeDatabaseInstance = limsService.getHomeDatabaseInstance(); + } + return homeDatabaseInstance; + } + + // + // Helper class + // + + private final class RegistrationHelper + { + private final File isFinishedFile; + + private final File incomingDataSetFile; + + private final DataSetInformation dataSetInformation; + + private final IDataStoreStrategy dataStoreStrategy; + + private final DataSetType dataSetType; + + private final File storeRoot; + + private BaseDirectoryHolder baseDirectoryHolder; + + private String errorMessageTemplate; + + RegistrationHelper(final File isFinishedFile) + { + assert isFinishedFile != null : "Unspecified is-finished file."; + final String name = isFinishedFile.getName(); + assert name.startsWith(IS_FINISHED_PREFIX) : "A finished file must starts with '" + + IS_FINISHED_PREFIX + "'."; + errorMessageTemplate = DATA_SET_STORAGE_FAILURE_TEMPLATE; + this.isFinishedFile = isFinishedFile; + incomingDataSetFile = getIncomingDataSetPath(isFinishedFile); + dataSetInformation = extractDataSetInformation(incomingDataSetFile); + if (dataSetInformation.getDataSetCode() == null) + { + // Extractor didn't extract an externally generated data set code, so request one + // from the openBIS server. + dataSetInformation.setDataSetCode(limsService.createDataSetCode()); + } + dataStoreStrategy = + dataStrategyStore.getDataStoreStrategy(dataSetInformation, incomingDataSetFile); + dataSetType = typeExtractor.getDataSetType(incomingDataSetFile); + storeRoot = storageProcessor.getStoreRootDirectory(); + } + + final void prepare() + { + final File baseDirectory = + createBaseDirectory(dataStoreStrategy, storeRoot, dataSetInformation); + baseDirectoryHolder = + new BaseDirectoryHolder(dataStoreStrategy, baseDirectory, incomingDataSetFile); + } + + final boolean hasDataSetBeenIdentified() + { + return dataStoreStrategy.getKey() == DataStoreStrategyKey.IDENTIFIED; + } + + /** + * This method is only ever called for identified data sets. + */ + final void registerDataSet() + { + final ExperimentPE experiment = dataSetInformation.getExperiment(); + final String procedureTypeCode = + typeExtractor.getProcedureType(incomingDataSetFile).getCode(); + final IProcessor processorOrNull = tryCreateProcessor(procedureTypeCode); + try + { + registerDataSetAndInitiateProcessing(experiment, procedureTypeCode, processorOrNull); + logAndNotifySuccessfulRegistration(experiment.getRegistrator().getEmail()); + if (fileOperations.exists(incomingDataSetFile) + && fileOperations.removeRecursivelyQueueing(incomingDataSetFile) == false) + { + operationLog.error("Cannot delete '" + incomingDataSetFile.getAbsolutePath() + + "'."); + } + deleteAndLogIsFinishedFile(); + } catch (final Throwable throwable) + { + rollback(throwable); + } + } + + private void rollback(final Throwable throwable) throws Error + { + stopped |= throwable instanceof StopException; + if (stopped) + { + Thread.interrupted(); // Ensure the thread's interrupted state is cleared. + operationLog.warn(String.format("Requested to stop registration of data set '%s'", + dataSetInformation)); + } else + { + notificationLog.error(String.format(errorMessageTemplate, dataSetInformation), + throwable); + } + // Errors which are not AssertionErrors leave the system in a state that we don't + // know and can't trust. Thus we will not perform any operations any more in this + // case. + if (throwable instanceof Error && throwable instanceof AssertionError == false) + { + throw (Error) throwable; + } + storageProcessor.unstoreData(incomingDataSetFile, baseDirectoryHolder + .getBaseDirectory()); + if (stopped == false) + { + final File baseDirectory = + createBaseDirectory(ERROR_DATA_STRATEGY, storeRoot, dataSetInformation); + baseDirectoryHolder = + new BaseDirectoryHolder(ERROR_DATA_STRATEGY, baseDirectory, + incomingDataSetFile); + boolean moveInCaseOfErrorOk = + FileRenamer.renameAndLog(incomingDataSetFile, baseDirectoryHolder + .getTargetFile()); + writeThrowable(throwable); + if (moveInCaseOfErrorOk) + { + deleteAndLogIsFinishedFile(); + } + } + } + + /** + * Registers the data set and, if possible, initiates the processing. + */ + private void registerDataSetAndInitiateProcessing(final ExperimentPE experiment, + final String procedureTypeCode, final IProcessor processorOrNull) + { + final File markerFile = createProcessingMarkerFile(); + try + { + if (operationLog.isInfoEnabled()) + { + operationLog.info("Start storing data set for sample '" + + dataSetInformation.getSampleIdentifier() + "'."); + } + final StopWatch watch = new StopWatch(); + watch.start(); + File dataFile = + storageProcessor.storeData(experiment, dataSetInformation, typeExtractor, + mailClient, incomingDataSetFile, baseDirectoryHolder + .getBaseDirectory()); + if (operationLog.isInfoEnabled()) + { + operationLog.info("Finished storing data set for sample '" + + dataSetInformation.getSampleIdentifier() + "', took " + watch); + } + assert dataFile != null : "The folder that contains the stored data should not be null."; + final String relativePath = FileUtilities.getRelativeFile(storeRoot, dataFile); + assert relativePath != null : String.format(TARGET_NOT_RELATIVE_TO_STORE_ROOT, + dataFile.getAbsolutePath(), storeRoot.getAbsolutePath()); + final StorageFormat availableFormat = storageProcessor.getStorageFormat(); + final BooleanOrUnknown isCompleteFlag = dataSetInformation.getIsCompleteFlag(); + // Ensure that we either register the data set and initiate the processing copy or + // do none of both. + getRegistrationLock().lock(); + try + { + errorMessageTemplate = DATA_SET_REGISTRATION_FAILURE_TEMPLATE; + plainRegisterDataSet(relativePath, procedureTypeCode, availableFormat, + isCompleteFlag); + deleteAndLogIsFinishedFile(); + deleteAndLogIsFinishedFile(); + if (processorOrNull == null) + { + return; + } + final StorageFormat requiredFormat = + processorOrNull.getRequiredInputDataFormat(); + boolean canInitiateProcessing = requiredFormat.equals(availableFormat); + if (canInitiateProcessing == false + && availableFormatMayContainRequiredFormat(availableFormat, + requiredFormat)) + { + // Special case: Check whether we can actually get back the original data. + dataFile = storageProcessor.tryGetProprietaryData(dataFile); + if (dataFile != null) + { + canInitiateProcessing = true; + } + } + if (canInitiateProcessing == false) + { + operationLog.error(String.format( + "Configuration Error: mismatch in data set format for data set '%s' between storage " + + "processor and processor (storage processor:" + + " %s, processor: %s) -> No processing initiated.", + dataSetInformation, availableFormat, requiredFormat)); + notificationLog.error(String.format( + "Configuration Error: no processing initiated for data set '%s'", + dataSetInformation)); + return; + } + final ProcessingInstructionDTO processingInstructionOrNull = + tryToGetAppropriateProcessingInstruction(experiment + .getProcessingInstructions(), procedureTypeCode); + if (processingInstructionOrNull != null) + { + try + { + processorOrNull.initiateProcessing(processingInstructionOrNull, + dataSetInformation, dataFile); + } catch (final RuntimeException e) + { + operationLog.error( + "Exception thrown when initiate processing for data set '" + + dataSetInformation + "'.", e); + notificationLog + .error("Couldn't initiate processing a data set for sample '" + + dataSetInformation.getSampleIdentifier() + + "' for some reason. For more details see log of the ETL Server."); + } + } + } finally + { + getRegistrationLock().unlock(); + } + } finally + { + fileOperations.delete(markerFile); + } + } + + private final File createProcessingMarkerFile() + { + final File baseDirectory = baseDirectoryHolder.getBaseDirectory(); + final File baseParentDirectory = baseDirectory.getParentFile(); + final String processingDirName = baseDirectory.getName(); + final File markerFile = + new File(baseParentDirectory, Constants.PROCESSING_PREFIX + processingDirName); + try + { + fileOperations.createNewFile(markerFile); + } catch (final WrappedIOException ex) + { + throw EnvironmentFailureException.fromTemplate(ex, + "Cannot create marker file '%s'.", markerFile.getPath()); + } + return markerFile; + } + + private boolean availableFormatMayContainRequiredFormat( + final StorageFormat availableFormat, final StorageFormat requiredFormat) + { + return StorageFormat.PROPRIETARY.equals(requiredFormat) + && StorageFormat.BDS_DIRECTORY.equals(availableFormat); + } + + /** + * This method is only ever called for unidentified or invalid data sets. + */ + final void moveDataSet() + { + final boolean ok = + FileRenamer.renameAndLog(incomingDataSetFile, baseDirectoryHolder + .getTargetFile()); + if (ok) + { + deleteAndLogIsFinishedFile(); + } + } + + private final void plainRegisterDataSet(final String relativePath, + final String procedureTypeCode, final StorageFormat storageFormat, + final BooleanOrUnknown isCompleteFlag) + { + final ExternalData data = + createExternalData(relativePath, storageFormat, isCompleteFlag); + // Finally: register the data set in the database. + limsService.registerDataSet(dataSetInformation, procedureTypeCode, data); + } + + private void logAndNotifySuccessfulRegistration(final String email) + { + String msg = null; + if (operationLog.isInfoEnabled()) + { + msg = getSuccessRegistrationMessage(); + operationLog.info(msg); + } + if (notifySuccessfulRegistration) + { + if (msg == null) + { + msg = getSuccessRegistrationMessage(); + } + if (notificationLog.isInfoEnabled()) + { + notificationLog.info(msg); + } + if (StringUtils.isBlank(email) == false) + { + mailClient.sendMessage(String.format(EMAIL_SUBJECT_TEMPLATE, dataSetInformation + .getExperimentIdentifier().getExperimentCode()), msg, null, email); + } + } + } + + private final String getSuccessRegistrationMessage() + { + final StringBuilder buffer = new StringBuilder(); + buffer.append(String.format(SUCCESSFULLY_REGISTERED_TEMPLATE, dataSetInformation + .getDataSetCode(), dataSetInformation.getSampleIdentifier(), dataSetType + .getCode(), dataSetInformation.getExperimentIdentifier())); + buffer.append(OSUtilities.LINE_SEPARATOR); + buffer.append(OSUtilities.LINE_SEPARATOR); + appendNameAndObject(buffer, "Experiment Identifier", dataSetInformation + .getExperimentIdentifier()); + appendNameAndObject(buffer, "Producer Code", dataSetInformation.getProducerCode()); + appendNameAndObject(buffer, "Production Date", dataSetInformation.getProductionDate()); + appendNameAndObject(buffer, "Parent Data Set", StringUtils + .trimToNull(dataSetInformation.getParentDataSetCode())); + appendNameAndObject(buffer, "Is complete", dataSetInformation.getIsCompleteFlag()); + return buffer.toString(); + } + + private final void appendNameAndObject(final StringBuilder buffer, final String name, + final Object object) + { + if (object != null) + { + buffer.append(name).append(":\t").append(object); + buffer.append(OSUtilities.LINE_SEPARATOR); + } + } + + /** + * From given <var>isFinishedPath</var> gets the incoming data set path and checks it. + * + * @return <code>null</code> if a problem has happened. Otherwise a useful and usable + * incoming data set path is returned. + */ + private final File getIncomingDataSetPath(final File isFinishedPath) + { + final File incomingDataSetPath = + FileUtilities.removePrefixFromFileName(isFinishedPath, IS_FINISHED_PREFIX); + if (operationLog.isDebugEnabled()) + { + operationLog.debug(String.format( + "Getting incoming data set path '%s' from is-finished path '%s'", + incomingDataSetPath, isFinishedPath)); + } + final String errorMsg = + fileOperations.checkPathFullyAccessible(incomingDataSetPath, + "incoming data set"); + if (errorMsg != null) + { + fileOperations.delete(isFinishedPath); + throw EnvironmentFailureException.fromTemplate(String.format( + "Error moving path '%s' from '%s' to '%s': %s", incomingDataSetPath + .getName(), incomingDataSetPath.getParent(), + storeRootDirectoryHolder.getStoreRootDirectory(), errorMsg)); + } + return incomingDataSetPath; + } + + /** + * From given <var>incomingDataSetPath</var> extracts a <code>DataSetInformation</code>. + * + * @return never <code>null</code> but prefers to throw an exception. + */ + private final DataSetInformation extractDataSetInformation(final File incomingDataSetPath) + { + try + { + final DataSetInformation dataSetInfo = + dataSetInfoExtractor.getDataSetInformation(incomingDataSetPath); + if (dataSetInfo.getSampleIdentifier() == null) + { + final String extractorName = dataSetInfoExtractor.getClass().getSimpleName(); + throw ConfigurationFailureException.fromTemplate( + "Data Set Information Extractor '%s' extracted no sample code " + + "for incoming data set '%s' (extractor contract violation).", + extractorName, incomingDataSetPath); + } + dataSetInfo.setInstanceCode(getHomeDatabaseInstance().getCode()); + dataSetInfo.setInstanceUUID(getHomeDatabaseInstance().getUuid()); + if (dataSetInfo.getGroupCode() == null) + { + dataSetInfo.setGroupCode(groupCode); + } + if (operationLog.isDebugEnabled()) + { + operationLog.debug(String.format( + "Extracting data set information '%s' from incoming " + + "data set path '%s'", dataSetInfo, incomingDataSetPath)); + } + return dataSetInfo; + } catch (final HighLevelException e) + { + throw e; + } catch (final RuntimeException ex) + { + throw new EnvironmentFailureException("Error when trying to identify data set '" + + incomingDataSetPath.getAbsolutePath() + "'.", ex); + } + } + + private final File createBaseDirectory(final IDataStoreStrategy strategy, + final File baseDir, final DataSetInformation dataSetInfo) + { + final File baseDirectory = + strategy.getBaseDirectory(baseDir, dataSetInfo, dataSetType); + baseDirectory.mkdirs(); + if (fileOperations.isDirectory(baseDirectory) == false) + { + throw EnvironmentFailureException.fromTemplate( + "Creating data set base directory '%s' for data set '%s' failed.", + baseDirectory.getAbsolutePath(), incomingDataSetFile); + } + return baseDirectory; + } + + private IProcessor tryCreateProcessor(final String procedureTypeCode) + { + final IProcessorFactory processorFactory = processorFactories.get(procedureTypeCode); + if (processorFactory == null) + { + return null; + } + return processorFactory.createProcessor(); + } + + private ProcessingInstructionDTO tryToGetAppropriateProcessingInstruction( + final ProcessingInstructionDTO[] processingInstructions, + final String procedureTypeCode) + { + if (processingInstructions != null) + { + for (final ProcessingInstructionDTO instruction : processingInstructions) + { + if (instruction.getProcedureTypeCode().equals(procedureTypeCode)) + { + return instruction; + } + } + } + return null; + } + + private final ExternalData createExternalData(final String relativePath, + final StorageFormat storageFormat, final BooleanOrUnknown isCompleteFlag) + { + final ExtractableData extractableData = dataSetInformation.getExtractableData(); + final ExternalData data = BeanUtils.createBean(ExternalData.class, extractableData); + data.setLocation(relativePath); + data.setLocatorType(typeExtractor.getLocatorType(incomingDataSetFile)); + data.setDataSetType(typeExtractor.getDataSetType(incomingDataSetFile)); + data.setFileFormatType(typeExtractor.getFileFormatType(incomingDataSetFile)); + data.setStorageFormat(storageFormat); + data.setComplete(isCompleteFlag); + return data; + } + + private final void writeThrowable(final Throwable throwable) + { + final String fileName = incomingDataSetFile.getName() + ".exception"; + final File file = + new File(baseDirectoryHolder.getTargetFile().getParentFile(), fileName); + FileWriter writer = null; + try + { + writer = new FileWriter(file); + throwable.printStackTrace(new PrintWriter(writer)); + } catch (final IOException e) + { + operationLog.warn(String.format( + "Could not write out the exception '%s' in file '%s'.", fileName, file + .getAbsolutePath()), e); + } finally + { + IOUtils.closeQuietly(writer); + } + } + + private boolean deleteAndLogIsFinishedFile() + { + if (fileOperations.exists(isFinishedFile) == false) + { + return false; + } + final boolean ok = fileOperations.delete(isFinishedFile); + final String absolutePath = isFinishedFile.getAbsolutePath(); + if (ok == false) + { + notificationLog.error(String.format("Removing file '%s' failed.", absolutePath)); + } else + { + if (operationLog.isDebugEnabled()) + { + operationLog.debug(String.format("File '%s' has been removed.", absolutePath)); + } + } + return ok; + } + } + +} diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/imsb/HCSImageFileExtractor.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/imsb/HCSImageFileExtractor.java new file mode 100644 index 00000000000..6112d16c551 --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/imsb/HCSImageFileExtractor.java @@ -0,0 +1,335 @@ +/* + * Copyright 2008 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver.imsb; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Properties; +import java.util.Set; + +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; + +import ch.rinn.restrictions.Private; +import ch.systemsx.cisd.bds.hcs.Channel; +import ch.systemsx.cisd.bds.hcs.Geometry; +import ch.systemsx.cisd.bds.hcs.Location; +import ch.systemsx.cisd.bds.hcs.WellGeometry; +import ch.systemsx.cisd.bds.storage.IDirectory; +import ch.systemsx.cisd.bds.storage.IFile; +import ch.systemsx.cisd.bds.storage.INode; +import ch.systemsx.cisd.common.exceptions.ConfigurationFailureException; +import ch.systemsx.cisd.common.exceptions.InvalidExternalDataException; +import ch.systemsx.cisd.common.exceptions.StopException; +import ch.systemsx.cisd.common.logging.LogCategory; +import ch.systemsx.cisd.common.logging.LogFactory; +import ch.systemsx.cisd.etlserver.ChannelSetHelper; +import ch.systemsx.cisd.etlserver.DataSetInformation; +import ch.systemsx.cisd.etlserver.HCSImageFileExtractionResult; +import ch.systemsx.cisd.etlserver.IHCSImageFileAccepter; +import ch.systemsx.cisd.etlserver.IHCSImageFileExtractor; + +/** + * A <code>IHCSImageFileExtractor</code> implementation suitable for <i>3V</i>. + * <p> + * This implementation extracts and processes image files having the format + * + * <code>Screening_<well id>_s<tile number>_w<channel number>_[<some UUID that we can just ignore>].tif</code> + * . An example is <code>Screening_H24_s6_w1_[UUID].tif</code>. + * </p> + * + * @author Christian Ribeaud + * @author Bernd Rinn + */ +// @Final +public class HCSImageFileExtractor implements IHCSImageFileExtractor +{ + private static final String TIFF_SUBDIRECTORY = "TIFF"; + + private static final Logger operationLog = + LogFactory.getLogger(LogCategory.OPERATION, HCSImageFileExtractor.class); + + static final String IMAGE_FILE_NOT_STANDARDIZABLE = + "Image file '%s' could not be standardized given following tokens [plateLocation=%s,wellLocation=%s,channel=%s]."; + + static final String IMAGE_FILE_NOT_ENOUGH_ENTITIES = + "Image file '%s' does not have enough entities."; + + static final String IMAGE_FILE_BELONGS_TO_WRONG_SAMPLE = + "Image file '%s' belongs to the wrong sample [expected=%s,found=%s]."; + + static final String IMAGE_FILE_ACCEPTED = + "Image file '%s' was accepted for channel %d, plate location %s and well location %s."; + + static final char TOKEN_SEPARATOR = '_'; + + private final Geometry wellGeometry; + + public HCSImageFileExtractor(final Properties properties) + { + assert properties != null : "Given properites should not be null"; + wellGeometry = getWellGeometry(properties); + } + + private final static Geometry getWellGeometry(final Properties properties) + { + final String property = properties.getProperty(WellGeometry.WELL_GEOMETRY); + if (property == null) + { + throw new ConfigurationFailureException(String.format( + "No '%s' property has been specified.", WellGeometry.WELL_GEOMETRY)); + } + final Geometry geometry = WellGeometry.createFromString(property); + if (geometry == null) + { + throw new ConfigurationFailureException(String.format( + "Could not create a geometry from property value '%s'.", property)); + } + return geometry; + } + + /** + * Extracts the well location from given <var>value</var>, following the convention adopted + * here. + * <p> + * Returns <code>null</code> if the operation fails. + * </p> + */ + private final Location tryGetWellLocation(final String value) + { + try + { + return Location.tryCreateLocationFromPosition(Integer.parseInt(value), wellGeometry); + } catch (final NumberFormatException ex) + { + // Nothing to do here. Rest of the code can handle this. + } + return null; + } + + /** + * Extracts the plate location from given <var>value</var>, following the convention adopted + * here. + * <p> + * Returns <code>null</code> if the operation fails. + * </p> + */ + private final static Location tryGetPlateLocation(final String value) + { + return Location.tryCreateLocationFromMatrixCoordinate(value); + } + + /** + * Extracts the channel from given <var>value</var>, following the convention adopted here. + * <p> + * Returns <code>0</code> if the operation fails. + * </p> + */ + private final int getChannelWavelength(final String value) + { + final String startsWith = "w"; + if (value.startsWith(startsWith)) + { + try + { + return Integer.parseInt(value.substring(startsWith.length())); + } catch (final NumberFormatException ex) + { + // Nothing to do here. Rest of the code can handle this. + } + } + return 0; + } + + private static class ImageFileRecord + { + final IFile imageFile; + + final Location plateLocation; + + final Location wellLocation; + + final int channelWavelength; + + ImageFileRecord(final IFile imageFile, final Location plateLocation, + final Location wellLocation, final int channelWavelength) + { + this.imageFile = imageFile; + this.plateLocation = plateLocation; + this.wellLocation = wellLocation; + this.channelWavelength = channelWavelength; + } + } + + /** Perform channel wavelength sorting on images. */ + private static class ChannelWavelengthSortingHCSImageFileAccepterDecorator implements + IHCSImageFileAccepter + { + + private final IHCSImageFileAccepter accepter; + + private final List<ImageFileRecord> images = new ArrayList<ImageFileRecord>(); + + private final ChannelSetHelper helper; + + ChannelWavelengthSortingHCSImageFileAccepterDecorator(final IHCSImageFileAccepter accepter) + { + this.accepter = accepter; + helper = new ChannelSetHelper(); + } + + /** + * Returns the set of <code>Channels</code>. + */ + final Set<Channel> getChannels() + { + return helper.getChannelSet(); + } + + /** + * Informs that {@link #accept(int, Location, Location, IFile)} will no longer get called. + * <p> + * We are now ready to construct the channels and to commit the images to the encapsulated + * <code>IHCSImageFileAccepter</code>. + * </p> + */ + final void commit() + { + for (final ImageFileRecord image : images) + { + accepter.accept(helper.getChannelForWavelength(image.channelWavelength) + .getCounter(), image.plateLocation, image.wellLocation, image.imageFile); + } + } + + // + // IHCSImageFileAccepter + // + + public final void accept(final int channelWavelength, final Location wellLocation, + final Location tileLocation, final IFile imageFile) + { + images + .add(new ImageFileRecord(imageFile, wellLocation, tileLocation, + channelWavelength)); + helper.addWavelength(channelWavelength); + } + + } + + // + // IHCSImageFileExtractor + // + + public final HCSImageFileExtractionResult process(final IDirectory incomingDataSetDirectory, + final DataSetInformation dataSetInformation, final IHCSImageFileAccepter accepter) + { + assert incomingDataSetDirectory != null; + final List<IFile> imageFiles = listTiffFiles(incomingDataSetDirectory); + final long start = System.currentTimeMillis(); + final List<IFile> invalidFiles = new LinkedList<IFile>(); + final ChannelWavelengthSortingHCSImageFileAccepterDecorator accepterDecorator = + new ChannelWavelengthSortingHCSImageFileAccepterDecorator(accepter); + for (final IFile imageFile : imageFiles) + { + StopException.check(); + if (operationLog.isDebugEnabled()) + { + operationLog.debug(String.format("Processing image file '%s'", imageFile)); + } + final String baseName = FilenameUtils.getBaseName(imageFile.getPath()); + final String[] tokens = StringUtils.split(baseName, TOKEN_SEPARATOR); + if (tokens.length < 4) + { + if (operationLog.isDebugEnabled()) + { + operationLog.debug(String.format(IMAGE_FILE_NOT_ENOUGH_ENTITIES, imageFile)); + } + invalidFiles.add(imageFile); + continue; + } + final String sampleCode = tokens[tokens.length - 4]; + if (dataSetInformation.getSampleIdentifier().getSampleCode().equals(sampleCode) == false) + { + if (operationLog.isDebugEnabled()) + { + operationLog.debug(String.format(IMAGE_FILE_BELONGS_TO_WRONG_SAMPLE, imageFile, + dataSetInformation.getSampleIdentifier(), sampleCode)); + } + invalidFiles.add(imageFile); + continue; + } + final String plateLocationStr = tokens[tokens.length - 3]; + final Location plateLocation = tryGetPlateLocation(plateLocationStr); + final String wellLocationStr = tokens[tokens.length - 2]; + final Location wellLocation = tryGetWellLocation(wellLocationStr); + final String channelStr = tokens[tokens.length - 1]; + final int channelWavelength = getChannelWavelength(channelStr); + if (wellLocation != null && plateLocation != null && channelWavelength > 0) + { + accepterDecorator.accept(channelWavelength, plateLocation, wellLocation, imageFile); + if (operationLog.isDebugEnabled()) + { + operationLog.debug(String.format(IMAGE_FILE_ACCEPTED, imageFile, + channelWavelength, plateLocation, wellLocation)); + } + } else + { + if (operationLog.isDebugEnabled()) + { + operationLog.debug(String.format(IMAGE_FILE_NOT_STANDARDIZABLE, imageFile, + plateLocationStr, wellLocationStr, channelStr)); + } + invalidFiles.add(imageFile); + } + } + accepterDecorator.commit(); + return new HCSImageFileExtractionResult(System.currentTimeMillis() - start, imageFiles + .size(), Collections.unmodifiableList(invalidFiles), accepterDecorator + .getChannels()); + } + + private IDirectory getTiffSubDirectory(final IDirectory incomingDataSetDirectory) + { + final INode tiffSubDirectoryNodeOrNull = + incomingDataSetDirectory.tryGetNode(TIFF_SUBDIRECTORY); + if (tiffSubDirectoryNodeOrNull == null) + { + throw InvalidExternalDataException.fromTemplate( + "The directory '%s' does not have a sub-directory '%s'.", + incomingDataSetDirectory.getPath(), TIFF_SUBDIRECTORY); + } + final IDirectory tiffSubDirectoryOrNull = tiffSubDirectoryNodeOrNull.tryAsDirectory(); + if (tiffSubDirectoryOrNull == null) + { + throw InvalidExternalDataException.fromTemplate("The file '%s/%s' is not a directory.", + incomingDataSetDirectory.getPath(), TIFF_SUBDIRECTORY); + } + return tiffSubDirectoryOrNull; + } + + @Private + List<IFile> listTiffFiles(final IDirectory directory) + { + final IDirectory tiffSubDirectory = getTiffSubDirectory(directory); + return tiffSubDirectory.listFiles(new String[] { "tif", "tiff" }, true); + } + +} diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/threev/AbstractDataSetInfoExtractorFor3V.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/threev/AbstractDataSetInfoExtractorFor3V.java new file mode 100644 index 00000000000..2b04add8cbd --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/threev/AbstractDataSetInfoExtractorFor3V.java @@ -0,0 +1,108 @@ +/* + * Copyright 2008 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver.threev; + +import java.io.File; +import java.util.Properties; + +import org.apache.commons.lang.StringUtils; + +import ch.rinn.restrictions.Private; +import ch.systemsx.cisd.common.exceptions.ConfigurationFailureException; +import ch.systemsx.cisd.common.exceptions.EnvironmentFailureException; +import ch.systemsx.cisd.common.exceptions.UserFailureException; +import ch.systemsx.cisd.common.utilities.PropertyUtils; +import ch.systemsx.cisd.etlserver.AbstractDataSetInfoExtractor; +import ch.systemsx.cisd.etlserver.DataSetInformation; +import ch.systemsx.cisd.etlserver.DataSetNameEntitiesProvider; +import ch.systemsx.cisd.etlserver.DefaultDataSetInfoExtractor; + +/** + * @author Franz-Josef Elmer + */ +abstract class AbstractDataSetInfoExtractorFor3V extends AbstractDataSetInfoExtractor +{ + /** + * Name of the property specifying the character which will be used to concatenate the entities + * specifying the data set code. + */ + @Private + static final String DATA_SET_CODE_ENTITIES_GLUE = "data-set-code-entities-glue"; + + private static final String DEFAULT_DATA_SET_CODE_ENTITIES_GLUE = "."; + + private final DefaultDataSetInfoExtractor codeExtractor; + + private final int[] dataSetCodeIndices; + + private final String dataSetCodeEntitiesGlue; + + public AbstractDataSetInfoExtractorFor3V(final Properties globalProperties, + final String indicesPropertyName) + { + super(globalProperties); + codeExtractor = new DefaultDataSetInfoExtractor(globalProperties); + final String indicesAsString = + PropertyUtils.getMandatoryProperty(properties, indicesPropertyName); + final String[] indicesAsStringArray = StringUtils.split(indicesAsString, ", "); + dataSetCodeIndices = new int[indicesAsStringArray.length]; + for (int i = 0; i < indicesAsStringArray.length; i++) + { + final String index = indicesAsStringArray[i]; + try + { + dataSetCodeIndices[i] = Integer.parseInt(index); + } catch (final NumberFormatException ex) + { + throw new ConfigurationFailureException(i + 1 + ". index in property '" + + indicesPropertyName + "' isn't a number: " + indicesAsString); + } + } + dataSetCodeEntitiesGlue = + properties.getProperty(DATA_SET_CODE_ENTITIES_GLUE, + DEFAULT_DATA_SET_CODE_ENTITIES_GLUE); + } + + // + // AbstractCodeExtractor + // + + public DataSetInformation getDataSetInformation(final File incomingDataSetPath) + throws UserFailureException, EnvironmentFailureException + { + final DataSetInformation dataSetInfo = + codeExtractor.getDataSetInformation(incomingDataSetPath); + final DataSetNameEntitiesProvider entitiesProvider = + new DataSetNameEntitiesProvider(incomingDataSetPath, entitySeparator, + stripExtension); + final StringBuilder builder = new StringBuilder(); + for (final int index : dataSetCodeIndices) + { + if (builder.length() > 0) + { + builder.append(dataSetCodeEntitiesGlue); + } + builder.append(entitiesProvider.getEntity(index)); + } + final String code = builder.toString(); + setCodeFor(dataSetInfo, code); + return dataSetInfo; + } + + protected abstract void setCodeFor(DataSetInformation dataSetInfo, String code); + +} diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/threev/DataSetInfoExtractorForDataAcquisition.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/threev/DataSetInfoExtractorForDataAcquisition.java new file mode 100644 index 00000000000..283b58a2195 --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/threev/DataSetInfoExtractorForDataAcquisition.java @@ -0,0 +1,83 @@ +/* + * Copyright 2008 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver.threev; + +import java.util.Properties; + +import ch.rinn.restrictions.Private; +import ch.systemsx.cisd.etlserver.DataSetInformation; +import ch.systemsx.cisd.etlserver.DefaultDataSetInfoExtractor; + +/** + * Implementation which assumes that the information can be extracted from the file name. Following + * information can be extracted: + * <ul> + * <li>Sample code + * <li>Parent data set code + * <li>Data producer code + * <li>Data production date + * <li>Data set code + * </ul> + * This class uses the same properties as {@link DefaultDataSetInfoExtractor}. In addition the + * following properties are used to extract the data set code: <table border="1" cellspacing="0" + * cellpadding="5"> + * <tr> + * <th>Property</th> + * <th>Default value</th> + * <th>Description</th> + * </tr> + * <tr> + * <td><code>indices-of-data-set-code-entities</code></td> + * <td> </td> + * <td>Space or comma separated list of entity indices which define the data set code uniquely. + * This is a mandatory property.</td> + * </tr> + * <tr> + * <td><code>data-set-code-entities-glue</code></td> + * <td><code>.</code></td> + * <td>Symbol used to concatenate entities defining the data set code.</td> + * </tr> + * </table> The first entity has index 0, the second 1, etc. Using negative numbers one can specify + * entities from the end. Thus, -1 means the last entity, -2 the second last entity, etc. + * + * @author Franz-Josef Elmer + */ +public class DataSetInfoExtractorForDataAcquisition extends AbstractDataSetInfoExtractorFor3V +{ + /** + * Name of the property specifying the indices of those entities which define uniquely the data + * set code. + * <p> + * Use a negative number to count from the end, e.g. <code>-1</code> indicates the last + * entity. + * </p> + */ + @Private + static final String INDICES_OF_DATA_SET_CODE_ENTITIES = "indices-of-data-set-code-entities"; + + public DataSetInfoExtractorForDataAcquisition(final Properties properties) + { + super(properties, INDICES_OF_DATA_SET_CODE_ENTITIES); + } + + @Override + protected void setCodeFor(final DataSetInformation dataSetInfo, final String code) + { + dataSetInfo.setDataSetCode(code); + } + +} diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/threev/DataSetInfoExtractorForImageAnalysis.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/threev/DataSetInfoExtractorForImageAnalysis.java new file mode 100644 index 00000000000..211c053e724 --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/threev/DataSetInfoExtractorForImageAnalysis.java @@ -0,0 +1,83 @@ +/* + * Copyright 2008 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver.threev; + +import java.util.Properties; + +import ch.rinn.restrictions.Private; +import ch.systemsx.cisd.etlserver.DataSetInformation; +import ch.systemsx.cisd.etlserver.DefaultDataSetInfoExtractor; + +/** + * Implementation which assumes that the information can be extracted from the file name. Following + * information can be extracted: + * <ul> + * <li>Sample code + * <li>Parent data set code + * <li>Data producer code + * <li>Data production date + * </ul> + * This class uses the same properties as {@link DefaultDataSetInfoExtractor} except + * <code>index-of-parent-data-set-code</code>. Instead the following properties are used to + * extract the parent data set code: <table border="1" cellspacing="0" cellpadding="5"> + * <tr> + * <th>Property</th> + * <th>Default value</th> + * <th>Description</th> + * </tr> + * <tr> + * <td><code>indices-of-parent-data-set-code-entities</code></td> + * <td> </td> + * <td>Space or comma separated list of entity indices which define the parent data set code + * uniquely. This is a mandatory property.</td> + * </tr> + * <tr> + * <td><code>data-set-code-entities-glue</code></td> + * <td><code>.</code></td> + * <td>Symbol used to concatenate entities defining the parent data set code.</td> + * </tr> + * </table> The first entity has index 0, the second 1, etc. Using negative numbers one can specify + * entities from the end. Thus, -1 means the last entity, -2 the second last entity, etc. + * + * @author Franz-Josef Elmer + */ +public class DataSetInfoExtractorForImageAnalysis extends AbstractDataSetInfoExtractorFor3V +{ + /** + * Name of the property specifying the indices of those entities which define uniquely the + * parent data set code. + * <p> + * Use a negative number to count from the end, e.g. <code>-1</code> indicates the last + * entity. + * </p> + */ + @Private + static final String INDICES_OF_PARENT_DATA_SET_CODE_ENTITIES = + "indices-of-parent-data-set-code-entities"; + + public DataSetInfoExtractorForImageAnalysis(final Properties properties) + { + super(properties, INDICES_OF_PARENT_DATA_SET_CODE_ENTITIES); + } + + @Override + protected void setCodeFor(final DataSetInformation dataSetInfo, final String code) + { + dataSetInfo.setParentDataSetCode(code); + } + +} diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/threev/HCSImageFileExtractor.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/threev/HCSImageFileExtractor.java new file mode 100644 index 00000000000..c250dfaebfe --- /dev/null +++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/threev/HCSImageFileExtractor.java @@ -0,0 +1,211 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver.threev; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Properties; + +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; + +import ch.systemsx.cisd.bds.hcs.Geometry; +import ch.systemsx.cisd.bds.hcs.Location; +import ch.systemsx.cisd.bds.hcs.WellGeometry; +import ch.systemsx.cisd.bds.storage.IDirectory; +import ch.systemsx.cisd.bds.storage.IFile; +import ch.systemsx.cisd.common.exceptions.ConfigurationFailureException; +import ch.systemsx.cisd.common.exceptions.StopException; +import ch.systemsx.cisd.common.logging.LogCategory; +import ch.systemsx.cisd.common.logging.LogFactory; +import ch.systemsx.cisd.etlserver.ChannelSetHelper; +import ch.systemsx.cisd.etlserver.DataSetInformation; +import ch.systemsx.cisd.etlserver.HCSImageFileExtractionResult; +import ch.systemsx.cisd.etlserver.IHCSImageFileAccepter; +import ch.systemsx.cisd.etlserver.IHCSImageFileExtractor; + +/** + * A <code>IHCSImageFileExtractor</code> implementation suitable for <i>3V</i>. + * <p> + * This implementation extracts and processes image files having the format + * + * <code>Screening_<well id>_s<tile number>_w<channel number>_[<some UUID that we can just ignore>].tif</code> + * . An example is <code>Screening_H24_s6_w1_[UUID].tif</code>. + * </p> + * + * @author Christian Ribeaud + */ +public final class HCSImageFileExtractor implements IHCSImageFileExtractor +{ + private static final Logger operationLog = + LogFactory.getLogger(LogCategory.OPERATION, HCSImageFileExtractor.class); + + static final String IMAGE_FILE_NOT_STANDARDIZABLE = + "Image file '%s' could not be standardized given following tokens [plateLocation=%s,wellLocation=%s,channel=%s]."; + + static final String IMAGE_FILE_ACCEPTED = + "Image file '%s' was accepted for channel %d, plate location %s and well location %s."; + + static final String FILE_PREFIX = "Screening_"; + + static final int TOKEN_NUMBER = 5; + + static final char TOKEN_SEPARATOR = '_'; + + private final Geometry wellGeometry; + + public HCSImageFileExtractor(final Properties properties) + { + assert properties != null : "Given properites should not be null"; + wellGeometry = getWellGeometry(properties); + } + + private final static Geometry getWellGeometry(final Properties properties) + { + final String property = properties.getProperty(WellGeometry.WELL_GEOMETRY); + if (property == null) + { + throw new ConfigurationFailureException(String.format( + "No '%s' property has been specified.", WellGeometry.WELL_GEOMETRY)); + } + final Geometry geometry = WellGeometry.createFromString(property); + if (geometry == null) + { + throw new ConfigurationFailureException(String.format( + "Could not create a geometry from property value '%s'.", property)); + } + return geometry; + } + + /** + * Extracts the well location from given <var>value</var>, following the convention adopted + * here. + * <p> + * Returns <code>null</code> if the operation fails. + * </p> + */ + private final Location tryGetWellLocation(final String value) + { + final String startsWith = "s"; + if (value.startsWith(startsWith)) + { + final String tileNo = value.substring(startsWith.length()); + try + { + return Location.tryCreateLocationFromPosition(Integer.parseInt(tileNo), + wellGeometry); + } catch (NumberFormatException ex) + { + // Nothing to do here. Rest of the code can handle this. + } + } + return null; + } + + /** + * Extracts the plate location from given <var>value</var>, following the convention adopted + * here. + * <p> + * Returns <code>null</code> if the operation fails. + * </p> + */ + private final static Location tryGetPlateLocation(final String value) + { + return Location.tryCreateLocationFromMatrixCoordinate(value); + } + + /** + * Extracts the wavelength from given <var>value</var>, following the convention adopted here. + * <p> + * Returns <code>0</code> if the operation fails. + * </p> + */ + private final int getWavelength(final String value) + { + final String startsWith = "w"; + if (value.startsWith(startsWith)) + { + try + { + return Integer.parseInt(value.substring(startsWith.length())); + } catch (NumberFormatException ex) + { + // Nothing to do here. Rest of the code can handle this. + } + } + return 0; + } + + // + // IHCSImageFileExtractor + // + + public final HCSImageFileExtractionResult process(final IDirectory incomingDataSetDirectory, + DataSetInformation dataSetInformation, final IHCSImageFileAccepter accepter) + { + assert incomingDataSetDirectory != null; + final List<IFile> imageFiles = incomingDataSetDirectory.listFiles(new String[] + { "tif", "tiff" }, true); + final long start = System.currentTimeMillis(); + final List<IFile> invalidFiles = new LinkedList<IFile>(); + final ChannelSetHelper helper = new ChannelSetHelper(); + for (final IFile imageFile : imageFiles) + { + StopException.check(); + if (operationLog.isDebugEnabled()) + { + operationLog.debug(String.format("Processing image file '%s'", imageFile)); + } + final String baseName = FilenameUtils.getBaseName(imageFile.getPath()); + if (baseName.startsWith(FILE_PREFIX) == false) + { + continue; + } + final String[] tokens = StringUtils.split(baseName, TOKEN_SEPARATOR); + if (tokens.length != TOKEN_NUMBER) + { + continue; + } + final Location plateLocation = tryGetPlateLocation(tokens[1]); + final Location wellLocation = tryGetWellLocation(tokens[2]); + final int wavelength = getWavelength(tokens[3]); + if (wellLocation != null && plateLocation != null && wavelength > 0) + { + helper.addWavelength(wavelength); + accepter.accept(wavelength, plateLocation, wellLocation, imageFile); + if (operationLog.isDebugEnabled()) + { + operationLog.debug(String.format(IMAGE_FILE_ACCEPTED, imageFile, wavelength, + plateLocation, wellLocation)); + } + } else + { + if (operationLog.isDebugEnabled()) + { + operationLog.debug(String.format(IMAGE_FILE_NOT_STANDARDIZABLE, imageFile, + tokens[0], tokens[1], tokens[2])); + } + invalidFiles.add(imageFile); + } + } + return new HCSImageFileExtractionResult(System.currentTimeMillis() - start, imageFiles + .size(), Collections.unmodifiableList(invalidFiles), helper.getChannelSet()); + } + +} diff --git a/datastore_server/sourceTest/bash/createFakeDataSet.sh b/datastore_server/sourceTest/bash/createFakeDataSet.sh new file mode 100755 index 00000000000..0fa1d6fb432 --- /dev/null +++ b/datastore_server/sourceTest/bash/createFakeDataSet.sh @@ -0,0 +1,179 @@ +#!/bin/sh +# +# @date: 2008-04-18 +# @author: franz-josef.elmer@systemsx.ch +# @author: basil.neff@systemsx.ch +# +# This bash script generates dummy data to test the installation. +# The output is currently in the Subfolder of the Script: <SAMPLE COLDE>/TIFF +# The Files are empty and have the following filename: +# <SAMPLE CODE>_<PLATE ROW (as character)><PLATE COLUMN>_<TILE NUMBER>_w<WAVELENGTH>.tif +# + +################### +## USED BINARIES ## +################### +TOUCH=/usr/bin/touch +BC=/usr/bin/bc + + +############################ +## DO NOT CROSS THIS LINE ## +############################ + +if [ $# -ne 4 ]; then + echo "Usage: `basename $0` <sample code> <number of channels> <plate geometry> <well geometry>" + exit 1 +fi + +SAMPLE=$1 +CHANNELS=$2 +PLATE_GEOMETRY=$3 +WELL_GEOMETRY=$4 + + + +if [ ${CHANNELS} -le 0 ]; then + echo "<number of channnels> has to be greater than 0" + exit 1 +fi + +case ${PLATE_GEOMETRY} in + 8x12) + PLATE_ROWS=8 + PLATE_COLUMNS=12 + ;; + 16x24) + PLATE_ROWS=16 + PLATE_COLUMNS=24 + ;; + 32x48) + PLATE_ROWS=32 + PLATE_COLUMNS=48 + ;; + *) + echo "Plate geometry has to be '8x12', '16x24', or '32x48'" + exit 1 +esac + +case ${WELL_GEOMETRY} in + 1x1) + WELL_ROWS=1 + WELL_COLUMNS=1 + TILES=1 + ;; + 2x2) + WELL_ROWS=2 + WELL_COLUMNS=2 + TILES=4 + ;; + 3x3) + WELL_ROWS=3 + WELL_COLUMNS=3 + TILES=9 + ;; + *) + echo "Well geometry has to be '1x1', '2x2', or '3x3'" + exit 1 +esac + +############### +## FUNCTIONS ## +############### + +function getCharacterFromInt { + POSITION=$1 + if [ ${POSITION} -lt 26 ];then + characters=(A B C D E F G H I J K L M N O P Q R S T U V W X Y Z) + echo ${characters[${POSITION}]} + else + FIRST_INTEGER=`echo $[(${POSITION}-25)/25]|${BC}` + FIRST_CHARACTER=`getCharacterFromInt ${FIRST_INTEGER}` + SECOND_CHARACTER=`getCharacterFromInt $[${POSITION}%26]` + echo "${FIRST_CHARACTER}${SECOND_CHARACTER}" + fi +} + +function getWavelength { + WAVELENGTH=$1 + echo "${WAVELENGTH}42" + } + +PROGRESS_INFO=1 +function printProgress { + PERCENT=`echo $1|${BC}` + WHEEL=`getProgressWheel ${PROGRESS_INFO}` + echo -ne " ${PERCENT}% [${WHEEL}]\r" + + PROGRESS_INFO=$[$PROGRESS_INFO+1] +} + +function getProgressWheel { + PARAMETER=$1 + modulo_tree=`echo $[${PARAMETER}%3]` + progress_array=(\| / - \\) + echo ${progress_array[${modulo_tree}]} +} + +############ +## SCRIPT ## +############ + +echo "${CHANNELS} channels, plate: ${PLATE_ROWS} x ${PLATE_COLUMNS}, well: ${WELL_ROWS} x ${WELL_COLUMNS}" + +if [ ! -d ${SAMPLE} ];then + mkdir ${SAMPLE} +fi +if [ ! -d ${SAMPLE}/TIFF ];then + mkdir ${SAMPLE}/TIFF +fi + +### +# LOOPS +### + +OVERALL_COUNTER=0 +TOTAL_FILES_TO_GENERATE=$[${CHANNELS}*${PLATE_ROWS}*${PLATE_COLUMNS}*${TILES}] +# Plate Rows +plateRowCounter=0 +while [ ${plateRowCounter} -lt ${PLATE_ROWS} ] +do + + # Plate Columns + plateColumsCounter=1 + while [ ${plateColumsCounter} -le ${PLATE_COLUMNS} ] + do + + PLATE_ROW_CHARACTER=`getCharacterFromInt ${plateRowCounter}` + + # Plate Column + if [ ${plateColumsCounter} -lt 10 ];then + PLATE_COLUMN_INT=0${plateColumsCounter} + else + PLATE_COLUMN_INT=${plateColumsCounter} + fi + + # TILES + tileCounter=1 + while [ ${tileCounter} -le ${TILES} ] + do + # Channels + channelCounter=1 + while [ ${channelCounter} -le ${CHANNELS} ] + do + FILECHANEL=`getWavelength ${channelCounter}` + ${TOUCH} ${SAMPLE}/TIFF/${SAMPLE}_${PLATE_ROW_CHARACTER}${PLATE_COLUMN_INT}_${tileCounter}_w${FILECHANEL}.tif + + OVERALL_COUNTER=$[${OVERALL_COUNTER}+1] + channelCounter=$[${channelCounter}+1] + done + tileCounter=$[$tileCounter+1] + done + printProgress $((${OVERALL_COUNTER}*100/${TOTAL_FILES_TO_GENERATE})) + plateColumsCounter=$[$plateColumsCounter+1] + done + plateRowCounter=$[$plateRowCounter+1] +done + +echo "" +echo "${OVERALL_COUNTER} files generated" diff --git a/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/BDSStorageProcessorTest.java b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/BDSStorageProcessorTest.java new file mode 100644 index 00000000000..93fed037522 --- /dev/null +++ b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/BDSStorageProcessorTest.java @@ -0,0 +1,539 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertFalse; +import static org.testng.AssertJUnit.assertNotNull; +import static org.testng.AssertJUnit.assertNull; +import static org.testng.AssertJUnit.fail; + +import java.io.File; +import java.io.IOException; +import java.sql.Date; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Properties; + +import org.apache.log4j.Level; +import org.jmock.Expectations; +import org.jmock.Mockery; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import ch.systemsx.cisd.bds.DataSet; +import ch.systemsx.cisd.bds.DataStructureLoader; +import ch.systemsx.cisd.bds.ExperimentRegistrator; +import ch.systemsx.cisd.bds.Format; +import ch.systemsx.cisd.bds.IDataStructure; +import ch.systemsx.cisd.bds.Sample; +import ch.systemsx.cisd.bds.UnknownFormatV1_0; +import ch.systemsx.cisd.bds.Utilities; +import ch.systemsx.cisd.bds.Version; +import ch.systemsx.cisd.bds.Utilities.Boolean; +import ch.systemsx.cisd.bds.hcs.Channel; +import ch.systemsx.cisd.bds.hcs.HCSImageFormatV1_0; +import ch.systemsx.cisd.bds.hcs.Location; +import ch.systemsx.cisd.bds.hcs.WellGeometry; +import ch.systemsx.cisd.bds.storage.IDirectory; +import ch.systemsx.cisd.bds.storage.IFile; +import ch.systemsx.cisd.bds.v1_1.IDataStructureV1_1; +import ch.systemsx.cisd.common.exceptions.ConfigurationFailureException; +import ch.systemsx.cisd.common.filesystem.AbstractFileSystemTestCase; +import ch.systemsx.cisd.common.filesystem.FileUtilities; +import ch.systemsx.cisd.common.filesystem.QueueingPathRemoverService; +import ch.systemsx.cisd.common.logging.BufferedAppender; +import ch.systemsx.cisd.common.mail.IMailClient; +import ch.systemsx.cisd.common.types.BooleanOrUnknown; +import ch.systemsx.cisd.openbis.generic.shared.dto.DataTypePE; +import ch.systemsx.cisd.openbis.generic.shared.dto.ExperimentPE; +import ch.systemsx.cisd.openbis.generic.shared.dto.GroupPE; +import ch.systemsx.cisd.openbis.generic.shared.dto.PersonPE; +import ch.systemsx.cisd.openbis.generic.shared.dto.ProjectPE; +import ch.systemsx.cisd.openbis.generic.shared.dto.PropertyTypePE; +import ch.systemsx.cisd.openbis.generic.shared.dto.SamplePropertyPE; +import ch.systemsx.cisd.openbis.generic.shared.dto.SampleTypePropertyTypePE; +import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.ExperimentIdentifier; +import ch.systemsx.cisd.openbis.generic.shared.dto.properties.EntityDataType; +import ch.systemsx.cisd.openbis.generic.shared.dto.types.SampleTypeCode; + +/** + * Test cases for corresponding {@link BDSStorageProcessor} class. + * + * @author Christian Ribeaud + */ +public final class BDSStorageProcessorTest extends AbstractFileSystemTestCase +{ + private static final String EXAMPLE_EMAIL = "j@d"; + + private static final String DATA_SET_CODE = "D"; + + private static final String INCOMING_DATA_SET_DIR = "NEMO.EXP1==CP001A-3AB"; + + private static final String EXAMPLE_TYPE_DESCRIPTION = "Screening Plate"; + + private static final Date REGISTRATION_DATE = new Date(47110000); + + private static final String EXAMPLE_INSTANCE = "I"; + + private static final String EXAMPLE_INSTANCE_GLOBAL = "222-333"; + + private static final String EXAMPLE_GROUP = "G"; + + private static final String DATA_STRUCTURE_NAME = "originalData"; + + private static final String EXAMPLE_DATA = "hello world!"; + + private static final String ORIGINAL_DATA_TXT = DATA_STRUCTURE_NAME + ".txt"; + + private static final String VERSION_PROPERTY_KEY = BDSStorageProcessor.VERSION_KEY; + + private static final String FORMAT_KEY = BDSStorageProcessor.FORMAT_KEY; + + private static final String SAMPLE_TYPE_DESCRIPTION_KEY = + BDSStorageProcessor.SAMPLE_TYPE_DESCRIPTION_KEY; + + private static final String SAMPLE_TYPE_CODE_KEY = + + BDSStorageProcessor.SAMPLE_TYPE_CODE_KEY; + + private static final String CHANNEL_COUNT_KEY = HCSImageFormatV1_0.NUMBER_OF_CHANNELS; + + private static final String CONTAINS_ORIGINAL_DATA_KEY = + HCSImageFormatV1_0.CONTAINS_ORIGINAL_DATA; + + private static final String WELL_GEOMETRY_KEY = WellGeometry.WELL_GEOMETRY; + + private static final String FILE_EXTRACTOR_KEY = IHCSImageFileExtractor.FILE_EXTRACTOR; + + private final static IProcedureAndDataTypeExtractor TYPE_EXTRACTOR = + new DefaultStorageProcessorTest.TestProcedureAndDataTypeExtractor(); + + private final static String STORE_ROOT_DIR = "store"; + + private BufferedAppender logRecorder; + + private Mockery context; + + private IMailClient mailClient; + + private final static Properties createProperties(final Format format) + { + final Properties props = createPropertiesWithVersion(); + props.setProperty(Main.STOREROOT_DIR_KEY, "store"); + props.setProperty(SAMPLE_TYPE_DESCRIPTION_KEY, EXAMPLE_TYPE_DESCRIPTION); + props.setProperty(SAMPLE_TYPE_CODE_KEY, SampleTypeCode.CELL_PLATE.getCode()); + props.setProperty(FORMAT_KEY, format.getCode() + " " + format.getVersion()); + props.setProperty(CHANNEL_COUNT_KEY, "1"); + props.setProperty(CONTAINS_ORIGINAL_DATA_KEY, Utilities.Boolean.TRUE.toString()); + props.setProperty(WELL_GEOMETRY_KEY, "3x3"); + props.setProperty(FILE_EXTRACTOR_KEY, TestImageFileExtractor.class.getName()); + return props; + } + + final static DataSetInformation createDataSetInformation() + { + final DataSetInformation dataSetInformation = new DataSetInformation(); + dataSetInformation.setInstanceCode(EXAMPLE_INSTANCE); + dataSetInformation.setInstanceUUID(EXAMPLE_INSTANCE_GLOBAL); + final ExperimentIdentifier experimentIdentifier = new ExperimentIdentifier(); + experimentIdentifier.setExperimentCode("E"); + experimentIdentifier.setProjectCode("P"); + experimentIdentifier.setGroupCode(EXAMPLE_GROUP); + dataSetInformation.setExperimentIdentifier(experimentIdentifier); + dataSetInformation.setSampleCode("S"); + dataSetInformation.setDataSetCode(DATA_SET_CODE); + final SamplePropertyPE plateGeometry = + createSamplePropertyPE(PlateDimensionParser.PLATE_GEOMETRY_PROPERTY_NAME, + EntityDataType.VARCHAR, "_16X24"); + dataSetInformation.setProperties(new SamplePropertyPE[] + { plateGeometry }); + return dataSetInformation; + } + + private final static SamplePropertyPE createSamplePropertyPE(final String code, + final EntityDataType dataType, final String value) + { + final SamplePropertyPE propertyPE = new SamplePropertyPE(); + final SampleTypePropertyTypePE entityTypePropertyTypePE = new SampleTypePropertyTypePE(); + final PropertyTypePE propertyTypePE = new PropertyTypePE(); + propertyTypePE.setCode(code); + propertyTypePE.setLabel(code); + final DataTypePE type = new DataTypePE(); + type.setCode(dataType); + propertyTypePE.setType(type); + entityTypePropertyTypePE.setPropertyType(propertyTypePE); + propertyPE.setEntityTypePropertyType(entityTypePropertyTypePE); + propertyPE.setValue(value); + return propertyPE; + } + + private final File createOriginalDataInDir() throws IOException + { + final File incoming = new File(workingDirectory, "incoming"); + incoming.mkdir(); + final File dir = new File(incoming, INCOMING_DATA_SET_DIR); + dir.mkdir(); + final File originalData = new File(dir, ORIGINAL_DATA_TXT); + FileUtilities.writeToFile(originalData, EXAMPLE_DATA); + return dir; + } + + static final ExperimentPE createExperiment() + { + final ExperimentPE baseExperiment = new ExperimentPE(); + baseExperiment.setRegistrationDate(REGISTRATION_DATE); + final PersonPE person = new PersonPE(); + person.setFirstName("Joe"); + person.setLastName("Doe"); + person.setEmail(EXAMPLE_EMAIL); + final GroupPE group = new GroupPE(); + group.setCode(EXAMPLE_GROUP); + final ProjectPE project = new ProjectPE(); + project.setGroup(group); + baseExperiment.setProject(project); + baseExperiment.setRegistrator(person); + return baseExperiment; + } + + private final static Properties createPropertiesWithVersion() + { + final Properties props = new Properties(); + props.setProperty(VERSION_PROPERTY_KEY, "1.1"); + return props; + } + + @BeforeClass + public void startQueueingPathRemover() + { + if (QueueingPathRemoverService.isRunning() == false) + { + QueueingPathRemoverService.start(); + } + } + + @Override + @BeforeMethod + public void setUp() throws IOException + { + super.setUp(); + logRecorder = new BufferedAppender("%-5p %c - %m%n", Level.INFO); + context = new Mockery(); + mailClient = context.mock(IMailClient.class); + } + + @AfterMethod + public void tearDown() + { + logRecorder.reset(); + // The following line of code should also be called at the end of each test method. + // Otherwise one do not known which test failed. + context.assertIsSatisfied(); + } + + @Test + public final void testConstructorWithUnspecifiedArgument() + { + boolean fail = true; + try + { + new BDSStorageProcessor(null); + } catch (final AssertionError ex) + { + fail = false; + } + assertFalse(fail); + } + + @Test + public final void testCheckVersionInProperties() + { + final Properties props = new Properties(); + props.setProperty(Main.STOREROOT_DIR_KEY, "store"); + final String version = "ae"; + props.setProperty(VERSION_PROPERTY_KEY, version); + try + { + new BDSStorageProcessor(props); + fail("No '.' in given version."); + } catch (final ConfigurationFailureException ex) + { + assertEquals(String.format(BDSStorageProcessor.NO_VERSION_FORMAT, + BDSStorageProcessor.VERSION_KEY, version), ex.getMessage()); + } + } + + @Test + public final void testCheckVersionCompatible() + { + final Properties props = new Properties(); + props.setProperty(Main.STOREROOT_DIR_KEY, "store"); + props.setProperty(VERSION_PROPERTY_KEY, "1.2"); + try + { + new BDSStorageProcessor(props); + fail("ConfigurationFailureException expected"); + } catch (final ConfigurationFailureException e) + { + assertEquals("Invalid version: V1.2", e.getMessage()); + } + } + + @Test + public final void testMissingFormat() throws Exception + { + final Properties props = createPropertiesWithVersion(); + try + { + new BDSStorageProcessor(props); + fail("ConfigurationFailureException expected"); + } catch (final ConfigurationFailureException e) + { + assertEquals("Given key 'format' not found in properties '[version]'", e.getMessage()); + } + } + + @Test + public final void testMissingSampleTypeDescription() throws Exception + { + final Properties props = createPropertiesWithVersion(); + final Format format = UnknownFormatV1_0.UNKNOWN_1_0; + props.setProperty(FORMAT_KEY, format.getCode() + " " + format.getVersion()); + try + { + new BDSStorageProcessor(props); + fail("ConfigurationFailureException expected"); + } catch (final ConfigurationFailureException e) + { + assertEquals("Given key 'sampleTypeDescription' not found in properties " + + "'[version, format]'", e.getMessage()); + } + } + + @DataProvider(name = "formatProvider") + public Object[][] getFormats() + { + return new Object[][] + { + { UnknownFormatV1_0.UNKNOWN_1_0 }, + { HCSImageFormatV1_0.HCS_IMAGE_1_0 } }; + } + + @Test(dataProvider = "formatProvider") + public final void testStoreData(final Format format) throws Exception + { + final Properties properties = createProperties(format); + final BDSStorageProcessor storageProcessor = new BDSStorageProcessor(properties); + assertEquals(0, workingDirectory.list().length); + final File incomingDataSetDirectory = createOriginalDataInDir(); + assertEquals(true, incomingDataSetDirectory.exists()); + final ExperimentPE baseExperiment = createExperiment(); + final DataSetInformation dataSetInformation = createDataSetInformation(); + prepareMailClient(format); + final File dataFile = + storageProcessor.storeData(baseExperiment, dataSetInformation, TYPE_EXTRACTOR, + mailClient, incomingDataSetDirectory, new File(workingDirectory, + STORE_ROOT_DIR)); + assertEquals(new File(workingDirectory, STORE_ROOT_DIR).getAbsolutePath(), dataFile + .getAbsolutePath()); + final IDataStructure dataStructure = + new DataStructureLoader(workingDirectory).load(STORE_ROOT_DIR); + assertEquals(true, dataStructure instanceof IDataStructureV1_1); + final IDataStructureV1_1 ds = (IDataStructureV1_1) dataStructure; + assertEquals(new Version(1, 1), ds.getVersion()); + final ch.systemsx.cisd.bds.ExperimentIdentifier eid = ds.getExperimentIdentifier(); + assertEquals(EXAMPLE_INSTANCE, eid.getInstanceCode()); + assertEquals(EXAMPLE_GROUP, eid.getGroupCode()); + assertEquals(dataSetInformation.getExperimentIdentifier().getProjectCode(), eid + .getProjectCode()); + assertEquals(dataSetInformation.getExperimentIdentifier().getExperimentCode(), eid + .getExperimentCode()); + final ExperimentRegistrator registrator = ds.getExperimentRegistrator(); + assertEquals(baseExperiment.getRegistrator().getFirstName(), registrator.getFirstName()); + assertEquals(baseExperiment.getRegistrator().getLastName(), registrator.getLastName()); + assertEquals(baseExperiment.getRegistrator().getEmail(), registrator.getEmail()); + assertEquals(REGISTRATION_DATE, ds.getExperimentRegistratorTimestamp().getDate()); + final Sample sample = ds.getSample(); + assertEquals(EXAMPLE_TYPE_DESCRIPTION, sample.getTypeDescription()); + assertEquals(dataSetInformation.getSampleIdentifier().getSampleCode(), sample.getCode()); + final Format f = ds.getFormattedData().getFormat(); + assertEquals(format, f); + final IDirectory directory = + (IDirectory) ds.getOriginalData().tryGetNode(INCOMING_DATA_SET_DIR); + assertEquals(EXAMPLE_DATA, Utilities.getTrimmedString(directory, ORIGINAL_DATA_TXT)); + assertEquals(false, incomingDataSetDirectory.exists()); + // DataSet + final DataSet dataSet = ds.getDataSet(); + assertEquals(DATA_SET_CODE, dataSet.getCode()); + assertEquals(TYPE_EXTRACTOR.getDataSetType(null).getCode(), dataSet.getDataSetTypeCode()); + assertEquals(0, dataSet.getParentCodes().size()); + assertNull(dataSet.getProducerCode()); + assertNull(dataSet.getProductionTimestamp()); + assertEquals(Boolean.TRUE, dataSet.isMeasured()); + + context.assertIsSatisfied(); + } + + @Test(dataProvider = "formatProvider") + public final void testUnstoreData(final Format format) throws Exception + { + final Properties properties = createProperties(format); + final BDSStorageProcessor storageAdapter = new BDSStorageProcessor(properties); + assertEquals(0, workingDirectory.list().length); + final File incomingDirectoryData = createOriginalDataInDir(); + // incoming/NEMO.EXP1==CP001A-3AB in 'workingDirectory' + assert incomingDirectoryData.exists(); + final ExperimentPE baseExperiment = createExperiment(); + final DataSetInformation dataSetInformation = createDataSetInformation(); + // NEMO.EXP1==CP001A-3AB in 'workingDirectory' + prepareMailClient(format); + final File storeRootDir = new File(workingDirectory, STORE_ROOT_DIR); + final File dataStore = + storageAdapter.storeData(baseExperiment, dataSetInformation, TYPE_EXTRACTOR, + mailClient, incomingDirectoryData, storeRootDir); + assertEquals(true, dataStore.isDirectory()); + assertEquals(false, incomingDirectoryData.exists()); + storageAdapter.unstoreData(incomingDirectoryData, storeRootDir); + assertEquals(false, dataStore.exists()); + assertEquals(true, incomingDirectoryData.isDirectory()); + + context.assertIsSatisfied(); + } + + @Test(dataProvider = "formatProvider") + public void testTryToGetOriginalData(final Format format) throws Exception + { + final Properties properties = createProperties(format); + final BDSStorageProcessor storageProcessor = new BDSStorageProcessor(properties); + final File incomingDirectoryData = createOriginalDataInDir(); + final ExperimentPE baseExperiment = createExperiment(); + final DataSetInformation dataSetInformation = createDataSetInformation(); + prepareMailClient(format); + final File storeData = + storageProcessor.storeData(baseExperiment, dataSetInformation, TYPE_EXTRACTOR, + mailClient, incomingDirectoryData, workingDirectory); + final File originalDataSet = storageProcessor.tryGetProprietaryData(storeData); + assertNotNull(originalDataSet); + assertEquals(INCOMING_DATA_SET_DIR, originalDataSet.getName()); + assertEquals(true, originalDataSet.isDirectory()); + assertEquals(format == UnknownFormatV1_0.UNKNOWN_1_0 ? BooleanOrUnknown.U + : BooleanOrUnknown.F, dataSetInformation.getIsCompleteFlag()); + final File[] files = originalDataSet.listFiles(); + assertEquals(1, files.length); + final File file = files[0]; + assertEquals(true, file.exists()); + assertEquals(false, file.isDirectory()); + assertEquals(ORIGINAL_DATA_TXT, file.getName()); + + context.assertIsSatisfied(); + } + + @Test + public void testTryToGetOriginalDataWhichAreNotAvailable() throws Exception + { + final Properties properties = createProperties(HCSImageFormatV1_0.HCS_IMAGE_1_0); + properties.setProperty(CONTAINS_ORIGINAL_DATA_KEY, Utilities.Boolean.FALSE.toString()); + final BDSStorageProcessor storageProcessor = new BDSStorageProcessor(properties); + final File incomingDirectoryData = createOriginalDataInDir(); + final ExperimentPE baseExperiment = createExperiment(); + final DataSetInformation dataSetInformation = createDataSetInformation(); + prepareMailClient(HCSImageFormatV1_0.HCS_IMAGE_1_0); + final File storeData = + storageProcessor.storeData(baseExperiment, dataSetInformation, TYPE_EXTRACTOR, + mailClient, incomingDirectoryData, workingDirectory); + logRecorder.resetLogContent(); + final File originalDataSet = storageProcessor.tryGetProprietaryData(storeData); + assertEquals(null, originalDataSet); + assertEquals("WARN OPERATION.BDSStorageProcessor - " + "Original data are not available.", + logRecorder.getLogContent()); + + context.assertIsSatisfied(); + } + + @Test + public void testConstructorWithInvalidFormat() throws Exception + { + final Properties properties = createProperties(new Format("bla", new Version(1, 2), "v")); + try + { + new BDSStorageProcessor(properties); + fail("ConfigurationFailureException expected"); + } catch (final ConfigurationFailureException e) + { + assertEquals("Property 'format': no valid and known format could be extracted " + + "from text 'bla V1.2'.", e.getMessage()); + } + } + + private void prepareMailClient(final Format format) + { + if (format != UnknownFormatV1_0.UNKNOWN_1_0) + { + context.checking(new Expectations() + { + { + one(mailClient) + .sendMessage( + "Incomplete data set 'NEMO.EXP1==CP001A-3AB'", + "Incomplete data set 'NEMO.EXP1==CP001A-3AB': " + + "3455 image file(s) are missing (locations: " + + "[[well=[x=16,y=1],tile=[x=3,y=3]], [well=[x=16,y=2],tile=[x=2,y=3]], " + + "[well=[x=16,y=3],tile=[x=1,y=3]], [well=[x=24,y=10],tile=[x=1,y=3]], " + + "[well=[x=24,y=9],tile=[x=2,y=3]], [well=[x=24,y=8],tile=[x=3,y=3]], " + + "[well=[x=7,y=6],tile=[x=1,y=2]], [well=[x=7,y=5],tile=[x=2,y=2]], " + + "[well=[x=7,y=4],tile=[x=3,y=2]], [well=[x=14,y=6],tile=[x=3,y=1]], " + + "... (3445 left)])", null, EXAMPLE_EMAIL); + } + }); + } + } + + // + // Helper classes + // + + public final static class TestImageFileExtractor implements IHCSImageFileExtractor + { + + public TestImageFileExtractor(final Properties properties) + { + } + + // + // IHCSImageFileExtractor + // + + public final HCSImageFileExtractionResult process( + final IDirectory incomingDataSetDirectory, + final DataSetInformation dataSetInformation, final IHCSImageFileAccepter accepter) + { + assertEquals(INCOMING_DATA_SET_DIR, incomingDataSetDirectory.getName()); + final List<IFile> listFiles = incomingDataSetDirectory.listFiles(null, true); + assertEquals(1, listFiles.size()); + final IFile file = listFiles.get(0); + assertEquals(ORIGINAL_DATA_TXT, file.getName()); + accepter.accept(1, new Location(1, 1), new Location(1, 1), file); + return new HCSImageFileExtractionResult(0, listFiles.size(), new ArrayList<IFile>(), + Collections.singleton(new Channel(1, 123))); + } + } + +} diff --git a/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/ChannelSetHelperTest.java b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/ChannelSetHelperTest.java new file mode 100644 index 00000000000..ce583e2ae41 --- /dev/null +++ b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/ChannelSetHelperTest.java @@ -0,0 +1,68 @@ +/* + * Copyright 2008 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.fail; + +import org.testng.annotations.Test; + +/** + * Test cases for corresponding {@link ChannelSetHelper} class. + * + * @author Christian Ribeaud + */ +public final class ChannelSetHelperTest +{ + + @Test + public final void testAddWavelength() + { + final int wavelength = 123; + final ChannelSetHelper helper = new ChannelSetHelper(); + helper.addWavelength(wavelength); + assertEquals(wavelength, helper.getChannelSet().iterator().next().getWavelength()); + try + { + helper.addWavelength(456); + fail("ChannelSetHelper is locked."); + } catch (final AssertionError e) + { + // Nothing to do here. + } + } + + @Test + public final void testGetChannelForWavelength() + { + final ChannelSetHelper helper = new ChannelSetHelper(); + helper.addWavelength(456); + helper.addWavelength(893); + helper.addWavelength(1); + assertEquals(1, helper.getChannelForWavelength(1).getCounter()); + assertEquals(2, helper.getChannelForWavelength(456).getCounter()); + assertEquals(3, helper.getChannelForWavelength(893).getCounter()); + try + { + helper.getChannelForWavelength(2); + fail("Given wavelength unknown."); + } catch (final AssertionError e) + { + // Nothing to do here. + } + } +} diff --git a/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/CodeExtractortTestCase.java b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/CodeExtractortTestCase.java new file mode 100644 index 00000000000..c733030a528 --- /dev/null +++ b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/CodeExtractortTestCase.java @@ -0,0 +1,44 @@ +/* + * Copyright 2008 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +/** + * @author Franz-Josef Elmer + */ +public abstract class CodeExtractortTestCase +{ + protected static final String PREFIX = IDataSetInfoExtractor.EXTRACTOR_KEY + "."; + + protected static final String ENTITY_SEPARATOR = + PREFIX + DefaultDataSetInfoExtractor.ENTITY_SEPARATOR_PROPERTY_NAME; + + protected static final String INDEX_OF_SAMPLE_CODE = + PREFIX + DefaultDataSetInfoExtractor.INDEX_OF_SAMPLE_CODE; + + protected static final String INDEX_OF_PARENT_DATA_SET_CODE = + PREFIX + DefaultDataSetInfoExtractor.INDEX_OF_PARENT_DATA_SET_CODE; + + protected static final String INDEX_OF_DATA_PRODUCER_CODE = + PREFIX + DefaultDataSetInfoExtractor.INDEX_OF_DATA_PRODUCER_CODE; + + protected static final String INDEX_OF_DATA_PRODUCTION_DATE = + PREFIX + DefaultDataSetInfoExtractor.INDEX_OF_DATA_PRODUCTION_DATE; + + protected static final String DATA_PRODUCTION_DATE_FORMAT = + PREFIX + DefaultDataSetInfoExtractor.DATA_PRODUCTION_DATE_FORMAT; + +} diff --git a/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/DataSetNameEntitiesProviderTest.java b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/DataSetNameEntitiesProviderTest.java new file mode 100644 index 00000000000..c747491fa75 --- /dev/null +++ b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/DataSetNameEntitiesProviderTest.java @@ -0,0 +1,94 @@ +/* + * Copyright 2008 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.fail; + +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import ch.systemsx.cisd.common.exceptions.UserFailureException; + +/** + * Test cases for corresponding {@link DataSetNameEntitiesProvider} class. + * + * @author Franz-Josef Elmer + */ +public class DataSetNameEntitiesProviderTest +{ + private static final String ALPHA = "alpha"; + + private static final String BETA = "beta"; + + private static final String GAMMA = "gamma"; + + private DataSetNameEntitiesProvider provider; + + @BeforeMethod + public void setup() + { + char separator = '.'; + provider = + new DataSetNameEntitiesProvider(ALPHA + separator + BETA + separator + GAMMA, + separator, false); + } + + @Test + public void testGetEntityWithValidIndex() + { + assertEquals(ALPHA, provider.getEntity(0)); + assertEquals(BETA, provider.getEntity(1)); + assertEquals(GAMMA, provider.getEntity(2)); + } + + @Test + public void testGetEntityWithValidNegativeIndex() + { + assertEquals(ALPHA, provider.getEntity(-3)); + assertEquals(BETA, provider.getEntity(-2)); + assertEquals(GAMMA, provider.getEntity(-1)); + } + + @Test + public void testGetEntityWithInvalidPositiveIndex() + { + try + { + provider.getEntity(3); + fail("UserFailureException expected"); + } catch (UserFailureException e) + { + assertEquals("Invalid data set name 'alpha.beta.gamma'. " + + "We need 4 entities, separated by '.', but got only 3.", e.getMessage()); + } + } + + @Test + public void testGetEntityWithInvalidNegativeIndex() + { + try + { + provider.getEntity(-4); + fail("UserFailureException expected"); + } catch (UserFailureException e) + { + assertEquals("Invalid data set name 'alpha.beta.gamma'. " + + "We need 4 entities, separated by '.', but got only 3.", e.getMessage()); + } + } +} diff --git a/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/DataStrategyStoreTest.java b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/DataStrategyStoreTest.java new file mode 100644 index 00000000000..d2fde9547c7 --- /dev/null +++ b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/DataStrategyStoreTest.java @@ -0,0 +1,261 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertTrue; + +import java.io.File; + +import org.apache.log4j.Level; +import org.jmock.Expectations; +import org.jmock.Mockery; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import ch.systemsx.cisd.common.filesystem.AbstractFileSystemTestCase; +import ch.systemsx.cisd.common.logging.BufferedAppender; +import ch.systemsx.cisd.common.mail.IMailClient; +import ch.systemsx.cisd.openbis.generic.shared.dto.ExperimentPE; +import ch.systemsx.cisd.openbis.generic.shared.dto.GroupPE; +import ch.systemsx.cisd.openbis.generic.shared.dto.InvalidationPE; +import ch.systemsx.cisd.openbis.generic.shared.dto.PersonPE; +import ch.systemsx.cisd.openbis.generic.shared.dto.ProjectPE; +import ch.systemsx.cisd.openbis.generic.shared.dto.SamplePropertyPE; +import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.SampleIdentifier; + +/** + * Test cases for corresponding {@link DataStrategyStore} class. + * + * @author Christian Ribeaud + */ +public final class DataStrategyStoreTest extends AbstractFileSystemTestCase +{ + private static final String EXPERIMENT_CODE = "E"; + + private static final String PROJECT_CODE = "P"; + + private static final String GROUP_CODE = "G"; + + private Mockery context; + + private IEncapsulatedLimsService limsService; + + private IMailClient mailClient; + + private DataStrategyStore dataStrategyStore; + + private BufferedAppender logRecorder; + + private final static ExperimentPE createBaseExperiment() + { + final ExperimentPE baseExperiment = new ExperimentPE(); + baseExperiment.setCode(EXPERIMENT_CODE); + final ProjectPE project = new ProjectPE(); + project.setCode(PROJECT_CODE); + final GroupPE group = new GroupPE(); + group.setCode(GROUP_CODE); + project.setGroup(group); + baseExperiment.setProject(project); + return baseExperiment; + } + + private final File createIncomingDataSetPath() + { + return new File(workingDirectory, "Twain"); + } + + @BeforeMethod + public void startup() + { + logRecorder = new BufferedAppender("%-5p %c - %m%n", Level.DEBUG); + } + + @AfterMethod + public final void tearDown() + { + logRecorder.reset(); + // The following line of code should also be called at the end of each test method. + // Otherwise one do not known which test failed. + context.assertIsSatisfied(); + } + + @BeforeClass + public final void beforeClass() + { + context = new Mockery(); + limsService = context.mock(IEncapsulatedLimsService.class); + mailClient = context.mock(IMailClient.class); + dataStrategyStore = new DataStrategyStore(limsService, mailClient); + } + + @Test + public final void testEasiestCases() + { + boolean exceptionThrown = false; + try + { + dataStrategyStore.getDataStoreStrategy(null, null); + } catch (final AssertionError ex) + { + exceptionThrown = true; + } + assertTrue("Null incoming data set path not permited", exceptionThrown); + final File incomingDataSetPath = createIncomingDataSetPath(); + final IDataStoreStrategy dataStoreStrategy = + dataStrategyStore.getDataStoreStrategy(null, incomingDataSetPath); + assertEquals(dataStoreStrategy.getKey(), DataStoreStrategyKey.UNIDENTIFIED); + } + + @Test + public final void testWithFullDataSetInfo() + { + final File incomingDataSetPath = createIncomingDataSetPath(); + final DataSetInformation dataSetInfo = IdentifiedDataStrategyTest.createDataSetInfo(); + final SampleIdentifier sampleIdentifier = dataSetInfo.getSampleIdentifier(); + final ExperimentPE baseExperiment = createBaseExperiment(); + context.checking(new Expectations() + { + { + one(limsService).getBaseExperiment(sampleIdentifier); + will(returnValue(baseExperiment)); + + one(limsService).getPropertiesOfTopSampleRegisteredFor(sampleIdentifier); + will(returnValue(new SamplePropertyPE[0])); + } + }); + final IDataStoreStrategy dataStoreStrategy = + dataStrategyStore.getDataStoreStrategy(dataSetInfo, incomingDataSetPath); + assertEquals(dataStoreStrategy.getKey(), DataStoreStrategyKey.IDENTIFIED); + context.assertIsSatisfied(); + } + + @Test + public final void testWithoutExperimentIdentifier() + { + final File incomingDataSetPath = createIncomingDataSetPath(); + final DataSetInformation dataSetInfo = IdentifiedDataStrategyTest.createDataSetInfo(); + dataSetInfo.setExperimentIdentifier(null); + final ExperimentPE baseExperiment = createBaseExperiment(); + final SampleIdentifier sampleIdentifier = dataSetInfo.getSampleIdentifier(); + context.checking(new Expectations() + { + { + one(limsService).getBaseExperiment(sampleIdentifier); + will(returnValue(baseExperiment)); + + one(limsService).getPropertiesOfTopSampleRegisteredFor(sampleIdentifier); + will(returnValue(new SamplePropertyPE[0])); + } + }); + final IDataStoreStrategy dataStoreStrategy = + dataStrategyStore.getDataStoreStrategy(dataSetInfo, incomingDataSetPath); + assertEquals(dataStoreStrategy.getKey(), DataStoreStrategyKey.IDENTIFIED); + context.assertIsSatisfied(); + } + + @Test + public final void testWithNullBaseExperiment() + { + final File incomingDataSetPath = createIncomingDataSetPath(); + final DataSetInformation dataSetInfo = IdentifiedDataStrategyTest.createDataSetInfo(); + context.checking(new Expectations() + { + { + one(limsService).getBaseExperiment(dataSetInfo.getSampleIdentifier()); + will(returnValue(null)); + } + }); + + final IDataStoreStrategy dataStoreStrategy = + dataStrategyStore.getDataStoreStrategy(dataSetInfo, incomingDataSetPath); + + assertEquals(dataStoreStrategy.getKey(), DataStoreStrategyKey.UNIDENTIFIED); + final String logContent = logRecorder.getLogContent(); + assertEquals("Unexpected log content: " + logContent, true, logContent + .startsWith("ERROR NOTIFY.DataStrategyStore")); + + context.assertIsSatisfied(); + } + + @Test + public final void testWithInvalidBaseExperiment() + { + final File incomingDataSetPath = createIncomingDataSetPath(); + final DataSetInformation dataSetInfo = IdentifiedDataStrategyTest.createDataSetInfo(); + final ExperimentPE baseExperiment = createBaseExperiment(); + baseExperiment.setInvalidation(new InvalidationPE()); + context.checking(new Expectations() + { + { + one(limsService).getBaseExperiment(dataSetInfo.getSampleIdentifier()); + will(returnValue(baseExperiment)); + } + }); + final IDataStoreStrategy dataStoreStrategy = + dataStrategyStore.getDataStoreStrategy(dataSetInfo, incomingDataSetPath); + assertEquals(dataStoreStrategy.getKey(), DataStoreStrategyKey.UNIDENTIFIED); + final String logContent = logRecorder.getLogContent(); + assertEquals("ERROR NOTIFY.DataStrategyStore - " + + "Data set for sample 'MY-INSTANCE:/S' can not be registered " + + "because experiment 'E' has been invalidated.", logContent); + + context.assertIsSatisfied(); + } + + @Test + public final void testSampleIsNotRegistered() + { + final File incomingDataSetPath = createIncomingDataSetPath(); + final DataSetInformation dataSetInfo = IdentifiedDataStrategyTest.createDataSetInfo(); + final ExperimentPE baseExperiment = createBaseExperiment(); + final PersonPE person = new PersonPE(); + final String email = "john.doe@freemail.org"; + person.setEmail(email); + baseExperiment.setRegistrator(person); + context.checking(new Expectations() + { + { + one(limsService).getBaseExperiment(dataSetInfo.getSampleIdentifier()); + will(returnValue(baseExperiment)); + + one(limsService).getPropertiesOfTopSampleRegisteredFor( + dataSetInfo.getSampleIdentifier()); + will(returnValue(null)); + + String replyTo = null; + one(mailClient).sendMessage( + with(equal(String.format(DataStrategyStore.SUBJECT_FORMAT, dataSetInfo + .getExperimentIdentifier()))), with(any(String.class)), + with(equal(replyTo)), with(equal(new String[] + { email }))); + } + }); + + final IDataStoreStrategy dataStoreStrategy = + dataStrategyStore.getDataStoreStrategy(dataSetInfo, incomingDataSetPath); + + assertEquals(dataStoreStrategy.getKey(), DataStoreStrategyKey.INVALID); + final String logContent = logRecorder.getLogContent(); + assertEquals("Unexpected log content: " + logContent, true, logContent + .startsWith("ERROR OPERATION.DataStrategyStore")); + + context.assertIsSatisfied(); + } +} diff --git a/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/DefaultDataSetInfoExtractorTest.java b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/DefaultDataSetInfoExtractorTest.java new file mode 100644 index 00000000000..985e19f131a --- /dev/null +++ b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/DefaultDataSetInfoExtractorTest.java @@ -0,0 +1,155 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertNull; +import static org.testng.AssertJUnit.fail; + +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.Properties; + +import org.testng.annotations.Test; + +import ch.systemsx.cisd.common.exceptions.UserFailureException; + +/** + * Test cases for the {@link DefaultDataSetInfoExtractor}. + * + * @author Bernd Rinn + */ +public final class DefaultDataSetInfoExtractorTest extends CodeExtractortTestCase +{ + + @Test + public void testHappyCaseWithDefaultProperties() + { + final String barcode = "XYZ123"; + final IDataSetInfoExtractor extractor = new DefaultDataSetInfoExtractor(new Properties()); + + final DataSetInformation dsInfo = + extractor.getDataSetInformation(new File("bla.bla." + barcode)); + + assertNull(dsInfo.getExperimentIdentifier()); + assertEquals(barcode, dsInfo.getSampleIdentifier().getSampleCode()); + assertEquals(null, dsInfo.getParentDataSetCode()); + assertEquals(null, dsInfo.getProducerCode()); + assertEquals(null, dsInfo.getProductionDate()); + } + + @Test + public void testHappyCaseWithProducerCodeAndProductionDate() + { + final Properties properties = new Properties(); + properties.setProperty(INDEX_OF_DATA_PRODUCER_CODE, "-2"); + properties.setProperty(INDEX_OF_DATA_PRODUCTION_DATE, "0"); + final IDataSetInfoExtractor extractor = new DefaultDataSetInfoExtractor(properties); + final String producerCode = "M1"; + final String productionDate = "20070903181312"; + final String barcode = "XYZ123"; + + final DataSetInformation dsInfo = + extractor.getDataSetInformation(new File(productionDate + ".A.B." + producerCode + + "." + barcode)); + + assertEquals(barcode, dsInfo.getSampleIdentifier().getSampleCode()); + assertEquals(producerCode, dsInfo.getProducerCode()); + final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss"); + assertEquals(productionDate, dateFormat.format(dsInfo.getProductionDate())); + } + + @Test + public void testHappyCaseWithAllPropertiesSet() + { + final Properties properties = new Properties(); + final String separator = "="; + properties.setProperty(ENTITY_SEPARATOR, separator); + properties.setProperty(INDEX_OF_SAMPLE_CODE, "0"); + properties.setProperty(INDEX_OF_PARENT_DATA_SET_CODE, "1"); + properties.setProperty(INDEX_OF_DATA_PRODUCER_CODE, "-2"); + properties.setProperty(INDEX_OF_DATA_PRODUCTION_DATE, "-1"); + final String format = "yyyy-MM-dd HH:mm:ss"; + properties.setProperty(DATA_PRODUCTION_DATE_FORMAT, format); + final IDataSetInfoExtractor extractor = new DefaultDataSetInfoExtractor(properties); + final String producerCode = "M1"; + final String parentDataSetCode = "1234-8"; + final String productionDate = "2007-09-03 18:03:12"; + final String barcode = "XYZ-123"; + final DataSetInformation dsInfo = + extractor.getDataSetInformation(new File(barcode + separator + parentDataSetCode + + separator + "A" + separator + producerCode + separator + productionDate)); + assertEquals(barcode, dsInfo.getSampleIdentifier().getSampleCode()); + assertEquals(parentDataSetCode, dsInfo.getParentDataSetCode()); + assertEquals(producerCode, dsInfo.getProducerCode()); + final SimpleDateFormat dateFormat = new SimpleDateFormat(format); + assertEquals(productionDate, dateFormat.format(dsInfo.getProductionDate())); + } + + @Test + public void testWrongProductionDateFormat() + { + final Properties properties = new Properties(); + properties.setProperty(INDEX_OF_DATA_PRODUCTION_DATE, "0"); + final IDataSetInfoExtractor extractor = new DefaultDataSetInfoExtractor(properties); + try + { + extractor.getDataSetInformation(new File("blabla.XYZ-123")); + fail("UserFailureException expected"); + } catch (final UserFailureException e) + { + assertEquals("Could not parse data production date 'blabla' " + + "because it violates the following format: yyyyMMddHHmmss", e.getMessage()); + } + } + + @Test + public void testIndexTooLarge() + { + final Properties properties = new Properties(); + properties.setProperty(INDEX_OF_SAMPLE_CODE, "1"); + final IDataSetInfoExtractor extractor = new DefaultDataSetInfoExtractor(properties); + try + { + extractor.getDataSetInformation(new File("XYZ-123")); + fail("UserFailureException expected"); + } catch (final UserFailureException e) + { + assertEquals("Invalid data set name 'XYZ-123'. We need 2 entities, separated by '.', " + + "but got only 1.", e.getMessage()); + } + } + + @Test + public void testIndexTooSmall() + { + final Properties properties = new Properties(); + properties.setProperty(ENTITY_SEPARATOR, "-"); + properties.setProperty(INDEX_OF_SAMPLE_CODE, "-3"); + final IDataSetInfoExtractor extractor = new DefaultDataSetInfoExtractor(properties); + try + { + extractor.getDataSetInformation(new File("XYZ-123")); + fail("UserFailureException expected"); + } catch (final UserFailureException e) + { + assertEquals("Invalid data set name 'XYZ-123'. We need 3 entities, separated by '-', " + + "but got only 2.", e.getMessage()); + } + } + +} diff --git a/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/DefaultStorageProcessorTest.java b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/DefaultStorageProcessorTest.java new file mode 100644 index 00000000000..c0c42d7f3be --- /dev/null +++ b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/DefaultStorageProcessorTest.java @@ -0,0 +1,157 @@ +/* + * Copyright 2008 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.fail; + +import java.io.File; +import java.util.Properties; + +import org.testng.annotations.Test; + +import ch.systemsx.cisd.common.filesystem.AbstractFileSystemTestCase; +import ch.systemsx.cisd.openbis.generic.shared.dto.DataSetType; +import ch.systemsx.cisd.openbis.generic.shared.dto.FileFormatType; +import ch.systemsx.cisd.openbis.generic.shared.dto.LocatorType; +import ch.systemsx.cisd.openbis.generic.shared.dto.ProcedureType; +import ch.systemsx.cisd.openbis.generic.shared.dto.types.ProcedureTypeCode; + +/** + * Test cases for corresponding {@link DefaultStorageProcessor} class. + * + * @author Christian Ribeaud + */ +public final class DefaultStorageProcessorTest extends AbstractFileSystemTestCase +{ + + private final static IProcedureAndDataTypeExtractor TYPE_EXTRACTOR = + new TestProcedureAndDataTypeExtractor(); + + private final DefaultStorageProcessor createStorageProcessor() + { + final Properties properties = new Properties(); + final DefaultStorageProcessor storageProcessor = new DefaultStorageProcessor(properties); + storageProcessor.setStoreRootDirectory(workingDirectory); + return storageProcessor; + } + + private File createDirectory(final String directoryName) + { + final File file = new File(workingDirectory, directoryName); + file.mkdir(); + assertEquals(true, file.isDirectory()); + return file; + } + + @Test + public final void testStoreData() + { + final DefaultStorageProcessor storageProcessor = createStorageProcessor(); + try + { + storageProcessor.storeData(null, null, null, null, null, null); + fail("Null values not accepted"); + } catch (final AssertionError e) + { + // Nothing to do here. + } + final File incomingDataSetDirectory = createDirectory("incoming"); + final File rootDir = createDirectory("root"); + final File storeData = + storageProcessor.storeData(null, null, TYPE_EXTRACTOR, null, + incomingDataSetDirectory, rootDir); + assertEquals(false, incomingDataSetDirectory.exists()); + assertEquals(true, storeData.isDirectory()); + assertEquals(new File(new File(workingDirectory, "root"), "incoming").getAbsolutePath(), + storeData.getAbsolutePath()); + } + + @Test + public final void testGetStoreRootDirectory() + { + DefaultStorageProcessor storageProcessor = createStorageProcessor(); + File storeRootDirectory = storageProcessor.getStoreRootDirectory(); + assertEquals(workingDirectory.getAbsolutePath(), storeRootDirectory.getAbsolutePath()); + } + + @Test + public final void testUnstoreData() + { + final DefaultStorageProcessor storageProcessor = createStorageProcessor(); + try + { + storageProcessor.unstoreData(null, null); + fail("Null values not accepted"); + } catch (final AssertionError e) + { + // Nothing to do here. + } + final File root = createDirectory("root"); + final File incomingDataSetDirectory = createDirectory("incoming"); + final File storeData = + storageProcessor.storeData(null, null, TYPE_EXTRACTOR, null, + incomingDataSetDirectory, root); + assertEquals(true, storeData.exists()); + assertEquals(false, incomingDataSetDirectory.exists()); + storageProcessor.unstoreData(incomingDataSetDirectory, root); + assertEquals(false, storeData.exists()); + assertEquals(true, incomingDataSetDirectory.exists()); + } + + // + // Helper classes + // + + final static class TestProcedureAndDataTypeExtractor implements IProcedureAndDataTypeExtractor + { + + static final String PROCEDURE_TYPE = ProcedureTypeCode.DATA_ACQUISITION.getCode(); + + static final String DATA_SET_TYPE = "dataSetType"; + + static final String LOCATOR_TYPE = "locatorType"; + + static final String FILE_FORMAT_TYPE = "fileFormatType"; + + // + // IProcedureAndDataTypeExtractor + // + + public final FileFormatType getFileFormatType(final File incomingDataSetPath) + { + return new FileFormatType(FILE_FORMAT_TYPE); + } + + public final LocatorType getLocatorType(final File incomingDataSetPath) + { + return new LocatorType(LOCATOR_TYPE); + } + + public final DataSetType getDataSetType(final File incomingDataSetPath) + { + return new DataSetType(DATA_SET_TYPE); + } + + public final ProcedureType getProcedureType(final File incomingDataSetPath) + { + final ProcedureType procedureType = new ProcedureType(PROCEDURE_TYPE); + procedureType.setDataAcquisition(true); + return procedureType; + } + } +} diff --git a/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/EncapsulatedLimsServiceTest.java b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/EncapsulatedLimsServiceTest.java new file mode 100644 index 00000000000..a29896c3df1 --- /dev/null +++ b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/EncapsulatedLimsServiceTest.java @@ -0,0 +1,143 @@ +/* + * Copyright 2008 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import org.jmock.Expectations; +import org.jmock.Mockery; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import ch.systemsx.cisd.common.exceptions.InvalidSessionException; +import ch.systemsx.cisd.openbis.generic.shared.IETLLIMSService; +import ch.systemsx.cisd.openbis.generic.shared.dto.ExternalData; +import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.SampleIdentifier; +import ch.systemsx.cisd.openbis.generic.shared.dto.types.ProcedureTypeCode; + +/** + * Test cases for corresponding {@link EncapsulatedLimsService} class. + * + * @author Basil Neff + */ +public class EncapsulatedLimsServiceTest +{ + private Mockery context; + + private IETLLIMSService limsService; + + private IEncapsulatedLimsService encapsulatedLimsService; + + private static final String LIMS_USER = "testuser"; + + private static final String LIMS_PASSWORD = "testpassword"; + + private final DataSetInformation createDataSetInformation() + { + final DataSetInformation dataSetInformation = new DataSetInformation(); + dataSetInformation.setSampleCode("S1"); + return dataSetInformation; + } + + private void prepareCallGetBaseExperiment(final Expectations exp, + final DataSetInformation dataSetInformation) + { + exp.one(limsService).authenticate(LIMS_USER, LIMS_PASSWORD); + exp.one(limsService).tryToGetBaseExperiment("", dataSetInformation.getSampleIdentifier()); + } + + @BeforeMethod + public void setUp() + { + context = new Mockery(); + limsService = context.mock(IETLLIMSService.class); + encapsulatedLimsService = + new EncapsulatedLimsService(limsService, LIMS_USER, LIMS_PASSWORD); + } + + @AfterMethod + public void tearDown() + { + // To following line of code should also be called at the end of each test method. + // Otherwise one do not known which test failed. + context.assertIsSatisfied(); + } + + @Test + public final void testGetBaseExperimentReauthentificate() + { + final DataSetInformation dataSetInformation = createDataSetInformation(); + context.checking(new Expectations() + { + { + prepareCallGetBaseExperiment(this, dataSetInformation); + will(throwException(new InvalidSessionException("error"))); + prepareCallGetBaseExperiment(this, dataSetInformation); + } + }); + encapsulatedLimsService.getBaseExperiment(dataSetInformation.getSampleIdentifier()); + context.assertIsSatisfied(); + } + + @Test + public final void testGetBaseExperiment() + { + final DataSetInformation dataSetInformation = createDataSetInformation(); + context.checking(new Expectations() + { + { + prepareCallGetBaseExperiment(this, dataSetInformation); + } + }); + encapsulatedLimsService.getBaseExperiment(dataSetInformation.getSampleIdentifier()); + context.assertIsSatisfied(); + } + + @Test + public final void testRegisterDataSet() + { + final DataSetInformation dataSetInfo = createDataSetInformation(); + final ProcedureTypeCode procedureTypeCode = ProcedureTypeCode.DATA_ACQUISITION; + final ExternalData data = new ExternalData(); + context.checking(new Expectations() + { + { + one(limsService).authenticate(LIMS_USER, LIMS_PASSWORD); + one(limsService).registerDataSet("", dataSetInfo.getSampleIdentifier(), + procedureTypeCode.getCode(), data); + } + }); + encapsulatedLimsService.registerDataSet(dataSetInfo, procedureTypeCode.getCode(), data); + + context.assertIsSatisfied(); + } + + @Test + public final void testIsSampleRegisteredForDataSet() + { + final SampleIdentifier sampleIdentifier = SampleIdentifier.createHomeGroup(""); + context.checking(new Expectations() + { + { + one(limsService).authenticate(LIMS_USER, LIMS_PASSWORD); + one(limsService).tryToGetPropertiesOfTopSampleRegisteredFor("", + sampleIdentifier); + } + }); + encapsulatedLimsService.getPropertiesOfTopSampleRegisteredFor(sampleIdentifier); + context.assertIsSatisfied(); + } +} diff --git a/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/FileBasedFileTest.java b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/FileBasedFileTest.java new file mode 100644 index 00000000000..e034e39b564 --- /dev/null +++ b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/FileBasedFileTest.java @@ -0,0 +1,87 @@ +/* + * Copyright 2008 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import static org.testng.AssertJUnit.*; + +import java.io.File; + +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import ch.systemsx.cisd.common.TimingParameters; +import ch.systemsx.cisd.common.filesystem.FileUtilities; +import ch.systemsx.cisd.common.logging.LogInitializer; + +/** + * Test cases for corresponding {@link FileBasedFile} class. + * + * @author Franz-Josef Elmer + */ +public class FileBasedFileTest +{ + private static final File WORKING_DIRECTORY = + new File("targets/unit-test-wd/FileBasedFileTest"); + + private static final File DESTINATION = new File(WORKING_DIRECTORY, "destination"); + + @BeforeTest + public void setUp() + { + LogInitializer.init(); + FileUtilities.deleteRecursively(WORKING_DIRECTORY); + assertTrue(WORKING_DIRECTORY.mkdirs()); + assertTrue(DESTINATION.mkdirs()); + } + + @Test + public void copyFileUsingHardLinks() + { + File file = new File(WORKING_DIRECTORY, "test.txt"); + FileUtilities.writeToFile(file, "hello world!"); + File destFile = new File(DESTINATION, "copy_of_test.txt"); + IFile destinationFile = + new FileBasedFileFactory(true, TimingParameters.getNoTimeoutNoRetriesParameters()) + .create(destFile.getPath()); + + destinationFile.copyFrom(file); + + assertEquals("hello world!", FileUtilities.loadToString(destFile).trim()); + } + + @Test + public void copyDirectoryUsingHardLinks() + { + File folder = new File(WORKING_DIRECTORY, "folder"); + assertTrue(folder.mkdir()); + File file1 = new File(folder, "file1.txt"); + FileUtilities.writeToFile(file1, "hello file1"); + File file2 = new File(folder, "file2.txt"); + FileUtilities.writeToFile(file2, "hello file2"); + File destFolder = new File(DESTINATION, "copy_folder"); + IFile destinationFolder = + new FileBasedFileFactory(true, TimingParameters.createNoRetries(2000L)) + .create(destFolder.getPath()); + + destinationFolder.copyFrom(folder); + + assertEquals("hello file1", FileUtilities.loadToString( + new File(DESTINATION, "copy_folder/file1.txt")).trim()); + assertEquals("hello file2", FileUtilities.loadToString( + new File(DESTINATION, "copy_folder/file2.txt")).trim()); + } +} diff --git a/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/HCSImageCheckListTest.java b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/HCSImageCheckListTest.java new file mode 100644 index 00000000000..dbb413dd5b9 --- /dev/null +++ b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/HCSImageCheckListTest.java @@ -0,0 +1,108 @@ +/* + * Copyright 2008 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.fail; + +import org.testng.annotations.Test; + +import ch.systemsx.cisd.bds.hcs.Location; +import ch.systemsx.cisd.bds.hcs.PlateGeometry; +import ch.systemsx.cisd.bds.hcs.WellGeometry; + +/** + * Test cases for corresponding {@link HCSImageCheckList} class. + * + * @author Christian Ribeaud + */ +public final class HCSImageCheckListTest +{ + + private static final WellGeometry WELL_GEOMETRY = new WellGeometry(1, 2); + + private static final PlateGeometry PLATE_GEOMETRY = new PlateGeometry(2, 1); + + private final static HCSImageCheckList createImageCheckList() + { + return new HCSImageCheckList(1, PLATE_GEOMETRY, WELL_GEOMETRY); + } + + @Test + public final void testConstructor() + { + try + { + new HCSImageCheckList(0, null, null); + fail("IllegalArgumentException expected"); + } catch (final IllegalArgumentException e) + { + assertEquals("Number of channels smaller than one.", e.getMessage()); + } + try + { + new HCSImageCheckList(1, null, null); + fail("IllegalArgumentException expected"); + } catch (final IllegalArgumentException e) + { + assertEquals("Unspecified plate geometry.", e.getMessage()); + } + try + { + new HCSImageCheckList(1, PLATE_GEOMETRY, null); + fail("IllegalArgumentException expected"); + } catch (final IllegalArgumentException e) + { + assertEquals("Unspecified well geometry.", e.getMessage()); + } + new HCSImageCheckList(1, PLATE_GEOMETRY, WELL_GEOMETRY); + } + + @Test + public final void testCheckOff() + { + final HCSImageCheckList checkList = createImageCheckList(); + assertEquals(4, checkList.getCheckedOnFullLocations().size()); + try + { + checkList.checkOff(1, new Location(2, 1), new Location(1, 1)); + fail("Wrong well location."); + } catch (final IllegalArgumentException ex) + { + } + try + { + checkList.checkOff(1, new Location(1, 2), new Location(1, 2)); + fail("Wrong tile location."); + } catch (final IllegalArgumentException ex) + { + } + checkList.checkOff(1, new Location(1, 2), new Location(2, 1)); + assertEquals(3, checkList.getCheckedOnFullLocations().size()); + try + { + checkList.checkOff(1, new Location(1, 2), new Location(2, 1)); + fail("Image already handled."); + } catch (IllegalArgumentException ex) + { + } + checkList.checkOff(1, new Location(1, 1), new Location(1, 1)); + checkList.checkOff(1, new Location(1, 1), new Location(2, 1)); + checkList.checkOff(1, new Location(1, 2), new Location(1, 1)); + assertEquals(0, checkList.getCheckedOnFullLocations().size()); + } +} diff --git a/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/IdentifiedDataStrategyTest.java b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/IdentifiedDataStrategyTest.java new file mode 100644 index 00000000000..2e6aa25956a --- /dev/null +++ b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/IdentifiedDataStrategyTest.java @@ -0,0 +1,141 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertNotNull; +import static org.testng.AssertJUnit.assertTrue; +import static org.testng.AssertJUnit.fail; + +import java.io.File; +import java.io.IOException; + +import org.apache.commons.io.FileUtils; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import ch.systemsx.cisd.common.exceptions.EnvironmentFailureException; +import ch.systemsx.cisd.common.filesystem.AbstractFileSystemTestCase; +import ch.systemsx.cisd.openbis.generic.shared.dto.DataSetType; +import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.ExperimentIdentifier; +import ch.systemsx.cisd.openbis.generic.shared.dto.types.DataSetTypeCode; + +/** + * Test cases for corresponding {@link IdentifiedDataStrategy} class. + * + * @author Christian Ribeaud + */ +public class IdentifiedDataStrategyTest extends AbstractFileSystemTestCase +{ + private static final String EXAMPLE_PROJECT_CODE = "P"; + + private static final String EXAMPLE_EXPERIMENT_CODE = "E"; + + private final static String FILE_NAME = "AX14"; + + private final static IdentifiedDataStrategy strategy = new IdentifiedDataStrategy(); + + private final static DataSetType dataSetType = + new DataSetType(DataSetTypeCode.HCS_IMAGE.getCode()); + + private static final String EXAMPLE_GROUP_CODE = "G"; + + final static DataSetInformation createDataSetInfo() + { + final DataSetInformation dataSetInfo = new DataSetInformation(); + final ExperimentIdentifier experimentIdentifier = new ExperimentIdentifier(); + experimentIdentifier.setExperimentCode(EXAMPLE_EXPERIMENT_CODE); + experimentIdentifier.setProjectCode(EXAMPLE_PROJECT_CODE); + experimentIdentifier.setGroupCode(EXAMPLE_GROUP_CODE); + dataSetInfo.setExperimentIdentifier(experimentIdentifier); + dataSetInfo.setSampleCode("S"); + dataSetInfo.setInstanceCode("my-instance"); + dataSetInfo.setInstanceUUID("1111-2222"); + dataSetInfo.setDataSetCode("data-set-code"); + return dataSetInfo; + } + + // + // AbstractFileSystemTestCase + // + + @Override + @BeforeMethod + public void setUp() throws IOException + { + super.setUp(); + } + + @Test + public final void testGetBaseDirectory() throws IOException + { + boolean exceptionThrown = false; + try + { + strategy.getBaseDirectory(null, null, null); + } catch (final AssertionError ex) + { + exceptionThrown = true; + } + assertTrue("Null values not permited here", exceptionThrown); + final DataSetInformation dataSetInfo = createDataSetInfo(); + File baseDirectory = strategy.getBaseDirectory(workingDirectory, dataSetInfo, dataSetType); + final File file = + new File(workingDirectory, "Instance_1111-2222/Group_G/Project_P/Experiment_E/" + + "DataSetType_HCS_IMAGE/Sample_S/Dataset_data-set-code"); + assertEquals(file, baseDirectory); + assertTrue(baseDirectory.exists() == false); + // Create a file instead of a directory + FileUtils.touch(file); + try + { + strategy.getBaseDirectory(workingDirectory, dataSetInfo, dataSetType); + fail("illegal storage layout not detected"); + } catch (final EnvironmentFailureException ex) + { + assertTrue("Unexpected exception message: " + ex.getMessage(), ex.getMessage() + .startsWith(IdentifiedDataStrategy.STORAGE_LAYOUT_ERROR_MSG_PREFIX)); + } + FileUtils.forceDelete(file); + assert file.exists() == false; + // Create base directory + baseDirectory.mkdirs(); + assertTrue(baseDirectory.exists() && baseDirectory.isDirectory()); + try + { + baseDirectory = strategy.getBaseDirectory(workingDirectory, dataSetInfo, dataSetType); + fail("illegal storage layout not detected"); + } catch (final EnvironmentFailureException ex) + { + assertTrue("Unexpected exception message: " + ex.getMessage(), ex.getMessage() + .startsWith(IdentifiedDataStrategy.STORAGE_LAYOUT_ERROR_MSG_PREFIX)); + } + } + + @Test + public final void testGetTargetPath() + { + File file = new File(FILE_NAME); + File targetPath = strategy.getTargetPath(workingDirectory, file); + assertEquals(new File(workingDirectory, FILE_NAME), targetPath); + final String property = System.getProperty("java.io.tmpdir"); + assertNotNull(property); + file = new File(property, FILE_NAME); + targetPath = strategy.getTargetPath(workingDirectory, file); + assertEquals(new File(workingDirectory, FILE_NAME), targetPath); + } +} diff --git a/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/MainTest.java b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/MainTest.java new file mode 100644 index 00000000000..1b2fc09fbb7 --- /dev/null +++ b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/MainTest.java @@ -0,0 +1,136 @@ +/* + * Copyright 2008 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import static org.testng.AssertJUnit.assertFalse; +import static org.testng.AssertJUnit.assertTrue; + +import java.io.File; + +import org.testng.AssertJUnit; +import org.testng.annotations.Test; + +import ch.rinn.restrictions.Friend; +import ch.systemsx.cisd.common.filesystem.AbstractFileSystemTestCase; +import ch.systemsx.cisd.openbis.generic.shared.dto.DatabaseInstancePE; + +/** + * Test cases for corresponding {@link Main} class. + * + * @author Christian Ribeaud + */ +@Friend(toClasses = Main.class) +public final class MainTest extends AbstractFileSystemTestCase +{ + + private final static DatabaseInstancePE createDatabaseInstance() + { + final DatabaseInstancePE databaseInstancePE = new DatabaseInstancePE(); + databaseInstancePE.setCode("XXX"); + databaseInstancePE.setUuid("1111-2222"); + return databaseInstancePE; + } + + @Test + public final void testMigrateStoreRootDir() + { + final File instanceDir = + new File( + new File(workingDirectory, IdentifiedDataStrategy.INSTANCE_PREFIX + "CISD"), + IdentifiedDataStrategy.GROUP_PREFIX + "CISD"); + instanceDir.mkdirs(); + assertTrue(instanceDir.exists()); + final DatabaseInstancePE databaseInstancePE = createDatabaseInstance(); + // Not same code + Main.migrateStoreRootDir(workingDirectory, databaseInstancePE); + assertTrue(instanceDir.exists()); + databaseInstancePE.setCode("CISD"); + // Same code + Main.migrateStoreRootDir(workingDirectory, databaseInstancePE); + assertFalse(instanceDir.exists()); + assertTrue(new File(workingDirectory, IdentifiedDataStrategy.INSTANCE_PREFIX + + databaseInstancePE.getUuid()).exists()); + // Trying again does not change anything + Main.migrateStoreRootDir(workingDirectory, databaseInstancePE); + assertFalse(instanceDir.exists()); + assertTrue(new File(workingDirectory, IdentifiedDataStrategy.INSTANCE_PREFIX + + databaseInstancePE.getUuid()).exists()); + } + + @Test + public void testMigrateDataStoreByRenamingObservableTypeToDataSetType() throws Exception + { + String observableTypeValue = "DST1"; + String observableTypeDirPrefix = "ObservableType_"; + File instanceDir = + new File(workingDirectory, IdentifiedDataStrategy.INSTANCE_PREFIX + "I1"); + File groupDir = new File(instanceDir, IdentifiedDataStrategy.GROUP_PREFIX + "G1"); + File projectDir = new File(groupDir, IdentifiedDataStrategy.PROJECT_PREFIX + "P1"); + File experimentDir = new File(projectDir, IdentifiedDataStrategy.EXPERIMENT_PREFIX + "E1"); + File observableTypeDir = + new File(experimentDir, observableTypeDirPrefix + observableTypeValue); + File sampleDir = new File(observableTypeDir, IdentifiedDataStrategy.SAMPLE_PREFIX + "S1"); + File dataSetDir = new File(sampleDir, IdentifiedDataStrategy.DATASET_PREFIX + "D1"); + File metadataDir = new File(dataSetDir, "metadata"); + File metadataDataSetDir = new File(metadataDir, "data_set"); + + // + // Don't break when directory does not exist + // + + Main.migrateDataStoreByRenamingObservableTypeToDataSetType(workingDirectory); + + // + // Rename ObservableType_<> directory and observable_type file + // + + // create directories + metadataDataSetDir.mkdirs(); + assertTrue(metadataDataSetDir.exists()); + assertTrue(observableTypeDir.getName() + .equals(observableTypeDirPrefix + observableTypeValue)); + + // create files + String observableTypeFileName = "observable_type"; + File observableTypeFile = new File(metadataDataSetDir, observableTypeFileName); + observableTypeFile.createNewFile(); + assertTrue(observableTypeFile.exists()); + AssertJUnit.assertEquals(observableTypeFileName, metadataDataSetDir.listFiles()[0] + .getName()); + assertTrue(observableTypeFile.getName().equals(observableTypeFileName)); + + // Do the migration + Main.migrateDataStoreByRenamingObservableTypeToDataSetType(workingDirectory); + + // check directory renamed + AssertJUnit.assertEquals(IdentifiedDataStrategy.DATA_SET_TYPE_PREFIX + observableTypeValue, + experimentDir.listFiles()[0].getName()); + + // update variables + observableTypeDir = + new File(experimentDir, IdentifiedDataStrategy.DATA_SET_TYPE_PREFIX + + observableTypeValue); + sampleDir = new File(observableTypeDir, IdentifiedDataStrategy.SAMPLE_PREFIX + "S1"); + dataSetDir = new File(sampleDir, IdentifiedDataStrategy.DATASET_PREFIX + "D1"); + metadataDir = new File(dataSetDir, "metadata"); + metadataDataSetDir = new File(metadataDir, "data_set"); + + // check file renamed + AssertJUnit.assertEquals("data_set_type", metadataDataSetDir.listFiles()[0].getName()); + } + +} diff --git a/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/NamedDataStrategyTest.java b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/NamedDataStrategyTest.java new file mode 100644 index 00000000000..d4750df591a --- /dev/null +++ b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/NamedDataStrategyTest.java @@ -0,0 +1,127 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertTrue; + +import java.io.File; +import java.io.IOException; + +import org.apache.commons.io.FileUtils; +import org.testng.annotations.Test; + +import ch.systemsx.cisd.common.filesystem.AbstractFileSystemTestCase; +import ch.systemsx.cisd.openbis.generic.shared.dto.DataSetType; + +/** + * Test cases for corresponding {@link NamedDataStrategy} class. + * + * @author Christian Ribeaud + */ +public final class NamedDataStrategyTest extends AbstractFileSystemTestCase +{ + private static final DataStoreStrategyKey UNIDENTIFIED = DataStoreStrategyKey.UNIDENTIFIED; + + private final static String FILE_NAME = "AX14"; + + private final static String TEST_FILENAME = "test"; + + private final static NamedDataStrategy strategy = new NamedDataStrategy(UNIDENTIFIED); + + private final void createSomeFiles() throws IOException + { + createNumberedFiles(workingDirectory); + assert workingDirectory.list().length == 3; + assert new File(workingDirectory, FILE_NAME + "_[1]").exists(); + } + + private final static void createNumberedFiles(final File dir) throws IOException + { + for (int i = 0; i < 3; i++) + { + FileUtils.touch(new File(dir, FILE_NAME + "_[" + (i + 1) + "]")); + } + } + + @Test + public void testCreateTargetPathNoFileExists() throws IOException + { + assertEquals(TEST_FILENAME, NamedDataStrategy.createTargetPath( + new File(workingDirectory, TEST_FILENAME)).getName()); + } + + @Test + public void testCreateTargetPathOriginalFileExists() throws IOException + { + FileUtils.touch(new File(workingDirectory, TEST_FILENAME)); + assertEquals(TEST_FILENAME + "_[1]", NamedDataStrategy.createTargetPath( + new File(workingDirectory, TEST_FILENAME)).getName()); + } + + @Test + public void testCreateTargetPathSomeFilesExist() throws IOException + { + FileUtils.touch(new File(workingDirectory, TEST_FILENAME)); + FileUtils.touch(new File(workingDirectory, TEST_FILENAME + "_[1]")); + FileUtils.touch(new File(workingDirectory, TEST_FILENAME + "_[2]")); + FileUtils.touch(new File(workingDirectory, TEST_FILENAME + "_[3]")); + assertEquals(TEST_FILENAME + "_[4]", NamedDataStrategy.createTargetPath( + new File(workingDirectory, TEST_FILENAME)).getName()); + } + + @Test + public final void testGetBaseDirectory() throws IOException + { + createSomeFiles(); + boolean exceptionThrown = false; + try + { + strategy.getBaseDirectory(null, null, null); + } catch (AssertionError e) + { + exceptionThrown = true; + } + assertTrue("Base directory can not be null", exceptionThrown); + final DataSetType dataSetType = new DataSetType("DataSet"); + final File baseDirectory = + strategy.getBaseDirectory(workingDirectory, null, dataSetType); + assertEquals(new File(new File(workingDirectory, NamedDataStrategy + .getDirectoryName(UNIDENTIFIED)), IdentifiedDataStrategy + .createDataSetTypeDirectory(dataSetType)), baseDirectory); + } + + @Test(dependsOnMethods = "testGetBaseDirectory") + public final void testGetTargetPath() throws IOException + { + createSomeFiles(); + boolean exceptionThrown = false; + try + { + strategy.getTargetPath(null, null); + } catch (AssertionError e) + { + exceptionThrown = true; + } + assertTrue("Base directory and incoming data set can not be null", exceptionThrown); + File targetPath = strategy.getTargetPath(workingDirectory, new File(FILE_NAME)); + assertEquals(new File(workingDirectory, FILE_NAME), targetPath); + FileUtils.touch(targetPath); + targetPath = strategy.getTargetPath(workingDirectory, new File(FILE_NAME)); + assertEquals(new File(workingDirectory, FILE_NAME + "_[4]"), targetPath); + } +} \ No newline at end of file diff --git a/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/SimpleTypeExtractorTest.java b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/SimpleTypeExtractorTest.java new file mode 100644 index 00000000000..3fd7bc8b613 --- /dev/null +++ b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/SimpleTypeExtractorTest.java @@ -0,0 +1,68 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import static org.testng.AssertJUnit.assertEquals; + +import java.util.Properties; + +import org.testng.annotations.Test; + +import ch.systemsx.cisd.openbis.generic.shared.dto.FileFormatType; +import ch.systemsx.cisd.openbis.generic.shared.dto.LocatorType; +import ch.systemsx.cisd.openbis.generic.shared.dto.types.DataSetTypeCode; +import ch.systemsx.cisd.openbis.generic.shared.dto.types.ProcedureTypeCode; + +/** + * Test cases for corresponding {@link SimpleTypeExtractor} class. + * + * @author Christian Ribeaud + */ +public class SimpleTypeExtractorTest +{ + + private final static Properties createProperties() + { + Properties props = new Properties(); + props.put(SimpleTypeExtractor.FILE_FORMAT_TYPE_KEY, "F"); + props.put(SimpleTypeExtractor.LOCATOR_TYPE_KEY, "L"); + props.put(SimpleTypeExtractor.DATA_SET_TYPE_KEY, "O"); + props.put(SimpleTypeExtractor.PROCEDURE_TYPE_KEY, ProcedureTypeCode.DATA_ACQUISITION + .getCode()); + return props; + } + + @Test + public final void testConstructor() + { + SimpleTypeExtractor extractor = new SimpleTypeExtractor(new Properties()); + assertEquals(extractor.getFileFormatType(null).getCode(), + FileFormatType.DEFAULT_FILE_FORMAT_TYPE_CODE); + assertEquals(extractor.getLocatorType(null).getCode(), + LocatorType.DEFAULT_LOCATOR_TYPE_CODE); + assertEquals(extractor.getDataSetType(null).getCode(), DataSetTypeCode.HCS_IMAGE + .getCode()); + assertEquals(extractor.getProcedureType(null).getCode(), ProcedureTypeCode.DATA_ACQUISITION + .getCode()); + extractor = new SimpleTypeExtractor(createProperties()); + assertEquals("F", extractor.getFileFormatType(null).getCode()); + assertEquals("L", extractor.getLocatorType(null).getCode()); + assertEquals("O", extractor.getDataSetType(null).getCode()); + assertEquals(ProcedureTypeCode.DATA_ACQUISITION.getCode(), extractor.getProcedureType(null) + .getCode()); + } +} \ No newline at end of file diff --git a/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/StandardProcessingFactoryTest.java b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/StandardProcessingFactoryTest.java new file mode 100644 index 00000000000..e91f484cf16 --- /dev/null +++ b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/StandardProcessingFactoryTest.java @@ -0,0 +1,127 @@ +/* + * Copyright 2008 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.fail; + +import java.util.Properties; + +import org.testng.annotations.Test; + +import ch.systemsx.cisd.common.exceptions.ConfigurationFailureException; + +/** + * Test cases for the {@link StandardProcessorFactory}. + * + * @author Bernd Rinn + */ +public class StandardProcessingFactoryTest +{ + + @Test + public void testCreateStandardProcessingFactoryWithMissingInputDataSetFormat() + { + try + { + final Properties props = new Properties(); + props.put("parameters-file", "parameters.dat"); + props.put("finished-file-template", ".finished_{0}"); + StandardProcessorFactory.create(props); + fail("Missing property not detected"); + } catch (ConfigurationFailureException ex) + { + assertEquals("Given key 'input-storage-format' not found in properties" + + " '[parameters-file, finished-file-template]'", ex.getMessage()); + } + } + + @Test + public void testCreateStandardProcessingFactoryWithIllegalInputDataSetFormat() + { + try + { + final Properties props = new Properties(); + props.put("input-storage-format", "ILLEGAL"); + props.put("parameters-file", "parameters.dat"); + props.put("finished-file-template", ".finished_{0}"); + StandardProcessorFactory.create(props); + fail("Illegal property value not detected"); + } catch (ConfigurationFailureException ex) + { + assertEquals("input-storage-format property has illegal value 'ILLEGAL'.", ex + .getMessage()); + } + } + + @Test + public void testCreateStandardProcessingFactoryWithInputDataSetFormatProprietary() + { + final Properties props = new Properties(); + props.put("input-storage-format", "PROPRIETARY"); + props.put("parameters-file", "parameters.dat"); + props.put("finished-file-template", ".finished_{0}"); + props.put("data-set-code-prefix-glue", "_"); + StandardProcessorFactory.create(props); + } + + @Test + public void testCreateStandardProcessingFactoryWithInputDataSetFormatBdsDirectory() + { + final Properties props = new Properties(); + props.put("input-storage-format", "BDS_DIRECTORY"); + props.put("parameters-file", "parameters.dat"); + props.put("finished-file-template", ".finished_{0}"); + props.put("data-set-code-prefix-glue", "_"); + StandardProcessorFactory.create(props); + } + + @Test + public void testCreateStandardProcessingFactoryWithMissingParametersFileProperty() + { + try + { + final Properties props = new Properties(); + props.put("input-storage-format", "BDS_DIRECTORY"); + props.put("finished-file-template", ".finished_{0}"); + StandardProcessorFactory.create(props); + fail("Missing property not detected"); + } catch (ConfigurationFailureException ex) + { + assertEquals("Given key 'parameters-file' not found in properties" + + " '[input-storage-format, finished-file-template]'", ex.getMessage()); + } + } + + @Test + public void testCreateStandardProcessingFactoryWithMissingFinishedFileTemplateProperty() + { + try + { + final Properties props = new Properties(); + props.put("input-storage-format", "BDS_DIRECTORY"); + props.put("parameters-file", "parameters.dat"); + StandardProcessorFactory.create(props); + fail("Missing property not detected"); + } catch (ConfigurationFailureException ex) + { + assertEquals("Given key 'finished-file-template' not found in properties" + + " '[parameters-file, input-storage-format]'", ex.getMessage()); + } + } + +} diff --git a/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/StandardProcessorTest.java b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/StandardProcessorTest.java new file mode 100644 index 00000000000..a1ff51e182e --- /dev/null +++ b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/StandardProcessorTest.java @@ -0,0 +1,209 @@ +/* + * Copyright 2008 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertTrue; +import static org.testng.AssertJUnit.fail; + +import java.io.File; +import java.io.IOException; + +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.jmock.Expectations; +import org.jmock.Mockery; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import ch.systemsx.cisd.common.exceptions.ConfigurationFailureException; +import ch.systemsx.cisd.common.filesystem.AbstractFileSystemTestCase; +import ch.systemsx.cisd.common.filesystem.PathPrefixPrepender; +import ch.systemsx.cisd.openbis.generic.shared.dto.ProcessingInstructionDTO; +import ch.systemsx.cisd.openbis.generic.shared.dto.StorageFormat; + +/** + * Test cases for the {@link StandardProcessor}. + * + * @author Christian Ribeaud + */ +public final class StandardProcessorTest extends AbstractFileSystemTestCase +{ + private final static class AbsolutPathMatcher extends BaseMatcher<String> + { + private final String absolutePath; + + AbsolutPathMatcher(final String absolutePath) + { + this.absolutePath = absolutePath; + } + + // + // BaseMatcher + // + + public final void describeTo(final Description description) + { + description.appendText(absolutePath); + } + + public final boolean matches(final Object item) + { + if (item instanceof String == false) + { + return false; + } + final String path = (String) item; + return path.replace('\\', '/').equals(absolutePath.replace('\\', '/')); + } + } + + private static final String PROCESSING_PATH = "processing"; + + private static final String PREFIX_FOR_RELATIVE_PATH = "rel"; + + private static final String PREFIX_FOR_ABSOLUTE_PATH = null; + + private static final String FINISHED_FILE_NAME_TEMPLATE = ".MARKER_is_finished_{0}"; + + private static final String PARAMETERS_FILE_NAME = "parameters"; + + private IProcessor processor; + + private Mockery context; + + private IFileFactory fileFactory; + + private PathPrefixPrepender pathPrefixPrepender; + + private IFile iFile; + + @Override + @BeforeMethod + public final void setUp() throws IOException + { + super.setUp(); + context = new Mockery(); + fileFactory = context.mock(IFileFactory.class); + iFile = context.mock(IFile.class); + pathPrefixPrepender = createPathPrefixPrepender(); + processor = createStandardProcessor(); + } + + @AfterMethod + public void tearDown() + { + // The following line of code should also be called at the end of each test method. + // Otherwise one do not known which test failed. + context.assertIsSatisfied(); + } + + private final PathPrefixPrepender createPathPrefixPrepender() + { + final File file = new File(workingDirectory, PREFIX_FOR_RELATIVE_PATH); + assertEquals(true, file.mkdir()); + assertTrue(file.exists()); + assertTrue(file.isDirectory()); + return new PathPrefixPrepender(PREFIX_FOR_ABSOLUTE_PATH, file.getAbsolutePath()); + } + + private final IProcessor createStandardProcessor() + { + return new StandardProcessor(fileFactory, StorageFormat.PROPRIETARY, pathPrefixPrepender, + PARAMETERS_FILE_NAME, FINISHED_FILE_NAME_TEMPLATE, "_"); + } + + private final ProcessingInstructionDTO createProcessingInstruction() + { + final ProcessingInstructionDTO processingInstruction = new ProcessingInstructionDTO(); + processingInstruction.setPath(PROCESSING_PATH); + return processingInstruction; + } + + @Test + public final void testInitiateProcessingWithNullParameters() + { + try + { + processor.initiateProcessing(null, null, null); + fail("Null parameters not allowed here."); + } catch (final AssertionError ex) + { + // Nothing to do here. + } + context.assertIsSatisfied(); + } + + @Test + public final void testInitiateProcessingWithNotSuitableDirectory() + { + final File dataSet = new File("dataSet"); + final String absolutePath = + new File(new File(workingDirectory, PREFIX_FOR_RELATIVE_PATH), PROCESSING_PATH) + .getAbsolutePath(); + context.checking(new Expectations() + { + { + one(fileFactory).create(with(new AbsolutPathMatcher(absolutePath))); + will(returnValue(iFile)); + + one(iFile).check(); + will(throwException(new ConfigurationFailureException(""))); + } + }); + try + { + processor.initiateProcessing(createProcessingInstruction(), new DataSetInformation(), + dataSet); + fail("Configuration failure exception."); + } catch (final ConfigurationFailureException ex) + { + // Nothing to do here. + } + context.assertIsSatisfied(); + } + + @Test + public final void testInitiateProcessing() + { + final String dataSetName = "dataSet"; + final File dataSet = new File(dataSetName); + final String absolutePath = + new File(new File(workingDirectory, PREFIX_FOR_RELATIVE_PATH), PROCESSING_PATH) + .getAbsolutePath(); + final DataSetInformation dataSetInformation = new DataSetInformation(); + final String dataSetCode = "data-set-code"; + dataSetInformation.setDataSetCode(dataSetCode); + final String dataSetFullName = dataSetCode + "_" + dataSetName; + context.checking(new Expectations() + { + { + one(fileFactory).create(with(new AbsolutPathMatcher(absolutePath))); + will(returnValue(iFile)); + + one(iFile).check(); + + one(fileFactory).create(iFile, dataSetFullName); + one(fileFactory).create(iFile, ".MARKER_is_finished_" + dataSetFullName); + } + }); + processor.initiateProcessing(createProcessingInstruction(), dataSetInformation, dataSet); + context.assertIsSatisfied(); + } + +} \ No newline at end of file diff --git a/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/ThreadParametersTest.java b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/ThreadParametersTest.java new file mode 100644 index 00000000000..f36cd5c7625 --- /dev/null +++ b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/ThreadParametersTest.java @@ -0,0 +1,48 @@ +/* + * Copyright 2008 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertNull; + +import java.util.Properties; + +import org.testng.annotations.Test; + +import ch.rinn.restrictions.Friend; + +/** + * Test cases for the {@link ThreadParameters}. + * + * @author Christian Ribeaud + */ +@Friend(toClasses = ThreadParameters.class) +public final class ThreadParametersTest +{ + + @Test + public final void testTryGetGroupCode() + { + final Properties properties = new Properties(); + assertNull(ThreadParameters.tryGetGroupCode(properties)); + properties.setProperty(ThreadParameters.GROUP_CODE_KEY, ""); + assertNull(ThreadParameters.tryGetGroupCode(properties)); + final String groupCode = "G1"; + properties.setProperty(ThreadParameters.GROUP_CODE_KEY, groupCode); + assertEquals(groupCode, ThreadParameters.tryGetGroupCode(properties)); + } +} diff --git a/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/TransferredDataSetHandlerTest.java b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/TransferredDataSetHandlerTest.java new file mode 100644 index 00000000000..24f2d60a65d --- /dev/null +++ b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/TransferredDataSetHandlerTest.java @@ -0,0 +1,905 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver; + +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertTrue; +import static org.testng.AssertJUnit.fail; + +import java.io.File; +import java.io.IOException; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Level; +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.jmock.Expectations; +import org.jmock.Mockery; +import org.jmock.api.ExpectationError; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import ch.rinn.restrictions.Friend; +import ch.systemsx.cisd.common.Constants; +import ch.systemsx.cisd.common.exceptions.ConfigurationFailureException; +import ch.systemsx.cisd.common.exceptions.EnvironmentFailureException; +import ch.systemsx.cisd.common.exceptions.UserFailureException; +import ch.systemsx.cisd.common.filesystem.AbstractFileSystemTestCase; +import ch.systemsx.cisd.common.filesystem.QueueingPathRemoverService; +import ch.systemsx.cisd.common.logging.BufferedAppender; +import ch.systemsx.cisd.common.logging.LogCategory; +import ch.systemsx.cisd.common.logging.LogInitializer; +import ch.systemsx.cisd.common.mail.IMailClient; +import ch.systemsx.cisd.common.mail.JavaMailProperties; +import ch.systemsx.cisd.common.test.LogMonitoringAppender; +import ch.systemsx.cisd.common.utilities.OSUtilities; +import ch.systemsx.cisd.openbis.generic.shared.IETLLIMSService; +import ch.systemsx.cisd.openbis.generic.shared.dto.DataSetType; +import ch.systemsx.cisd.openbis.generic.shared.dto.DatabaseInstancePE; +import ch.systemsx.cisd.openbis.generic.shared.dto.ExperimentPE; +import ch.systemsx.cisd.openbis.generic.shared.dto.ExternalData; +import ch.systemsx.cisd.openbis.generic.shared.dto.FileFormatType; +import ch.systemsx.cisd.openbis.generic.shared.dto.GroupPE; +import ch.systemsx.cisd.openbis.generic.shared.dto.LocatorType; +import ch.systemsx.cisd.openbis.generic.shared.dto.PersonPE; +import ch.systemsx.cisd.openbis.generic.shared.dto.ProcedureType; +import ch.systemsx.cisd.openbis.generic.shared.dto.ProcessingInstructionDTO; +import ch.systemsx.cisd.openbis.generic.shared.dto.ProjectPE; +import ch.systemsx.cisd.openbis.generic.shared.dto.SamplePropertyPE; +import ch.systemsx.cisd.openbis.generic.shared.dto.StorageFormat; +import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.ExperimentIdentifier; +import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.SampleIdentifier; +import ch.systemsx.cisd.openbis.generic.shared.dto.types.ProcedureTypeCode; + +/** + * Test cases for corresponding {@link TransferredDataSetHandler} class. + * + * @author Franz-Josef Elmer + */ +@Friend(toClasses = TransferredDataSetHandler.class) +public final class TransferredDataSetHandlerTest extends AbstractFileSystemTestCase +{ + + private static final String SAMPLE_CODE = "sample1"; + + private static final String LOG_MSG_OF_DATA_FORMAT_MISMATCH = + "Configuration Error: no processing initiated for data set"; + + private static final String FOLDER_NAME = "folder"; + + private static final String DATA2_NAME = "data2"; + + private static final String DATA1_NAME = "data1"; + + private static final String SESSION_TOKEN = "sessionToken"; + + private static final String DATA_SET_CODE = "4711-42"; + + private static final String PARENT_DATA_SET_CODE = "4711-1"; + + private static final ProcedureType PROCEDURE_TYPE = + new ProcedureType(ProcedureTypeCode.DATA_ACQUISITION.getCode()); + + private static final LocatorType LOCATOR_TYPE = new LocatorType("L1"); + + private static final DataSetType DATA_SET_TYPE = new DataSetType("O1"); + + private static final FileFormatType FILE_FORMAT_TYPE = new FileFormatType("FF1"); + + private static final String DATA_PRODUCER_CODE = "microscope"; + + private static final Date DATA_PRODUCTION_DATE = new Date(2001); + + private static final String EXAMPLE_PROCEDURE_TYPE_CODE = + ProcedureTypeCode.DATA_ACQUISITION.getCode(); + + private static final class ExternalDataMatcher extends BaseMatcher<ExternalData> + { + private final ExternalData expectedData; + + public ExternalDataMatcher(final ExternalData externalData) + { + this.expectedData = externalData; + } + + public void describeTo(final Description description) + { + description.appendValue(expectedData); + } + + public boolean matches(final Object item) + { + if (item instanceof ExternalData == false) + { + return false; + } + final ExternalData data = (ExternalData) item; + assertEquals(expectedData.getCode(), data.getCode()); + assertEquals(expectedData.getDataProducerCode(), data.getDataProducerCode()); + assertEquals(expectedData.getLocation(), data.getLocation()); + assertEquals(expectedData.getLocatorType(), data.getLocatorType()); + assertEquals(expectedData.getFileFormatType(), data.getFileFormatType()); + assertEquals(expectedData.getDataSetType(), data.getDataSetType()); + assertEquals(expectedData.getParentDataSetCode(), data.getParentDataSetCode()); + assertEquals(expectedData.getProductionDate(), data.getProductionDate()); + assertEquals(expectedData.getStorageFormat(), data.getStorageFormat()); + return true; + } + + // This method throws an ExpectationError instead of the usual AssertionError. + // Reason: TransferredDataSetHandler catches AssertionError. + private void assertEquals(final Object expected, final Object actual) + { + if (expected == null ? expected != actual : expected.equals(actual) == false) + { + throw new ExpectationError("Expecting <" + expected + "> but got <" + actual + ">", + this); + } + } + } + + /** + * This wrapper is needed because the class name of the wrapped class is not known. + */ + private static final class MockDataSetInfoExtractor implements IDataSetInfoExtractor + { + private final IDataSetInfoExtractor codeExtractor; + + MockDataSetInfoExtractor(final IDataSetInfoExtractor codeExtractor) + { + this.codeExtractor = codeExtractor; + } + + public DataSetInformation getDataSetInformation(final File incomingDataSetPath) + throws UserFailureException, EnvironmentFailureException + { + return codeExtractor.getDataSetInformation(incomingDataSetPath); + } + } + + private Mockery context; + + private IDataSetInfoExtractor dataSetInfoExtractor; + + private IProcedureAndDataTypeExtractor typeExtractor; + + private IStorageProcessor storageProcessor; + + private IETLLIMSService limsService; + + private TransferredDataSetHandler handler; + + private File data1; + + private File isFinishedData1; + + private DataSetInformation dataSetInformation; + + private String relativeTargetFolder; + + private ExternalData targetData1; + + private File folder; + + private File isFinishedFolder; + + private File data2; + + private IMailClient mailClient; + + private IProcessorFactory processorFactory; + + private IProcessor processor; + + private BufferedAppender logRecorder; + + private DatabaseInstancePE homeDatabaseInstance; + + @BeforeTest + public void init() + { + QueueingPathRemoverService.start(); + } + + @AfterTest + public void finish() + { + QueueingPathRemoverService.stop(); + } + + @Override + @BeforeMethod + public final void setUp() throws IOException + { + super.setUp(); + LogInitializer.init(); + data1 = new File(workingDirectory, DATA1_NAME); + FileUtils.touch(data1); + isFinishedData1 = + new File(workingDirectory, Constants.IS_FINISHED_PREFIX + data1.getName()); + FileUtils.touch(isFinishedData1); + + folder = new File(workingDirectory, FOLDER_NAME); + folder.mkdir(); + data2 = new File(folder, DATA2_NAME); + FileUtils.touch(data2); + isFinishedFolder = + new File(workingDirectory, Constants.IS_FINISHED_PREFIX + folder.getName()); + FileUtils.touch(isFinishedFolder); + + context = new Mockery(); + dataSetInfoExtractor = context.mock(IDataSetInfoExtractor.class); + typeExtractor = context.mock(IProcedureAndDataTypeExtractor.class); + final Properties properties = new Properties(); + properties.setProperty(JavaMailProperties.MAIL_SMTP_HOST, "host"); + properties.setProperty(JavaMailProperties.MAIL_FROM, "me"); + properties.setProperty(Main.STOREROOT_DIR_KEY, workingDirectory.getPath()); + storageProcessor = context.mock(IStorageProcessor.class); + limsService = context.mock(IETLLIMSService.class); + mailClient = context.mock(IMailClient.class); + processorFactory = context.mock(IProcessorFactory.class); + processor = context.mock(IProcessor.class); + final Map<String, IProcessorFactory> map = new HashMap<String, IProcessorFactory>(); + map.put(EXAMPLE_PROCEDURE_TYPE_CODE, processorFactory); + final IETLServerPlugin plugin = + new ETLServerPlugin(new MockDataSetInfoExtractor(dataSetInfoExtractor), + typeExtractor, storageProcessor); + final IEncapsulatedLimsService authorizedLimsService = + new EncapsulatedLimsService(limsService, "u", "p"); + handler = + new TransferredDataSetHandler(null, storageProcessor, plugin, + authorizedLimsService, mailClient, true); + + handler.setProcessorFactories(map); + dataSetInformation = new DataSetInformation(); + final ExperimentIdentifier experimentIdentifier = new ExperimentIdentifier(); + experimentIdentifier.setExperimentCode("experiment1".toUpperCase()); + experimentIdentifier.setProjectCode("project1".toUpperCase()); + experimentIdentifier.setGroupCode("group1".toUpperCase()); + homeDatabaseInstance = new DatabaseInstancePE(); + homeDatabaseInstance.setCode("my-instance"); + homeDatabaseInstance.setUuid("1111-2222"); + dataSetInformation.setInstanceCode(homeDatabaseInstance.getCode()); + dataSetInformation.setInstanceUUID(homeDatabaseInstance.getUuid()); + dataSetInformation.setExperimentIdentifier(experimentIdentifier); + dataSetInformation.setSampleCode(SAMPLE_CODE); + dataSetInformation.setProducerCode(DATA_PRODUCER_CODE); + dataSetInformation.setProductionDate(DATA_PRODUCTION_DATE); + dataSetInformation.setDataSetCode(DATA_SET_CODE); + dataSetInformation.setParentDataSetCode(PARENT_DATA_SET_CODE); + relativeTargetFolder = + "Instance_1111-2222" + File.separator + "Group_" + + experimentIdentifier.getGroupCode() + File.separator + "Project_" + + experimentIdentifier.getProjectCode() + File.separator + "Experiment_" + + experimentIdentifier.getExperimentCode() + File.separator + + "DataSetType_" + DATA_SET_TYPE.getCode() + File.separator + "Sample_" + + dataSetInformation.getSampleIdentifier().getSampleCode() + File.separator + + "Dataset_" + DATA_SET_CODE; + targetData1 = createTargetData(data1); + logRecorder = new BufferedAppender("%-5p %c - %m%n", Level.INFO); + } + + private final String createLogMsgOfSuccess(final ExperimentIdentifier identifier, + final SampleIdentifier sampleIdentifier) + { + return String.format(TransferredDataSetHandler.SUCCESSFULLY_REGISTERED_TEMPLATE, + DATA_SET_CODE, sampleIdentifier, DATA_SET_TYPE.getCode(), identifier); + } + + private final void assertLog(final String expectedLog) + { + assertEquals(expectedLog, normalize(logRecorder.getLogContent())); + } + + private final String normalize(final String message) + { + return message.replace(workingDirectory.getAbsolutePath(), "/<wd>").replace( + workingDirectory.getPath(), "<wd>").replace('\\', '/'); + } + + private final ExternalData createTargetData(final File dataSet) + { + final ExternalData data = new ExternalData(); + data.setLocation(relativeTargetFolder + File.separator + dataSet.getName()); + data.setLocatorType(LOCATOR_TYPE); + data.setDataSetType(DATA_SET_TYPE); + data.setFileFormatType(FILE_FORMAT_TYPE); + data.setStorageFormat(StorageFormat.BDS_DIRECTORY); + data.setDataProducerCode(DATA_PRODUCER_CODE); + data.setProductionDate(DATA_PRODUCTION_DATE); + data.setCode(DATA_SET_CODE); + data.setParentDataSetCode(PARENT_DATA_SET_CODE); + return data; + } + + private final static ExperimentPE createBaseExperiment( + final DataSetInformation dataSetInformation) + { + final ExperimentPE baseExperiment = new ExperimentPE(); + final ExperimentIdentifier experimentIdentifier = + dataSetInformation.getExperimentIdentifier(); + baseExperiment.setCode(experimentIdentifier.getExperimentCode()); + final GroupPE group = new GroupPE(); + group.setCode(experimentIdentifier.getGroupCode()); + final ProjectPE project = new ProjectPE(); + project.setCode(experimentIdentifier.getProjectCode()); + project.setGroup(group); + baseExperiment.setProject(project); + final PersonPE person = new PersonPE(); + person.setEmail("john.doe@somewhere.com"); + baseExperiment.setRegistrator(person); + baseExperiment.setProcessingInstructions(new ProcessingInstructionDTO[] + { create() }); + return baseExperiment; + } + + private final static ProcessingInstructionDTO create() + { + final ProcessingInstructionDTO processingInstruction = new ProcessingInstructionDTO(); + processingInstruction.setProcedureTypeCode(EXAMPLE_PROCEDURE_TYPE_CODE); + return processingInstruction; + } + + @AfterMethod + public void tearDown() + { + logRecorder.reset(); + // The following line of code should also be called at the end of each test method. + // Otherwise one do not known which test failed. + context.assertIsSatisfied(); + } + + private final void prepareForStrategy(final File dataSet, final ExperimentPE baseExperiment) + { + context.checking(new Expectations() + { + { + one(dataSetInfoExtractor).getDataSetInformation(dataSet); + will(returnValue(dataSetInformation)); + + one(limsService).authenticate("u", "p"); + will(returnValue(SESSION_TOKEN)); + + one(limsService).getHomeDatabaseInstance(SESSION_TOKEN); + will(returnValue(homeDatabaseInstance)); + + one(storageProcessor).getStoreRootDirectory(); + will(returnValue(workingDirectory)); + + atLeast(1).of(limsService).tryToGetBaseExperiment(SESSION_TOKEN, + dataSetInformation.getSampleIdentifier()); + will(returnValue(baseExperiment)); + + allowing(typeExtractor).getDataSetType(dataSet); + will(returnValue(DATA_SET_TYPE)); + } + }); + } + + private final void prepareForStrategyIDENTIFIED(final File dataSet, + final ExternalData targetData, final ExperimentPE baseExperiment) + { + prepareForStrategy(dataSet, baseExperiment); + context.checking(new Expectations() + { + { + one(limsService).tryToGetPropertiesOfTopSampleRegisteredFor(SESSION_TOKEN, + dataSetInformation.getSampleIdentifier()); + will(returnValue(new SamplePropertyPE[0])); + } + }); + } + + private final void prepareForRegistration(final File dataSet) + { + context.checking(new Expectations() + { + { + one(typeExtractor).getLocatorType(dataSet); + will(returnValue(LOCATOR_TYPE)); + + one(typeExtractor).getFileFormatType(dataSet); + will(returnValue(FILE_FORMAT_TYPE)); + + one(typeExtractor).getProcedureType(dataSet); + will(returnValue(PROCEDURE_TYPE)); + } + }); + } + + private final String getNotificationEmailContent(final DataSetInformation dataset, + final String dataSetCode) + { + final StringBuffer stringBuffer = new StringBuffer(); + stringBuffer.append(createLogMsgOfSuccess(dataset.getExperimentIdentifier(), dataset + .getSampleIdentifier()) + + OSUtilities.LINE_SEPARATOR + OSUtilities.LINE_SEPARATOR); + stringBuffer.append("Experiment Identifier:\t" + + dataSetInformation.getExperimentIdentifier() + OSUtilities.LINE_SEPARATOR); + stringBuffer.append("Producer Code:\t" + dataset.getProducerCode() + + OSUtilities.LINE_SEPARATOR); + if (dataset.getProductionDate() != null) + { + stringBuffer.append("Production Date:\t" + dataset.getProductionDate() + + OSUtilities.LINE_SEPARATOR); + } + if (StringUtils.isNotBlank(dataset.getParentDataSetCode())) + { + stringBuffer.append("Parent Data Set:\t" + dataset.getParentDataSetCode() + + OSUtilities.LINE_SEPARATOR); + } + stringBuffer.append("Is complete:\t" + dataset.getIsCompleteFlag() + + OSUtilities.LINE_SEPARATOR); + + return stringBuffer.toString(); + } + + private final void checkSuccessEmailNotification(final Expectations expectations, + final DataSetInformation dataSet, final String dataSetCode, final String recipient) + { + expectations.one(mailClient).sendMessage( + String.format(TransferredDataSetHandler.EMAIL_SUBJECT_TEMPLATE, dataSet + .getExperimentIdentifier().getExperimentCode()), + getNotificationEmailContent(dataSet, dataSetCode), null, recipient); + } + + @Test + public final void testDataSetFileIsReadOnly() + { + data1.setReadOnly(); + context.checking(new Expectations() + { + { + one(storageProcessor).getStoreRootDirectory(); + will(returnValue(workingDirectory)); + } + }); + + try + { + handler.handle(isFinishedData1); + fail("EnvironmentFailureException expected"); + } catch (final EnvironmentFailureException e) + { + final String normalizedMessage = normalize(e.getMessage()); + assertEquals("Error moving path 'data1' from '<wd>' to '<wd>': " + + "Incoming data set directory '<wd>/data1' is not writable.", + normalizedMessage); + } + + context.assertIsSatisfied(); + } + + @Test + public final void testNoDataSetInfoCouldBeExtractedFromDataSetFileName() + { + context.checking(new Expectations() + { + { + one(dataSetInfoExtractor).getDataSetInformation(data1); + will(returnValue(new DataSetInformation())); + } + }); + + try + { + handler.handle(isFinishedData1); + fail("ConfigurationFailureException expected."); + } catch (final ConfigurationFailureException e) + { + final String normalizedMessage = normalize(e.getMessage()); + assertEquals("Data Set Information Extractor 'MockDataSetInfoExtractor' extracted " + + "no sample code for incoming data set '<wd>/data1' " + + "(extractor contract violation).", normalizedMessage); + } + + context.assertIsSatisfied(); + } + + @Test + public final void testBaseDirectoryCouldNotBeCreated() throws IOException + { + FileUtils.touch(new File(workingDirectory, + "Instance_1111-2222/Group_GROUP1/Project_PROJECT1/Experiment_EXPERIMENT1/" + + "DataSetType_O1/Sample_" + SAMPLE_CODE + "/Dataset_" + DATA_SET_CODE)); + prepareForStrategyIDENTIFIED(data1, null, createBaseExperiment(dataSetInformation)); + try + { + handler.handle(isFinishedData1); + fail("Base directory could not be created because" + + " there is already a file with the same name."); + } catch (final EnvironmentFailureException ex) + { + assertTrue(ex.getMessage().indexOf( + IdentifiedDataStrategy.STORAGE_LAYOUT_ERROR_MSG_PREFIX) > -1); + } + assertLog("INFO OPERATION.DataStrategyStore - " + + "Identified that database knows experiment '/GROUP1/PROJECT1/EXPERIMENT1' " + + "and sample 'MY-INSTANCE:/sample1'."); + context.assertIsSatisfied(); + } + + @Test + public final void testMoveIdentifiedDataSetFile() + { + final ExperimentPE baseExperiment = createBaseExperiment(dataSetInformation); + baseExperiment.setProcessingInstructions(new ProcessingInstructionDTO[] + { create() }); + final File baseDir = new File(workingDirectory, relativeTargetFolder); + prepareForStrategyIDENTIFIED(data1, targetData1, baseExperiment); + prepareForRegistration(data1); + context.checking(new Expectations() + { + { + one(limsService).registerDataSet(with(equal(SESSION_TOKEN)), + with(equal(dataSetInformation.getSampleIdentifier())), + with(equal(PROCEDURE_TYPE.getCode())), + with(new ExternalDataMatcher(targetData1))); + + checkSuccessEmailNotification(this, dataSetInformation, DATA_SET_CODE, + baseExperiment.getRegistrator().getEmail()); + + allowing(storageProcessor).getStorageFormat(); + will(returnValue(StorageFormat.BDS_DIRECTORY)); + one(storageProcessor).storeData(baseExperiment, dataSetInformation, + typeExtractor, mailClient, data1, baseDir); + final File finalDataSetPath = new File(baseDir, DATA1_NAME); + will(returnValue(finalDataSetPath)); + + one(processorFactory).createProcessor(); + will(returnValue(processor)); + allowing(processor).getRequiredInputDataFormat(); + will(returnValue(StorageFormat.BDS_DIRECTORY)); + one(processor).initiateProcessing( + baseExperiment.getProcessingInstructions()[0], dataSetInformation, + finalDataSetPath); + } + }); + final LogMonitoringAppender appender = + LogMonitoringAppender.addAppender(LogCategory.OPERATION, String + .format(createLogMsgOfSuccess(dataSetInformation.getExperimentIdentifier(), + dataSetInformation.getSampleIdentifier()))); + handler.handle(isFinishedData1); + final File dataSetPath = + new File(baseDir.getParentFile(), IdentifiedDataStrategy.DATASET_PREFIX + + DATA_SET_CODE); + assertEquals(true, dataSetPath.isDirectory()); + appender.verifyLogHasHappened(); + context.assertIsSatisfied(); + } + + @Test + public final void testMoveIdentifiedDataSetFileToBDSContainerWithOriginalData() + { + final ExperimentPE baseExperiment = createBaseExperiment(dataSetInformation); + baseExperiment.setProcessingInstructions(new ProcessingInstructionDTO[] + { create() }); + final File baseDir = new File(workingDirectory, relativeTargetFolder); + prepareForStrategyIDENTIFIED(data1, targetData1, baseExperiment); + prepareForRegistration(data1); + context.checking(new Expectations() + { + { + one(limsService).registerDataSet(with(equal(SESSION_TOKEN)), + with(equal(dataSetInformation.getSampleIdentifier())), + with(equal(PROCEDURE_TYPE.getCode())), + with(new ExternalDataMatcher(targetData1))); + + checkSuccessEmailNotification(this, dataSetInformation, DATA_SET_CODE, + baseExperiment.getRegistrator().getEmail()); + + allowing(storageProcessor).getStorageFormat(); + will(returnValue(StorageFormat.BDS_DIRECTORY)); + one(storageProcessor).storeData(baseExperiment, dataSetInformation, + typeExtractor, mailClient, data1, baseDir); + final File finalDataSetPath = new File(baseDir, DATA1_NAME); + will(returnValue(finalDataSetPath)); + + one(processorFactory).createProcessor(); + will(returnValue(processor)); + allowing(processor).getRequiredInputDataFormat(); + will(returnValue(StorageFormat.PROPRIETARY)); + one(storageProcessor).tryGetProprietaryData(finalDataSetPath); + final File finalOriginalDataSetPath = new File(finalDataSetPath, "original"); + will(returnValue(finalOriginalDataSetPath)); + one(processor).initiateProcessing( + baseExperiment.getProcessingInstructions()[0], dataSetInformation, + finalOriginalDataSetPath); + } + }); + final LogMonitoringAppender appender = + LogMonitoringAppender.addAppender(LogCategory.OPERATION, createLogMsgOfSuccess( + dataSetInformation.getExperimentIdentifier(), dataSetInformation + .getSampleIdentifier())); + handler.handle(isFinishedData1); + final File dataSetPath = + new File(baseDir.getParentFile(), IdentifiedDataStrategy.DATASET_PREFIX + + DATA_SET_CODE); + assertEquals(true, dataSetPath.isDirectory()); + appender.verifyLogHasHappened(); + context.assertIsSatisfied(); + } + + @Test + public final void testMoveIdentifiedDataSetFileButMismatchOfDataFormat() + { + final ExperimentPE baseExperiment = createBaseExperiment(dataSetInformation); + baseExperiment.setProcessingInstructions(new ProcessingInstructionDTO[] + { create() }); + final File baseDir = new File(workingDirectory, relativeTargetFolder); + targetData1.setStorageFormat(StorageFormat.PROPRIETARY); + prepareForStrategyIDENTIFIED(data1, targetData1, baseExperiment); + prepareForRegistration(data1); + context.checking(new Expectations() + { + { + one(limsService).registerDataSet(with(equal(SESSION_TOKEN)), + with(equal(dataSetInformation.getSampleIdentifier())), + with(equal(PROCEDURE_TYPE.getCode())), + with(new ExternalDataMatcher(targetData1))); + + checkSuccessEmailNotification(this, dataSetInformation, DATA_SET_CODE, + baseExperiment.getRegistrator().getEmail()); + + allowing(storageProcessor).getStorageFormat(); + will(returnValue(StorageFormat.PROPRIETARY)); + one(storageProcessor).storeData(baseExperiment, dataSetInformation, + typeExtractor, mailClient, data1, baseDir); + final File finalDataSetPath = new File(baseDir, DATA1_NAME); + will(returnValue(finalDataSetPath)); + + one(processorFactory).createProcessor(); + will(returnValue(processor)); + allowing(processor).getRequiredInputDataFormat(); + will(returnValue(StorageFormat.BDS_DIRECTORY)); + + } + }); + final LogMonitoringAppender appender = + LogMonitoringAppender.addAppender(LogCategory.NOTIFY, + LOG_MSG_OF_DATA_FORMAT_MISMATCH); + handler.handle(isFinishedData1); + final File dataSetPath = + new File(baseDir.getParentFile(), IdentifiedDataStrategy.DATASET_PREFIX + + DATA_SET_CODE); + assertEquals(true, dataSetPath.isDirectory()); + appender.verifyLogHasHappened(); + context.assertIsSatisfied(); + } + + @Test + public final void testMoveIdentifiedDataSetFileButMismatchOfDataFormat2() + { + final ExperimentPE baseExperiment = createBaseExperiment(dataSetInformation); + baseExperiment.setProcessingInstructions(new ProcessingInstructionDTO[] + { create() }); + final File baseDir = new File(workingDirectory, relativeTargetFolder); + prepareForStrategyIDENTIFIED(data1, targetData1, baseExperiment); + prepareForRegistration(data1); + context.checking(new Expectations() + { + { + one(limsService).registerDataSet(with(equal(SESSION_TOKEN)), + with(equal(dataSetInformation.getSampleIdentifier())), + with(equal(PROCEDURE_TYPE.getCode())), + with(new ExternalDataMatcher(targetData1))); + + checkSuccessEmailNotification(this, dataSetInformation, DATA_SET_CODE, + baseExperiment.getRegistrator().getEmail()); + + allowing(storageProcessor).getStorageFormat(); + will(returnValue(StorageFormat.BDS_DIRECTORY)); + one(storageProcessor).storeData(baseExperiment, dataSetInformation, + typeExtractor, mailClient, data1, baseDir); + final File finalDataSetPath = new File(baseDir, DATA1_NAME); + will(returnValue(finalDataSetPath)); + + one(storageProcessor).tryGetProprietaryData(finalDataSetPath); + will(returnValue(null)); + + one(processorFactory).createProcessor(); + will(returnValue(processor)); + + allowing(processor).getRequiredInputDataFormat(); + will(returnValue(StorageFormat.PROPRIETARY)); + + } + }); + final LogMonitoringAppender appender = + LogMonitoringAppender.addAppender(LogCategory.NOTIFY, + LOG_MSG_OF_DATA_FORMAT_MISMATCH); + handler.handle(isFinishedData1); + final File dataSetPath = + new File(baseDir.getParentFile(), IdentifiedDataStrategy.DATASET_PREFIX + + DATA_SET_CODE); + assertEquals(true, dataSetPath.isDirectory()); + appender.verifyLogHasHappened(); + context.assertIsSatisfied(); + } + + @Test + public final void testMoveInvalidDataSetFile() + { + assertEquals(new File(workingDirectory, DATA1_NAME), data1); + assertEquals(new File(new File(workingDirectory, FOLDER_NAME), DATA2_NAME), data2); + assert data1.exists() && data2.exists(); + final ExperimentPE baseExperiment = createBaseExperiment(dataSetInformation); + prepareForStrategy(data1, baseExperiment); + context.checking(new Expectations() + { + { + one(limsService).tryToGetPropertiesOfTopSampleRegisteredFor(SESSION_TOKEN, + dataSetInformation.getSampleIdentifier()); + will(returnValue(null)); + + final ExperimentIdentifier experimentIdentifier = + dataSetInformation.getExperimentIdentifier(); + final String subject = + String.format(DataStrategyStore.SUBJECT_FORMAT, experimentIdentifier); + final String body = + DataStrategyStore.createInvalidSampleCodeMessage(dataSetInformation); + final String email = baseExperiment.getRegistrator().getEmail(); + one(mailClient).sendMessage(subject, body, null, email); + } + }); + handler.handle(isFinishedData1); + + assertEquals(false, isFinishedData1.exists()); + assertLog("ERROR OPERATION.DataStrategyStore - " + + "Incoming data set '<wd>/data1' claims to belong to experiment " + + "'/GROUP1/PROJECT1/EXPERIMENT1' and sample identifier 'MY-INSTANCE:/" + + SAMPLE_CODE + + "', " + + "but according to the openBIS server there is no such sample for this experiment " + + "(it has maybe been invalidated?). We thus consider it invalid." + + OSUtilities.LINE_SEPARATOR + "INFO OPERATION.FileRenamer - " + + "Moving file 'data1' from '<wd>' to '<wd>/invalid/DataSetType_O1'."); + + context.assertIsSatisfied(); + } + + @Test + public final void testMoveUnidentifiedDataSetFile() + { + assertEquals(new File(workingDirectory, DATA1_NAME), data1); + assertEquals(new File(new File(workingDirectory, FOLDER_NAME), DATA2_NAME), data2); + assert data1.exists() && data2.exists(); + prepareForStrategy(data1, null); + final File toDir = + new File(new File(workingDirectory, NamedDataStrategy + .getDirectoryName(DataStoreStrategyKey.UNIDENTIFIED)), + IdentifiedDataStrategy.createDataSetTypeDirectory(DATA_SET_TYPE)); + + final LogMonitoringAppender appender = + LogMonitoringAppender.addAppender(LogCategory.OPERATION, "to '" + toDir + "'"); + handler.handle(isFinishedData1); + assertEquals(false, isFinishedData1.exists()); + appender.verifyLogHasHappened(); + + checkNamedStrategyDirectoryPresent(DataStoreStrategyKey.UNIDENTIFIED, data1); + + context.assertIsSatisfied(); + } + + @Test + public final void testMoveIdentifiedDataSetFolderButStoreDataFailed() + { + final ExperimentPE baseExperiment = createBaseExperiment(dataSetInformation); + final File baseDir = new File(workingDirectory, relativeTargetFolder); + prepareForStrategyIDENTIFIED(folder, targetData1, baseExperiment); + context.checking(new Expectations() + { + { + one(processorFactory).createProcessor(); + will(returnValue(processor)); + + one(typeExtractor).getProcedureType(folder); + will(returnValue(PROCEDURE_TYPE)); + + one(storageProcessor).storeData(baseExperiment, dataSetInformation, + typeExtractor, mailClient, folder, baseDir); + will(throwException(new Exception("Could store data by storage processor"))); + + one(storageProcessor).unstoreData(with(equal(folder)), with(equal(baseDir))); + } + }); + final LogMonitoringAppender appender = + LogMonitoringAppender.addAppender(LogCategory.OPERATION, createLogMsgOfSuccess( + dataSetInformation.getExperimentIdentifier(), dataSetInformation + .getSampleIdentifier())); + final LogMonitoringAppender appender2 = + LogMonitoringAppender.addAppender(LogCategory.NOTIFY, String.format( + TransferredDataSetHandler.DATA_SET_STORAGE_FAILURE_TEMPLATE, + dataSetInformation)); + handler.handle(isFinishedFolder); + + appender.verifyLogHasNotHappened(); + appender2.verifyLogHasHappened(); + + checkNamedStrategyDirectoryPresent(DataStoreStrategyKey.ERROR, folder); + + context.assertIsSatisfied(); + } + + private final void checkNamedStrategyDirectoryPresent(final DataStoreStrategyKey key, + final File dataSet) + { + final File strategyDirectory = + new File(workingDirectory, NamedDataStrategy.getDirectoryName(key)); + assertEquals(true, strategyDirectory.exists()); + final File dataSetTypeDir = + new File(strategyDirectory, IdentifiedDataStrategy + .createDataSetTypeDirectory(DATA_SET_TYPE)); + assertEquals(true, dataSetTypeDir.exists()); + final File targetFile = new File(dataSetTypeDir, dataSet.getName()); + assertEquals(true, targetFile.exists()); + assertEquals(false, dataSet.exists()); + } + + @Test + public void testMoveIdentifiedDataSetFolderButWebServiceRegistrationFailed() + { + final ExperimentPE baseExperiment = createBaseExperiment(dataSetInformation); + final File baseDir = new File(workingDirectory, relativeTargetFolder); + targetData1.setStorageFormat(null); + prepareForStrategyIDENTIFIED(folder, targetData1, baseExperiment); + prepareForRegistration(folder); + context.checking(new Expectations() + { + { + one(processorFactory).createProcessor(); + will(returnValue(processor)); + one(storageProcessor).storeData(baseExperiment, dataSetInformation, + typeExtractor, mailClient, folder, baseDir); + will(returnValue(new File(baseDir, DATA1_NAME))); + + one(limsService).registerDataSet(with(equal(SESSION_TOKEN)), + with(equal(dataSetInformation.getSampleIdentifier())), + with(equal(PROCEDURE_TYPE.getCode())), + with(new ExternalDataMatcher(targetData1))); + will(throwException(new EnvironmentFailureException( + "Could not register data set folder"))); + + one(storageProcessor).unstoreData(with(equal(folder)), with(equal(baseDir))); + one(storageProcessor).getStorageFormat(); + } + }); + final LogMonitoringAppender appender = + LogMonitoringAppender.addAppender(LogCategory.OPERATION, createLogMsgOfSuccess( + dataSetInformation.getExperimentIdentifier(), dataSetInformation + .getSampleIdentifier())); + final LogMonitoringAppender appender2 = + LogMonitoringAppender.addAppender(LogCategory.NOTIFY, String.format( + TransferredDataSetHandler.DATA_SET_REGISTRATION_FAILURE_TEMPLATE, + dataSetInformation)); + + handler.handle(isFinishedFolder); + + appender.verifyLogHasNotHappened(); + appender2.verifyLogHasHappened(); + + context.assertIsSatisfied(); + } +} \ No newline at end of file diff --git a/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/imsb/HCSImageFileExtractorTest.java b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/imsb/HCSImageFileExtractorTest.java new file mode 100644 index 00000000000..1c017bbdf2c --- /dev/null +++ b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/imsb/HCSImageFileExtractorTest.java @@ -0,0 +1,247 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver.imsb; + +import static org.testng.AssertJUnit.*; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +import org.apache.commons.io.FileUtils; +import org.apache.log4j.Level; +import org.jmock.Expectations; +import org.jmock.Mockery; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import ch.rinn.restrictions.Friend; +import ch.systemsx.cisd.bds.hcs.Location; +import ch.systemsx.cisd.bds.hcs.WellGeometry; +import ch.systemsx.cisd.bds.storage.IDirectory; +import ch.systemsx.cisd.bds.storage.IFile; +import ch.systemsx.cisd.bds.storage.filesystem.NodeFactory; +import ch.systemsx.cisd.common.filesystem.AbstractFileSystemTestCase; +import ch.systemsx.cisd.common.logging.BufferedAppender; +import ch.systemsx.cisd.etlserver.DataSetInformation; +import ch.systemsx.cisd.etlserver.IHCSImageFileAccepter; + +/** + * Test cases for the {@link HCSImageFileExtractor}. + * + * @author Christian Ribeaud + */ +@Friend(toClasses = HCSImageFileExtractor.class) +public final class HCSImageFileExtractorTest extends AbstractFileSystemTestCase +{ + + private final IDirectory workingDirectoryNode; + + public HCSImageFileExtractorTest() + { + super(); + this.workingDirectoryNode = NodeFactory.createDirectoryNode(workingDirectory); + } + + static class TestHCSImageFileExtractor extends HCSImageFileExtractor + { + + private List<IFile> files; + + @Override + List<IFile> listTiffFiles(final IDirectory directory) + { + return files; + } + + public TestHCSImageFileExtractor(final Properties properties) + { + super(properties); + } + + void setFiles(final List<IFile> files) + { + this.files = files; + } + + } + + private static final String WELL_GEOMETRY = "3x3"; + + private static final String SAMPLE_CODE = "CP042-1ab"; + + private final DataSetInformation dataSetInformation = createDataSetInformation(); + + private TestHCSImageFileExtractor fileExtractor; + + private Mockery context; + + private IHCSImageFileAccepter fileAccepter; + + private BufferedAppender logRecorder; + + private final void prepareFileExtractor() + { + context = new Mockery(); + fileAccepter = context.mock(IHCSImageFileAccepter.class); + logRecorder = new BufferedAppender("%m", Level.WARN); + fileExtractor = new TestHCSImageFileExtractor(createProperties()); + } + + private final static DataSetInformation createDataSetInformation() + { + final DataSetInformation dataSetInformation = new DataSetInformation(); + dataSetInformation.setSampleCode(SAMPLE_CODE); + return dataSetInformation; + } + + private final static Properties createProperties() + { + final Properties props = new Properties(); + props.setProperty(WellGeometry.WELL_GEOMETRY, WELL_GEOMETRY); + return props; + } + + private final IFile createFile(final String fileName) throws IOException + { + final File file = new File(workingDirectory, fileName); + FileUtils.touch(file); + assertTrue(file.exists()); + return NodeFactory.createFileNode(file); + } + + // + // AbstractFileSystemTestCase + // + + @Override + @BeforeMethod + public final void setUp() throws IOException + { + super.setUp(); + prepareFileExtractor(); + logRecorder.resetLogContent(); + } + + @AfterMethod + public final void tearDown() + { + // To following line of code should also be called at the end of each test method. + // Otherwise one do not known which test failed. + context.assertIsSatisfied(); + } + + @Test + public final void processWithNull() + { + try + { + fileExtractor.process(null, null, null); + fail("Null values not allowed here."); + } catch (final AssertionError ex) + { + // Nothing to do here. + } + context.assertIsSatisfied(); + } + + @Test + public final void testProcessWithIncorrectSample() throws IOException + { + final String imagePath = "CP002-2bc_H24_6_w460.tif"; + final List<IFile> files = new ArrayList<IFile>(); + fileExtractor.setFiles(files); + files.add(createFile(imagePath)); + assertEquals("", logRecorder.getLogContent()); + assertEquals(1, fileExtractor.process(NodeFactory.createDirectoryNode(workingDirectory), + dataSetInformation, fileAccepter).getInvalidFiles().size()); + } + + @Test + public final void testProcessHappyCase() throws IOException + { + final String imagePath1 = "Some_trash_" + SAMPLE_CODE + "_H24_6_w530.tif"; + final String imagePath2 = "Some_trash_" + SAMPLE_CODE + "_H24_6_w460.tif"; + final List<IFile> files = new ArrayList<IFile>(); + fileExtractor.setFiles(files); + final IFile file1 = createFile(imagePath1); + files.add(file1); + final IFile file2 = createFile(imagePath2); + files.add(file2); + final int channel1 = 2; + final int channel2 = 1; + final Location plateLocation = new Location(24, 8); + final Location wellLocation = new Location(3, 2); + context.checking(new Expectations() + { + { + one(fileAccepter).accept(channel1, plateLocation, wellLocation, file1); + one(fileAccepter).accept(channel2, plateLocation, wellLocation, file2); + } + }); + assertEquals("", logRecorder.getLogContent()); + final List<IFile> invalidFiles = + fileExtractor.process(workingDirectoryNode, dataSetInformation, fileAccepter) + .getInvalidFiles(); + assertEquals(0, invalidFiles.size()); + context.assertIsSatisfied(); + } + + @Test + public final void testProcessWithNotEnoughTokens() throws IOException + { + final String imagePath = "H24_6_w460.tif"; + final List<IFile> files = new ArrayList<IFile>(); + fileExtractor.setFiles(files); + final IFile file1 = createFile(imagePath); + files.add(file1); + createFile(imagePath); + fileExtractor.process(workingDirectoryNode, dataSetInformation, fileAccepter); + assertEquals(1, fileExtractor.process(workingDirectoryNode, dataSetInformation, fileAccepter) + .getInvalidFiles().size()); + } + + @Test + public final void testProcessWithIncorrectPlateCoordinate() throws IOException + { + final String imagePath = "Screening_" + SAMPLE_CODE + "_XX_6_w530.tiff"; + final List<IFile> files = new ArrayList<IFile>(); + fileExtractor.setFiles(files); + final IFile file1 = createFile(imagePath); + files.add(file1); + createFile(imagePath); + fileExtractor.process(workingDirectoryNode, dataSetInformation, fileAccepter); + assertEquals(1, fileExtractor.process(workingDirectoryNode, dataSetInformation, fileAccepter) + .getInvalidFiles().size()); + } + + @Test + public final void testProcessWithIncorrectWellCoordinate() throws IOException + { + final String imagePath = "Doesnt_matter_" + SAMPLE_CODE + "_H24_s6_w530.tif"; + final List<IFile> files = new ArrayList<IFile>(); + fileExtractor.setFiles(files); + final IFile file1 = createFile(imagePath); + files.add(file1); + fileExtractor.process(workingDirectoryNode, dataSetInformation, fileAccepter); + assertEquals(1, fileExtractor.process(workingDirectoryNode, dataSetInformation, fileAccepter) + .getInvalidFiles().size()); + } +} \ No newline at end of file diff --git a/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/threev/DataSetInfoExtractorForDataAcquisitionTest.java b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/threev/DataSetInfoExtractorForDataAcquisitionTest.java new file mode 100644 index 00000000000..bea0c12945e --- /dev/null +++ b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/threev/DataSetInfoExtractorForDataAcquisitionTest.java @@ -0,0 +1,119 @@ +/* + * Copyright 2008 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver.threev; + +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.fail; + +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.Properties; + +import org.testng.annotations.Test; + +import ch.systemsx.cisd.common.exceptions.ConfigurationFailureException; +import ch.systemsx.cisd.etlserver.CodeExtractortTestCase; +import ch.systemsx.cisd.etlserver.DataSetInformation; +import ch.systemsx.cisd.etlserver.IDataSetInfoExtractor; + +/** + * @author Franz-Josef Elmer + */ +public class DataSetInfoExtractorForDataAcquisitionTest extends CodeExtractortTestCase +{ + private static final String INDICES_OF_DATA_SET_CODE_ENTITIES = + PREFIX + DataSetInfoExtractorForDataAcquisition.INDICES_OF_DATA_SET_CODE_ENTITIES; + + private static final String DATA_SET_CODE_ENTITIES_GLUE = + PREFIX + AbstractDataSetInfoExtractorFor3V.DATA_SET_CODE_ENTITIES_GLUE; + + @Test + public void testHappyCaseWithOnlyMandatoryPorperties() + { + final Properties properties = new Properties(); + properties.setProperty(INDICES_OF_DATA_SET_CODE_ENTITIES, "1, 0"); + final IDataSetInfoExtractor extractor = + new DataSetInfoExtractorForDataAcquisition(properties); + + final DataSetInformation dataSetInfo = + extractor.getDataSetInformation(new File("alpha.42.beta")); + assertEquals("42.alpha", dataSetInfo.getDataSetCode()); + assertEquals("beta", dataSetInfo.getSampleIdentifier().getSampleCode()); + assertEquals(null, dataSetInfo.getParentDataSetCode()); + assertEquals(null, dataSetInfo.getProducerCode()); + assertEquals(null, dataSetInfo.getProductionDate()); + } + + @Test + public void testHappyCaseWithAllProperties() + { + final Properties properties = new Properties(); + properties.setProperty(INDICES_OF_DATA_SET_CODE_ENTITIES, "-1 -2"); + properties.setProperty(DATA_SET_CODE_ENTITIES_GLUE, "-"); + properties.setProperty(ENTITY_SEPARATOR, "_"); + properties.setProperty(INDEX_OF_SAMPLE_CODE, "0"); + properties.setProperty(INDEX_OF_PARENT_DATA_SET_CODE, "1"); + properties.setProperty(INDEX_OF_DATA_PRODUCER_CODE, "2"); + properties.setProperty(INDEX_OF_DATA_PRODUCTION_DATE, "3"); + final String dateFormat = "yyyy-MM-dd"; + properties.setProperty(DATA_PRODUCTION_DATE_FORMAT, dateFormat); + final IDataSetInfoExtractor extractor = + new DataSetInfoExtractorForDataAcquisition(properties); + + final String date = "2007-12-24"; + final DataSetInformation dataSetInfo = + extractor.getDataSetInformation(new File("a_b_c_" + date)); + assertEquals("2007-12-24-c", dataSetInfo.getDataSetCode()); + assertEquals("a", dataSetInfo.getSampleIdentifier().getSampleCode()); + assertEquals("b", dataSetInfo.getParentDataSetCode()); + assertEquals("c", dataSetInfo.getProducerCode()); + assertEquals(date, new SimpleDateFormat(dateFormat).format(dataSetInfo.getProductionDate())); + } + + @Test + public void testConstructorWithMissingMandatoryProperty() + { + try + { + new DataSetInfoExtractorForDataAcquisition(new Properties()); + fail("ConfigurationFailureException expected"); + } catch (final ConfigurationFailureException e) + { + final String message = e.getMessage(); + assertEquals( + "Given key 'indices-of-data-set-code-entities' not found in properties '[]'", + message); + } + } + + @Test + public void testConstructorWithInvalidValuesForPropertyIndicesOfDataSetCodeEntities() + { + try + { + final Properties properties = new Properties(); + properties.setProperty(INDICES_OF_DATA_SET_CODE_ENTITIES, "2,u"); + new DataSetInfoExtractorForDataAcquisition(properties); + fail("ConfigurationFailureException expected"); + } catch (final ConfigurationFailureException e) + { + assertEquals("2. index in property 'indices-of-data-set-code-entities' " + + "isn't a number: 2,u", e.getMessage()); + } + + } +} diff --git a/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/threev/DataSetInfoExtractorForImageAnalysisTest.java b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/threev/DataSetInfoExtractorForImageAnalysisTest.java new file mode 100644 index 00000000000..46ae5ddb907 --- /dev/null +++ b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/threev/DataSetInfoExtractorForImageAnalysisTest.java @@ -0,0 +1,70 @@ +/* + * Copyright 2008 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver.threev; + +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.fail; + +import java.io.File; +import java.util.Properties; + +import org.testng.annotations.Test; + +import ch.systemsx.cisd.common.exceptions.ConfigurationFailureException; +import ch.systemsx.cisd.etlserver.CodeExtractortTestCase; +import ch.systemsx.cisd.etlserver.DataSetInformation; +import ch.systemsx.cisd.etlserver.IDataSetInfoExtractor; + +/** + * @author Franz-Josef Elmer + */ +public class DataSetInfoExtractorForImageAnalysisTest extends CodeExtractortTestCase +{ + private static final String INDICES_OF_PARENT_DATA_SET_CODE_ENTITIES = + PREFIX + DataSetInfoExtractorForImageAnalysis.INDICES_OF_PARENT_DATA_SET_CODE_ENTITIES; + + @Test + public void testHappyCaseWithOnlyMandatoryPorperties() + { + Properties properties = new Properties(); + properties.setProperty(INDICES_OF_PARENT_DATA_SET_CODE_ENTITIES, "1, 0"); + IDataSetInfoExtractor extractor = new DataSetInfoExtractorForImageAnalysis(properties); + + DataSetInformation dataSetInfo = extractor.getDataSetInformation(new File("alpha.42.beta")); + assertEquals("42.alpha", dataSetInfo.getParentDataSetCode()); + assertEquals("beta", dataSetInfo.getSampleIdentifier().getSampleCode()); + assertEquals(null, dataSetInfo.getDataSetCode()); + assertEquals(null, dataSetInfo.getProducerCode()); + assertEquals(null, dataSetInfo.getProductionDate()); + } + + @Test + public void testConstructorWithMissingMandatoryProperty() + { + try + { + new DataSetInfoExtractorForImageAnalysis(new Properties()); + fail("ConfigurationFailureException expected"); + } catch (ConfigurationFailureException e) + { + String message = e.getMessage(); + assertEquals( + "Given key 'indices-of-parent-data-set-code-entities' not found in properties '[]'", + message); + } + } +} diff --git a/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/threev/HCSImageFileExtractorTest.java b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/threev/HCSImageFileExtractorTest.java new file mode 100644 index 00000000000..b74684e2a0b --- /dev/null +++ b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/threev/HCSImageFileExtractorTest.java @@ -0,0 +1,186 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.etlserver.threev; + +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.util.Properties; + +import org.apache.commons.io.FileUtils; +import org.apache.log4j.Level; +import org.jmock.Expectations; +import org.jmock.Mockery; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import ch.systemsx.cisd.bds.hcs.Location; +import ch.systemsx.cisd.bds.hcs.WellGeometry; +import ch.systemsx.cisd.bds.storage.IDirectory; +import ch.systemsx.cisd.bds.storage.IFile; +import ch.systemsx.cisd.bds.storage.filesystem.NodeFactory; +import ch.systemsx.cisd.common.filesystem.AbstractFileSystemTestCase; +import ch.systemsx.cisd.common.logging.BufferedAppender; +import ch.systemsx.cisd.etlserver.IHCSImageFileAccepter; + +/** + * Test cases for the {@link HCSImageFileExtractor}. + * + * @author Christian Ribeaud + */ +public final class HCSImageFileExtractorTest extends AbstractFileSystemTestCase +{ + + private static final String WELL_GEOMETRY = "3x3"; + + private HCSImageFileExtractor fileExtractor; + + private Mockery context; + + private IHCSImageFileAccepter fileAccepter; + + private BufferedAppender logRecorder; + + private final IDirectory workingDirectoryNode; + + public HCSImageFileExtractorTest() + { + super(); + this.workingDirectoryNode = NodeFactory.createDirectoryNode(workingDirectory); + } + + private final void prepareFileExtractor() + { + context = new Mockery(); + fileAccepter = context.mock(IHCSImageFileAccepter.class); + logRecorder = new BufferedAppender("%m", Level.WARN); + fileExtractor = new HCSImageFileExtractor(createProperties()); + } + + private final static Properties createProperties() + { + final Properties props = new Properties(); + props.setProperty(WellGeometry.WELL_GEOMETRY, WELL_GEOMETRY); + return props; + } + + private final IFile createFile(final String fileName) throws IOException + { + final File file = new File(workingDirectory, fileName); + FileUtils.touch(file); + assertTrue(file.exists()); + return NodeFactory.createFileNode(file); + } + + // + // AbstractFileSystemTestCase + // + + @Override + @BeforeMethod + public final void setUp() throws IOException + { + super.setUp(); + prepareFileExtractor(); + logRecorder.resetLogContent(); + } + + @AfterMethod + public final void tearDown() + { + // To following line of code should also be called at the end of each test method. + // Otherwise one do not known which test failed. + context.assertIsSatisfied(); + } + + @Test + public final void processWithNull() + { + boolean exceptionThrown = false; + try + { + fileExtractor.process(null, null, null); + } catch (AssertionError ex) + { + exceptionThrown = true; + } + assertTrue("Null values not allowed here.", exceptionThrown); + context.assertIsSatisfied(); + } + + @Test + public final void testProcessWithNoCorrectPrefix() throws IOException + { + final String imagePath = "H24_s6_w1_[UUID].tif"; + createFile(imagePath); + assertEquals(0, fileExtractor.process(workingDirectoryNode, null, fileAccepter) + .getInvalidFiles().size()); + } + + @Test + public final void testProcessHappyCase() throws IOException + { + final String imagePath = "Screening_H24_s6_w1_[UUID].tif"; + final IFile file = createFile(imagePath); + final int channel = 1; + final Location plateLocation = new Location(24, 8); + final Location wellLocation = new Location(3, 2); + context.checking(new Expectations() + { + { + one(fileAccepter).accept(channel, plateLocation, wellLocation, file); + } + }); + assertEquals("", logRecorder.getLogContent()); + assertTrue(fileExtractor.process(workingDirectoryNode, null, fileAccepter).getInvalidFiles() + .isEmpty()); + context.assertIsSatisfied(); + } + + @Test + public final void testProcessWithNotEnoughTokens() throws IOException + { + final String imagePath = "Screening_H24_s6_w1.tif"; + createFile(imagePath); + fileExtractor.process(workingDirectoryNode, null, fileAccepter); + assertEquals(0, fileExtractor.process(workingDirectoryNode, null, fileAccepter) + .getInvalidFiles().size()); + } + + @Test + public final void testProcessWithNoRightPlateCoordinate() throws IOException + { + final String imagePath = "Screening_XX_s6_w1_UUID.tif"; + createFile(imagePath); + fileExtractor.process(workingDirectoryNode, null, fileAccepter); + assertEquals(1, fileExtractor.process(workingDirectoryNode, null, fileAccepter) + .getInvalidFiles().size()); + } + + @Test + public final void testProcessWithNoRightWellCoordinate() throws IOException + { + final String imagePath = "Screening_H24_6_w1_UUID.tif"; + createFile(imagePath); + fileExtractor.process(workingDirectoryNode, null, fileAccepter); + assertEquals(1, fileExtractor.process(workingDirectoryNode, null, fileAccepter) + .getInvalidFiles().size()); + } +} \ No newline at end of file diff --git a/datastore_server/sourceTest/java/tests.xml b/datastore_server/sourceTest/java/tests.xml new file mode 100644 index 00000000000..e8c6f023e2b --- /dev/null +++ b/datastore_server/sourceTest/java/tests.xml @@ -0,0 +1,14 @@ +<!DOCTYPE suite SYSTEM "http://beust.com/testng/testng-1.0.dtd" > + +<suite name="All" verbose="1"> + <test name="All"> + <groups> + <run> + <exclude name="broken" /> + </run> + </groups> + <packages> + <package name="ch.systemsx.cisd.etlserver.*" /> + </packages> + </test> +</suite> diff --git a/datastore_server/sourceTest/java/tests_fast.xml b/datastore_server/sourceTest/java/tests_fast.xml new file mode 100644 index 00000000000..3aee85772b9 --- /dev/null +++ b/datastore_server/sourceTest/java/tests_fast.xml @@ -0,0 +1,15 @@ +<!DOCTYPE suite SYSTEM "http://beust.com/testng/testng-1.0.dtd" > + +<suite name="Fast" verbose="1"> + <test name="Fast"> + <groups> + <run> + <exclude name="slow" /> + <exclude name="broken" /> + </run> + </groups> + <packages> + <package name="ch.systemsx.cisd.etlserver.*" /> + </packages> + </test> +</suite> -- GitLab