From 843c9960ebfae19d2f832e70ddc1ef442d7d5ea0 Mon Sep 17 00:00:00 2001 From: gmantele Date: Mon, 8 Jun 2015 16:07:37 +0200 Subject: [PATCH] Merge branch 'master' into objectPosition --- README.md | 14 +- buildADQL.xml | 64 +- buildTAP.xml | 104 +- buildUWS.xml | 65 +- lib/cos-1.5beta.jar | Bin 0 -> 61344 bytes lib/{uploadUtilsSrc => }/cos.jar | Bin lib/stil_3.0-11.jar | Bin 0 -> 1987282 bytes lib/uploadUtils.jar | Bin 180336 -> 0 bytes lib/uploadUtilsSrc/binarySavot.jar | Bin 41844 -> 0 bytes lib/uploadUtilsSrc/buildJar.xml | 23 - lib/uploadUtilsSrc/cds.savot.common.jar | Bin 1780 -> 0 bytes lib/uploadUtilsSrc/cds.savot.model.jar | Bin 39452 -> 0 bytes lib/uploadUtilsSrc/cds.savot.pull.jar | Bin 19939 -> 0 bytes lib/uploadUtilsSrc/cds.savot.writer.jar | Bin 12704 -> 0 bytes lib/uploadUtilsSrc/kxml2-min.jar | Bin 10649 -> 0 bytes src/adql/db/DBChecker.java | 1277 ++- src/adql/db/DBColumn.java | 20 +- src/adql/db/DBCommonColumn.java | 18 +- src/adql/db/DBTable.java | 24 +- src/adql/db/DBType.java | 150 + src/adql/db/DefaultDBColumn.java | 95 +- src/adql/db/DefaultDBTable.java | 56 +- src/adql/db/FunctionDef.java | 541 ++ src/adql/db/STCS.java | 1683 ++++ src/adql/db/SearchColumnList.java | 7 +- src/adql/db/SearchTableList.java | 38 +- .../UnresolvedFunctionException.java | 124 + .../UnresolvedIdentifiersException.java | 24 +- ...Join.java => UnresolvedJoinException.java} | 11 +- src/adql/parser/.gitignore | 3 + src/adql/parser/ADQLParser.java | 7002 ++++++++--------- src/adql/parser/ADQLQueryFactory.java | 53 +- src/adql/parser/QueryChecker.java | 2 +- src/adql/parser/TokenMgrError.java | 9 +- src/adql/parser/adqlGrammar.jj | 118 +- src/adql/query/ADQLList.java | 22 +- src/adql/query/ADQLObject.java | 8 +- src/adql/query/ADQLQuery.java | 19 +- src/adql/query/ClauseADQL.java | 2 +- src/adql/query/SelectAllColumns.java | 8 +- src/adql/query/SelectItem.java | 10 +- src/adql/query/TextPosition.java | 10 +- src/adql/query/constraint/Between.java | 10 +- src/adql/query/constraint/Comparison.java | 10 +- src/adql/query/constraint/Exists.java | 10 +- src/adql/query/constraint/In.java | 10 +- src/adql/query/constraint/IsNull.java | 10 +- src/adql/query/constraint/NotConstraint.java | 10 +- src/adql/query/from/ADQLJoin.java | 22 +- src/adql/query/from/FromContent.java | 14 +- src/adql/query/operand/ADQLColumn.java | 32 +- src/adql/query/operand/ADQLOperand.java | 28 +- src/adql/query/operand/Concatenation.java | 14 +- src/adql/query/operand/NegativeOperand.java | 19 +- src/adql/query/operand/NumericConstant.java | 20 +- src/adql/query/operand/Operation.java | 21 +- src/adql/query/operand/StringConstant.java | 21 +- src/adql/query/operand/UnknownType.java | 52 + src/adql/query/operand/WrappedOperand.java | 16 +- .../query/operand/function/ADQLFunction.java | 10 +- .../query/operand/function/DefaultUDF.java | 77 +- .../query/operand/function/MathFunction.java | 11 +- .../query/operand/function/SQLFunction.java | 13 +- .../operand/function/UserDefinedFunction.java | 59 +- .../function/geometry/AreaFunction.java | 13 +- .../function/geometry/BoxFunction.java | 12 +- .../function/geometry/CentroidFunction.java | 13 +- .../function/geometry/CircleFunction.java | 11 +- .../function/geometry/ContainsFunction.java | 13 +- .../function/geometry/DistanceFunction.java | 13 +- .../function/geometry/ExtractCoord.java | 13 +- .../function/geometry/ExtractCoordSys.java | 13 +- .../function/geometry/GeometryFunction.java | 27 +- .../function/geometry/IntersectsFunction.java | 13 +- .../function/geometry/PointFunction.java | 13 +- .../function/geometry/PolygonFunction.java | 14 +- .../function/geometry/RegionFunction.java | 11 +- src/adql/translator/ADQLTranslator.java | 6 +- src/adql/translator/JDBCTranslator.java | 908 +++ src/adql/translator/PgSphereTranslator.java | 593 +- src/adql/translator/PostgreSQLTranslator.java | 801 +- src/cds/utils/TextualSearchList.java | 2 +- src/org/json/Json4Uws.java | 35 +- src/tap/ADQLExecutor.java | 716 +- src/tap/AbstractTAPFactory.java | 297 +- src/tap/AsyncThread.java | 75 +- src/tap/ExecutionProgression.java | 11 +- src/tap/ServiceConnection.java | 637 +- src/tap/TAPException.java | 250 +- src/tap/TAPExecutionReport.java | 113 +- src/tap/TAPFactory.java | 460 +- src/tap/TAPJob.java | 308 +- src/tap/TAPRequestParser.java | 216 + src/tap/TAPSyncJob.java | 255 +- src/tap/backup/DefaultTAPBackupManager.java | 267 +- .../config/ConfigurableServiceConnection.java | 1594 ++++ src/tap/config/ConfigurableTAPFactory.java | 334 + src/tap/config/ConfigurableTAPServlet.java | 234 + src/tap/config/TAPConfiguration.java | 539 ++ src/tap/config/gums_table.txt | 49 + src/tap/config/tap_configuration_file.html | 720 ++ src/tap/config/tap_full.properties | 536 ++ src/tap/config/tap_min.properties | 107 + .../DataReadException.java} | 30 +- src/tap/data/LimitedTableIterator.java | 252 + src/tap/data/ResultSetTableIterator.java | 544 ++ src/tap/data/TableIterator.java | 138 + src/tap/data/VOTableIterator.java | 484 ++ src/tap/db/DBConnection.java | 259 +- src/tap/db/JDBCConnection.java | 2840 ++++++- src/tap/error/DefaultTAPErrorWriter.java | 302 +- src/tap/file/LocalTAPFileManager.java | 148 - src/tap/file/TAPFileManager.java | 44 - src/tap/formatter/FITSFormat.java | 101 + src/tap/formatter/HTMLFormat.java | 215 + src/tap/formatter/JSONFormat.java | 225 +- src/tap/formatter/OutputFormat.java | 45 +- .../formatter/ResultSet2JsonFormatter.java | 121 - src/tap/formatter/ResultSet2SVFormatter.java | 105 - .../formatter/ResultSet2TextFormatter.java | 85 - .../formatter/ResultSet2VotableFormatter.java | 127 - src/tap/formatter/SVFormat.java | 298 +- src/tap/formatter/TextFormat.java | 214 +- src/tap/formatter/VOTableFormat.java | 798 +- src/tap/log/DefaultTAPLog.java | 229 +- src/tap/log/TAPLog.java | 120 +- src/tap/metadata/TAPColumn.java | 627 +- src/tap/metadata/TAPMetadata.java | 686 +- src/tap/metadata/TAPSchema.java | 310 +- src/tap/metadata/TAPTable.java | 677 +- src/tap/metadata/TAPTypes.java | 359 - src/tap/metadata/TableSetParser.java | 905 +++ src/tap/metadata/VotType.java | 344 +- src/tap/parameters/DALIUpload.java | 601 ++ src/tap/parameters/FormatController.java | 59 +- src/tap/parameters/MaxRecController.java | 73 +- .../TAPDestructionTimeController.java | 107 +- .../TAPExecutionDurationController.java | 86 +- src/tap/parameters/TAPParameters.java | 384 +- src/tap/resource/ASync.java | 156 +- src/tap/resource/Availability.java | 77 +- src/tap/resource/Capabilities.java | 102 +- src/tap/resource/HomePage.java | 307 + src/tap/resource/Sync.java | 57 +- src/tap/resource/TAP.java | 901 ++- src/tap/resource/TAPResource.java | 66 +- src/tap/resource/VOSIResource.java | 57 +- src/tap/upload/LimitedSizeInputStream.java | 59 +- src/tap/upload/TableLoader.java | 104 - src/tap/upload/Uploader.java | 275 +- src/uws/ClientAbortException.java | 56 + src/uws/ISO8601Format.java | 342 + src/uws/UWSException.java | 128 +- src/uws/UWSExceptionFactory.java | 157 +- src/uws/UWSToolBox.java | 327 +- src/uws/job/ErrorSummary.java | 11 +- src/uws/job/ErrorType.java | 14 +- src/uws/job/JobList.java | 162 +- src/uws/job/JobPhase.java | 37 +- src/uws/job/JobThread.java | 195 +- src/uws/job/Result.java | 15 +- src/uws/job/SerializableUWSObject.java | 53 +- src/uws/job/UWSJob.java | 533 +- .../AbstractQueuedExecutionManager.java | 130 +- .../manager/DefaultDestructionManager.java | 26 +- .../job/manager/DefaultExecutionManager.java | 67 +- src/uws/job/manager/DestructionManager.java | 20 +- src/uws/job/manager/ExecutionManager.java | 46 +- .../job/manager/QueuedExecutionManager.java | 21 +- .../parameters/DestructionTimeController.java | 74 +- .../ExecutionDurationController.java | 101 +- .../job/parameters/InputParamController.java | 1 + .../job/parameters/StringParamController.java | 20 +- src/uws/job/parameters/UWSParameters.java | 355 +- src/uws/job/serializer/JSONSerializer.java | 167 +- src/uws/job/serializer/UWSSerializer.java | 136 +- src/uws/job/serializer/XMLSerializer.java | 218 +- src/uws/service/AbstractUWSFactory.java | 20 +- src/uws/service/UWS.java | 84 +- src/uws/service/UWSFactory.java | 27 +- src/uws/service/UWSService.java | 541 +- src/uws/service/UWSServlet.java | 533 +- src/uws/service/UWSUrl.java | 38 +- src/uws/service/UserIdentifier.java | 12 +- src/uws/service/actions/AddJob.java | 28 +- src/uws/service/actions/DestroyJob.java | 26 +- src/uws/service/actions/GetJobParam.java | 72 +- src/uws/service/actions/JobSummary.java | 31 +- src/uws/service/actions/ListJobs.java | 31 +- src/uws/service/actions/SetJobParam.java | 36 +- src/uws/service/actions/SetUWSParameter.java | 110 + src/uws/service/actions/ShowHomePage.java | 37 +- src/uws/service/actions/UWSAction.java | 53 +- .../backup/DefaultUWSBackupManager.java | 232 +- src/uws/service/backup/UWSBackupManager.java | 2 +- .../error/AbstractServiceErrorWriter.java | 291 - .../service/error/DefaultUWSErrorWriter.java | 343 +- src/uws/service/error/ServiceErrorWriter.java | 118 +- src/uws/service/file/EventFrequency.java | 498 ++ src/uws/service/file/LocalUWSFileManager.java | 343 +- src/uws/service/file/UWSFileManager.java | 123 +- .../file/UnsupportedURIProtocolException.java | 45 + src/uws/service/log/DefaultUWSLog.java | 612 +- src/uws/service/log/UWSLog.java | 318 +- src/uws/service/log/UWSLogType.java | 53 - .../service/request/FormEncodedParser.java | 178 + src/uws/service/request/MultipartParser.java | 250 + src/uws/service/request/NoEncodingParser.java | 165 + src/uws/service/request/RequestParser.java | 93 + src/uws/service/request/UWSRequestParser.java | 141 + src/uws/service/request/UploadFile.java | 207 + test/adql/SearchColumnListTest.java | 240 - test/adql/SearchIterator.java | 76 - test/adql/SearchResult.java | 251 - test/adql/TestADQLQuery.java | 108 +- test/adql/TestGetPositionInAllADQLObject.java | 4 +- test/adql/TestIN.java | 60 +- test/adql/TestIdentifierField.java | 25 + test/adql/db/TestDBChecker.java | 740 ++ test/adql/db/TestFunctionDef.java | 312 + test/adql/db/TestSTCS.java | 536 ++ test/adql/parser/TestADQLParser.java | 43 + test/adql/query/from/TestCrossJoin.java | 95 + test/adql/query/from/TestInnerJoin.java | 158 + .../translator/TestPgSphereTranslator.java | 332 + test/tap/config/AllTAPConfigTests.java | 29 + .../TestConfigurableServiceConnection.java | 1200 +++ .../config/TestConfigurableTAPFactory.java | 512 ++ test/tap/config/TestTAPConfiguration.java | 416 + test/tap/data/ResultSetTableIteratorTest.java | 127 + test/tap/data/VOTableIteratorTest.java | 196 + test/tap/data/emptyset.vot | 123 + test/tap/data/emptyset_binary.vot | 125 + test/tap/data/testdata.vot | 676 ++ test/tap/data/testdata_binary.vot | 137 + test/tap/db/JDBCConnectionTest.java | 1094 +++ test/tap/db/TestTAPDb.db | Bin 0 -> 26624 bytes test/tap/db/upload_example.vot | 75 + test/tap/formatter/JSONFormatTest.java | 142 + test/tap/formatter/SVFormatTest.java | 136 + .../tap/formatter/ServiceConnection4Test.java | 139 + test/tap/formatter/TextFormatTest.java | 136 + test/tap/formatter/VOTableFormatTest.java | 141 + test/tap/metadata/MetadataExtractionTest.java | 147 + test/tap/metadata/TableSetParserTest.java | 1162 +++ .../parameters/ServiceConnectionOfTest.java | 177 + test/tap/parameters/TestFormatController.java | 89 + test/tap/parameters/TestMaxRecController.java | 142 + .../TestTAPDestructionTimeController.java | 187 + .../TestTAPExecutionDurationController.java | 136 + test/testtools/CommandExecute.java | 51 + test/testtools/DBTools.java | 136 + test/testtools/MD5Checksum.java | 46 + test/uws/TestISO8601Format.java | 163 + .../TestDestructionTimeController.java | 186 + .../TestExecutionDurationController.java | 134 + test/uws/service/UWSUrlTest.java | 560 ++ test/uws/service/file/TestLogRotation.java | 277 + test/uws/service/log/TestDefaultUWSLog.java | 63 + 259 files changed, 47107 insertions(+), 11474 deletions(-) create mode 100644 lib/cos-1.5beta.jar rename lib/{uploadUtilsSrc => }/cos.jar (100%) create mode 100644 lib/stil_3.0-11.jar delete mode 100644 lib/uploadUtils.jar delete mode 100644 lib/uploadUtilsSrc/binarySavot.jar delete mode 100644 lib/uploadUtilsSrc/buildJar.xml delete mode 100644 lib/uploadUtilsSrc/cds.savot.common.jar delete mode 100644 lib/uploadUtilsSrc/cds.savot.model.jar delete mode 100644 lib/uploadUtilsSrc/cds.savot.pull.jar delete mode 100644 lib/uploadUtilsSrc/cds.savot.writer.jar delete mode 100644 lib/uploadUtilsSrc/kxml2-min.jar create mode 100644 src/adql/db/DBType.java create mode 100644 src/adql/db/FunctionDef.java create mode 100644 src/adql/db/STCS.java create mode 100644 src/adql/db/exception/UnresolvedFunctionException.java rename src/adql/db/exception/{UnresolvedJoin.java => UnresolvedJoinException.java} (85%) create mode 100644 src/adql/parser/.gitignore create mode 100644 src/adql/query/operand/UnknownType.java create mode 100644 src/adql/translator/JDBCTranslator.java create mode 100644 src/tap/TAPRequestParser.java create mode 100644 src/tap/config/ConfigurableServiceConnection.java create mode 100644 src/tap/config/ConfigurableTAPFactory.java create mode 100644 src/tap/config/ConfigurableTAPServlet.java create mode 100644 src/tap/config/TAPConfiguration.java create mode 100644 src/tap/config/gums_table.txt create mode 100644 src/tap/config/tap_configuration_file.html create mode 100644 src/tap/config/tap_full.properties create mode 100644 src/tap/config/tap_min.properties rename src/tap/{formatter/ResultSetFormatter.java => data/DataReadException.java} (53%) create mode 100644 src/tap/data/LimitedTableIterator.java create mode 100644 src/tap/data/ResultSetTableIterator.java create mode 100644 src/tap/data/TableIterator.java create mode 100644 src/tap/data/VOTableIterator.java delete mode 100644 src/tap/file/LocalTAPFileManager.java delete mode 100644 src/tap/file/TAPFileManager.java create mode 100644 src/tap/formatter/FITSFormat.java create mode 100644 src/tap/formatter/HTMLFormat.java delete mode 100644 src/tap/formatter/ResultSet2JsonFormatter.java delete mode 100644 src/tap/formatter/ResultSet2SVFormatter.java delete mode 100644 src/tap/formatter/ResultSet2TextFormatter.java delete mode 100644 src/tap/formatter/ResultSet2VotableFormatter.java delete mode 100644 src/tap/metadata/TAPTypes.java create mode 100644 src/tap/metadata/TableSetParser.java create mode 100644 src/tap/parameters/DALIUpload.java create mode 100644 src/tap/resource/HomePage.java delete mode 100644 src/tap/upload/TableLoader.java create mode 100644 src/uws/ClientAbortException.java create mode 100644 src/uws/ISO8601Format.java create mode 100644 src/uws/service/actions/SetUWSParameter.java delete mode 100644 src/uws/service/error/AbstractServiceErrorWriter.java create mode 100644 src/uws/service/file/EventFrequency.java create mode 100644 src/uws/service/file/UnsupportedURIProtocolException.java delete mode 100644 src/uws/service/log/UWSLogType.java create mode 100644 src/uws/service/request/FormEncodedParser.java create mode 100644 src/uws/service/request/MultipartParser.java create mode 100644 src/uws/service/request/NoEncodingParser.java create mode 100644 src/uws/service/request/RequestParser.java create mode 100644 src/uws/service/request/UWSRequestParser.java create mode 100644 src/uws/service/request/UploadFile.java delete mode 100644 test/adql/SearchColumnListTest.java delete mode 100644 test/adql/SearchIterator.java delete mode 100644 test/adql/SearchResult.java create mode 100644 test/adql/TestIdentifierField.java create mode 100644 test/adql/db/TestDBChecker.java create mode 100644 test/adql/db/TestFunctionDef.java create mode 100644 test/adql/db/TestSTCS.java create mode 100644 test/adql/parser/TestADQLParser.java create mode 100644 test/adql/query/from/TestCrossJoin.java create mode 100644 test/adql/query/from/TestInnerJoin.java create mode 100644 test/adql/translator/TestPgSphereTranslator.java create mode 100644 test/tap/config/AllTAPConfigTests.java create mode 100644 test/tap/config/TestConfigurableServiceConnection.java create mode 100644 test/tap/config/TestConfigurableTAPFactory.java create mode 100644 test/tap/config/TestTAPConfiguration.java create mode 100644 test/tap/data/ResultSetTableIteratorTest.java create mode 100644 test/tap/data/VOTableIteratorTest.java create mode 100644 test/tap/data/emptyset.vot create mode 100644 test/tap/data/emptyset_binary.vot create mode 100644 test/tap/data/testdata.vot create mode 100644 test/tap/data/testdata_binary.vot create mode 100644 test/tap/db/JDBCConnectionTest.java create mode 100644 test/tap/db/TestTAPDb.db create mode 100644 test/tap/db/upload_example.vot create mode 100644 test/tap/formatter/JSONFormatTest.java create mode 100644 test/tap/formatter/SVFormatTest.java create mode 100644 test/tap/formatter/ServiceConnection4Test.java create mode 100644 test/tap/formatter/TextFormatTest.java create mode 100644 test/tap/formatter/VOTableFormatTest.java create mode 100644 test/tap/metadata/MetadataExtractionTest.java create mode 100644 test/tap/metadata/TableSetParserTest.java create mode 100644 test/tap/parameters/ServiceConnectionOfTest.java create mode 100644 test/tap/parameters/TestFormatController.java create mode 100644 test/tap/parameters/TestMaxRecController.java create mode 100644 test/tap/parameters/TestTAPDestructionTimeController.java create mode 100644 test/tap/parameters/TestTAPExecutionDurationController.java create mode 100644 test/testtools/CommandExecute.java create mode 100644 test/testtools/DBTools.java create mode 100644 test/testtools/MD5Checksum.java create mode 100644 test/uws/TestISO8601Format.java create mode 100644 test/uws/job/parameters/TestDestructionTimeController.java create mode 100644 test/uws/job/parameters/TestExecutionDurationController.java create mode 100644 test/uws/service/UWSUrlTest.java create mode 100644 test/uws/service/file/TestLogRotation.java create mode 100644 test/uws/service/log/TestDefaultUWSLog.java diff --git a/README.md b/README.md index 25c9163..901ccd1 100644 --- a/README.md +++ b/README.md @@ -33,14 +33,18 @@ Each library has its own package (`adql` for ADQL, `uws` for UWS and `tap` for T ### Dependencies Below are summed up the dependencies of each library: -* ADQL: `adql`, `cds.utils` -* UWS: `uws`, `org.json` -* TAP: `adql`, `uws`, `cds.*`, `org.json` +* ADQL: `adql`, `cds.utils`, `org.postgresql` *(for adql.translator.PgSphereTranslator only)* +* UWS: `uws`, `org.json`, HTTP Multipart lib. (`com.oreilly.servlet`) +* TAP: `adql`, `uws`, `cds.*`, `org.json`, `org.postgresql` *(for adql.translator.PgSphereTranslator only)*, HTTP Multipart lib. (`com.oreilly.servlet`), STIL (`nom.tap`, `org.apache.tools.bzip2`, `uk.ac.starlink`) + +In the `lib` directory, you will find 2 JAR files: +* `cos-1.5beta.jar` to deal with HTTP multipart requests +* `stil3.0-5.jar` for [STIL](http://www.star.bris.ac.uk/~mbt/stil/) (VOTable and other formats support) ### ANT scripts At the root of the repository, there are 3 ANT scripts. Each is dedicated to one library. They are able to generate JAR for sources, binaries and Javadoc. 3 properties must be set before using one of these scripts: -* `CATALINA`: a path toward a JAR or a binary directory containing org.apache.catalina.connector.ClientAbortException.class +* `POSTGRES`: a path toward a JAR or a binary directory containing all org.postgresql.* - [https://jdbc.postgresql.org/download.html](JDBC Postgres driver) - **(ONLY for ADQL and TAP if you want to keep adql.translator.PgSphereTranslator)** * `SERVLET-API`: a path toward a JAR or a binary directory containing all javax.servlet.* -* (`JUNIT-API` *not required before the version 2.0 of the tap library*: a path toward one or several JARs or binary directories containing all classes to use JUnit.) +* (`JUNIT-API` *not required before the version 2.0 of the tap library OR if you are not interested by the `test` directory (JUnit tests)*: a path toward one or several JARs or binary directories containing all classes to use JUnit.) diff --git a/buildADQL.xml b/buildADQL.xml index 398c502..26de09d 100644 --- a/buildADQL.xml +++ b/buildADQL.xml @@ -2,7 +2,7 @@ - + @@ -11,7 +11,7 @@ - + @@ -22,6 +22,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + ADQL LIBRARY VERSION = ${version} @@ -30,7 +57,24 @@ - + + + + + + + + + + + + + + + + + + @@ -41,9 +85,11 @@ - + - + + + @@ -63,7 +109,9 @@ - + + + @@ -76,7 +124,7 @@ - + @@ -85,4 +133,4 @@ - \ No newline at end of file + diff --git a/buildTAP.xml b/buildTAP.xml index 4ad484e..c8af0a6 100644 --- a/buildTAP.xml +++ b/buildTAP.xml @@ -2,39 +2,68 @@ - + + - - + + + + + - - - + + + - - + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + TAP LIBRARY VERSION = ${version} @@ -43,7 +72,34 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -52,9 +108,9 @@ - + - + @@ -62,14 +118,26 @@ Generate the library: - + + + + + + + - - + + + + + + + + @@ -83,7 +151,7 @@ - + @@ -93,4 +161,4 @@ - \ No newline at end of file + diff --git a/buildUWS.xml b/buildUWS.xml index 88a8ded..d6ddd7f 100644 --- a/buildUWS.xml +++ b/buildUWS.xml @@ -2,35 +2,47 @@ - + + + + + - - + + + - - - + + + - + + - + + + + + + + UWS LIBRARY VERSION = ${version} @@ -39,7 +51,24 @@ - + + + + + + + + + + + + + + + + + + @@ -48,21 +77,27 @@ - + - + Generate the library: - + + + + - + + + + @@ -75,7 +110,7 @@ - + @@ -85,4 +120,4 @@ - \ No newline at end of file + diff --git a/lib/cos-1.5beta.jar b/lib/cos-1.5beta.jar new file mode 100644 index 0000000000000000000000000000000000000000..c30b6238dd5372f873e46acc19273a1d3394edb9 GIT binary patch literal 61344 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 Cp+S5A literal 0 HcmV?d00001 diff --git a/lib/uploadUtilsSrc/cos.jar b/lib/cos.jar similarity index 100% rename from lib/uploadUtilsSrc/cos.jar rename to lib/cos.jar diff --git a/lib/stil_3.0-11.jar b/lib/stil_3.0-11.jar new file mode 100644 index 0000000000000000000000000000000000000000..1c7d53a179b8dd7c4d50f91a028d533e4cf59aa6 GIT binary patch literal 1987282 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%0AXCMZBJ1g~$4p=_sn!cq0BjtWji{wz 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

4TdCl`m=?%dLDh}B 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

g1$J4BB1cR;&-nwdh)8 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

1x;f#iF;Hj48LVP{@?iMI*uuvl%wSj?S=!9xWy zni6igVeheJI415N<<)c)4~D$_h^bOqFETJ_Zq@8L-_tG2M$=~I;ruNbOTXVqI$F?B zK&<+pe9x^~9=`Ewq>}jo17PiTa_OhCv8I<&+aB5qo;D~XA4pq)Ld7EJcV&}0e!0F^ zWcbvGHNB-mC8Ke4#(F6LGu!PeeIoj8tGvK zd`?vGedRVm6~yStnxLRiHmvqUn-8UbgyYUl($c3#Cmi;SgD#}N~N1m*66!0FP2)R z>3)_OS)Ko?V|rvyO|Ds2k;zJIH>;NlE}wvV&0cA7WQ5OO$&v6eQ2#4^e-l(zm+ z&|QypYG19k%73O=j}CRM4JbQ&m@uw>q~2Z9Y~`Y+J$7*uax4cEMi{D1#cvX0+cw51 z7I;wl20dZyU(e%zK;e&MIhjvZWt-V0Z0CP)hZT31nTeSYwW=UEyoa+kxwtXUn4M;G zyVT6L)ExDPDXwg9TIERl(y1%tGl(dMgA_SAe}CWChpMW9W@uu}C9zq>Nifr1j=t15 zRw>S?4|^8LLhUU4^vvx^WSMrQOMLn%$cJq)4;#PLAIATxOP8@O>!Z0Fqn?UpS1i^t zf)y8*a6(Pf@1m!+FGu7HNb=v)w0}>Ta#obosnCeSEvXGmu`yHnFpSJxtJ#D*^#nnZ z(-up{eIIhCtWnUXSM*=I#2cd4$SrGUY#?lLCpUDr4{m{>E7&iZq^7!Y-x?-O=c1c! zO6M{^L1mHEX44d=vz9tVEYA;8oP^ql=k&Ko4^y>f&I(B%>5Mq=k2N?{Q%R0w=hCe=1K3}=k?Hw?VCrQH$>jd_l#|VD|)5Lb~L&yxCUS^ zAeu}ffO1&48AQq0E5IO>N*)wqqz`Ho3;VM0JGNJaL4u2dEQDJ>W|5pMRJG4+o0~xv z7vrYyAe-NlA4Mm)Pd)$AOi-8)he*9{ia1vq^Q6{o+x`sEuC89rQxzffq|TyGWeH9n z2+fi8*})7sL7cOLYgCX;*&2!20ucdq(LQ_MfxMGaF0bD^Dsvs8fa>(*5k%pOf-5w~ zrW)4+I97yi?31!pY@qtd-wjoRz9DQIvfTpRPt{%q6uaUEO+MIv@<5iLmBNAViF;^a z?j+jv!oWql*pWN_yyRLAitK?M$vW)t?`aw^Mc9(U<#*y%Md&e1-cW5_m~|d{Mn+Lw z3j)qr!T$XySy`+*b4AgEX0Iz_k0F`Plxu+({i?&EWJ-i94MN>F4`&amM6|-EQBF^O zb#a;#c9v7Uidq+}4rMpQWKhOrcyU3T6Ml{~vJD0-3N!9#kz_Xy>Z?C@5I4s6Crn>h zK9AKLUJ)crM{d%7yAaKnVkC4d0v zg~B$c*H#%w!6q3}YH7^2+HJVJwN`9+YrROdCgEyWN9e$Z5QC=&SlSueVnt~iGXRI1 zwCRfaIHVLp3JHY18vTS%?SQ3c{Qg+8m`2ZvU38 zfl_BCj~0@R=tGtW*NUMX{<(PEt8zoi@renX!pC**5I2E`s$=-{FL-(xd}IrEOMa*{;{bC$Sdo7Y^Ke+K_ji}`VetHpGj?f&YB0D z&sP>!=(g^uGUeG>iYsK%$s`tA!F?%4e@Yr3=?*;=W9QOiFpT>o*hU_q$uEX{Dv8*C zMXg}Y)uTt3h5R#ZI7yKUGH_e3@Qa6T`62>$HiGsSm@T6#npQ!AlZ)P$OW((z#go54 zv^?GN+YU4+K0Sw#SwMtw99AuDfL=hnQKJp)6OA+%gB8L%lKDC_rD)CS4xabWi5K@7HVT_+3JYTXwT5j zw5;;)J<4P}WHrQWi|&2vH91RJEC|fYmPMCImkDZd>U=CV79vdXn)A=5k@C#m7)(`K#gvF@Gt|VAJqKoXm_uvf>6&mrCFz ztk!<#Rm4xYuHBn04%t;>I-(|{(Xj{=aMNJGslF6<)?hd@+?666Z>dg4bSb@5 zYv06Af<2ohIB(WDFNUHZK78 zaT^Z)1t(9-qU;}%!8y@xT`x4WBh+WD1~((a#|$EQ~q zjZIAD*gu-460Q&85qB{LFxwDom$5nAC8QI_gEdT&=>0y+pecTPYR9ps%mB%pW)Wd2EFH>FU;bj8o6=Xm>sgGqQS{*@xQ+VRs`##VdU z8?O__)ZekYqlrThObT2WJBFo%BhKuJtO1@I$WyfwXY$S_wBrDV%d70rI}mCHfI~;BCLb>pK0X^ zH>lbs$lEp5t5Uy>kBT)t`<|+7BN{a%wi>o<#T0BhavY-+o(JH5x~ttFuB?Tye1-_X zJ^h&-<8N#G#=bO`e(YCZ^NY(JPMw}}7T!abo@)j!kc6QCl?!tG0us~Lgp(-kMS$(~ zR=n)u&4HaB<_{g9_#HMV){o&iGE1lf^Y|7fOL-#}BnFozCDWz2C)DkV?X??q)|qq_ zC|rbLPkbLk#7!8{jyf)RnDv!QD{nla#_$-0k|vv8o7S<&QF;X zxK!SzjdI7ct@i7Kk5IJr^V5bhp;F7+w1hgKbj|lOp35XSZaOs<81MEd{>{$0)v7Xhzk{`-d% z{O$kE9#C|&u{ZkHn#M|wU$z?v@DeuklaUwN7LVM~-%0vr-b=H^SN(FJVp~}^vW(@E z0`oVCdy$b;%yPQ+!t27rONX~(o%h44{$|#}R%%vGIF+!_4^qH0e`BudHr5bS1`oZk zrAW_U$oYo8JV;6WENfyCgE6UzXRH#FJ+R};egtBm0NvFqMyC0F5neZ5W=ai zo|IjD<|Pp$${$QYZeI?d^Oq}5#TkC$Vo6@8n=%BrHR@i*C}B#hw5BB$536zICPA%L zfx{@u9o8BX0p-Q$1t1GSql97QvwG4R>6RPDTW>{~{FExJ@^^9Oe8wd)^$&s&)^*AE z5Rs1+c#2mOL_!!zwR$EWu6)%og$?8sQ%yP7n4DDe^QnkF5pZ)aY~RChb$j4286br&VbYWVLlb3q1H}6$c_dZr$0Tw3^s3#bVQGGs6SZtc2xCD%S(qN~&L_7P$iNhs``IN!!p=$f~4>c^v z|HBX|_@Vdw#|KaS|3JEknWfRcxHA8so#fyCN8ySZiZgs@Z!#&Y%mH8F@BA_c>uAMx zi`DX(6!h|$l#L-@Iv-;apgs|G8TvmedS1<$xJ^#|1OF?_VPRh^F|wA8bMJ%Hi`1H@ zKL-c&9~9M?GYx3`x!|FZg#-In7<>mf$eh#yZnJm3S4#oL7P7fVtktt>A;6kn8Z$Oc zy*Vp36lI$cdgWrAKo0(r#}q%?l6}{0r`nA#UCqQO)hr!mEl!-av(JW#v79nuAG%82 z(_yK~@q+wZ`sspJ>?sXpOlpVAx%w@sL`E$F_M(jN$k! z8m-J=nGK4F!a%N1VWf{PCdty19`*}8)}1B!__w=Gb8C0oRvRDp4V#hc^_cSm}+t8`;Q?-cmjwIz#sdOU8p&Z(v^&Kzc1SZvZ| z$g--P@ts-;_oXphWXrdH7|@10u*Doj-|W>gZ5dWIP^OK3nsq_CXPgF14;l~+b>qK4 zrA|HGp&dl!W>%@VW{M|~zWU%L_Oup3P{5=G@iCqhuszIif}4W)EAspPGN?3C-s)w4 zO|2YK7(kqUK)a2x;HP&F;QPZpqGWbEr9?`PB7!p{+EHwP(!BTHB2b&%^v~pe0 z=hq@k)W2PX`A?z#FDs%*#sAXzuq?5`jPN32vtx+r@q(c01eHYKGb= zDC6=KMBP5T&jmGULtGmwH@v+70sGBg&NaO^^5qRtj`c-vzNc(WtaG6EJ-hCUj_Ml- zPR*{L9o7(C)*-)vEzM{xS*tsPtrP-z2kIGmE&4E@MT?v}TU`wUF{Lfc^QCE7)ZOj6 z1gs5S;B+NH2pg1aMb{4e#)KbM>py8*rOU|+B)B`(4MQ@i7!PQmb?%hWi{8Zi@wK>1 zZ})qg1FLsp{f1%vi;OO9=l8N2hUs#Z0@jImCZi&!)MjItLvYn``WHpNA6hPJ2K!eCRiP@i_&y|1|Aj0Zrxv>~7uCWIDoXR6i1fmIh2)Ij7@ zj_t?WB)$usl|U9ghrAqfj2Cl$yKaw^TRUb)iO}O@%H3fnIbgfA z-XSqF6o^D8u?x3N+pKcK1kJVf?uy|Os<=VDbtx7#1YZ+1UYA`Hjog(#vd28LSr(8F zyu=%Ym8@0a!atbiAKCv|)Q52QkbV^ugJ&3*!kAmxo&rY?=g=bD4y)(C`DPz76D<1o z`#%b($@=1#Z|Jvgw*Pjs>kxq(*7I&Ip&KcJbUH+- zX+USm5sULBi~p6dRxjPsgTO5b*ICGOB>@9UTI{etOSBg=kIuqjS2o@6Uh{z5kj>wu zp*e|%<-6`(Uk+EekU;3Ud|MIWTGf>bdeh=_*uwQXg$@c(2h%N$9 z`B6HjcjcB&Sq=Q~KZqP~oF3y<36&R%4==UD}m3&(2s;9iBsgbPW0ly-{dg{@zxmevF(cJ7kft5Kuu$Cn0q}SZn%k|gO+dADK zCA*=aut{^OEU~<7k5_1Wi9%qMW{UHwMxtC!UMgG$sua!npy3?KC9XSP2``1N)5yxn z9amo7-l z=-^zu@w*qt_A2Pg<0?|iQ=#C`rZ`{K5+FJf=@*CPW%C^X(0b-C4(Gl&{ORB7v=>5n zD9$6fPyZ3j?tU>LOWq9eo}z*C^i&~jgNL<a%t#@VvUpZCk2pcbr*YcWero#qIl(2ct#w(C+q9y!_-4daQT zb0XSX?@`Lbs5fgHn-oVQE-;&U6rYhWp9M;sFb|!8OeCINY&6_rTo_D@&Sv@HjDZY6 zEeBI=3>ab!3gRfdX2^of+bJS9+d+J_34>hdDfpHM<)Uv?N5X8|2qN`i<%_tY9mB4V z97x$Q@4pp6hR_P)SSk6@ppW!|Mq6U$W*!@22(I?|G(}^laFE`p%#fn@SnpLstT3qb z38F?te|9qb%OXe11pJ8M3q`2^5fuL)lN_yo^tD{+MQ{ORvb=?U$}i?*NC;kK`~HX~ z3+saUbYX5xQDL5!Mq+=Ly~qAo2d_07aQ-7&p+5b z6ibo`bQZc(0YNYLR5`{yEHxvt?!N?mJ--4~ER5fNK4mK25y9qpiQGw(Ww5|A?h0|( z^*=UUeBFgZUrOB;48c7o=NZINOd&%uph0%9Ro0I)PxA#Mpy-#mvzr_d67ax`Vx-QN zds7-u;aCX)8u+u`-&A2JG{tL8cSc$ny-Y=^*tHyXLTQH{M8=o-y9VshYbfncBLtCB zU-nD|R4y0e8jDFT?}5x1k#_0wdQ#8T6E(jTw^3pnqdU5IIN~nXg?n-xM?JEER73Rc ziDY@}A+pwt6^`?xBeA1{^oJyD~VWD)gS}i z!8{I5rzrPN-y$jX7lohyRucK2R^|Wpt5MRh!Bj%{9MU$)d~CDIU_}WjpJ7?B zi))j@*OZ@IvxW`Q{Fa=6kYjn_)G=zjtbUzz5E*QbyeAK`_vZ)y86%Pupaj}R^d0&0 z2i@_&L6o{gyNB@NeZwuRU6i8kNOK-;1;39tNrbpsyuom%yI>Kn`J#xE6p` zk~dUh0`|~~F7B#52qPzH8e$+nq5vqm&ExGZ*fT;k;&)tCt(7v_rSKIGP#v_YIPzDY zd{9MB8%JK7(ug{YMtq$^?v-CEF--5;T}`S_c#L#gpO`{-)=kPt<4`AioJL6S(_n@Q zCZ!tAfKED=x~}U%L_)S6tTBO|MzWVEeRe7r`^HDh7IlhhnZL@U=3s8qkah^H7aC`Q zsluk>Fxq~ch$qaKbU1{~ebPSoaYbdKh7hjj$I>NFkJ)v|jwNXUZG$9vA|pcuSdOT$ zVue{feG#hPi?%kW0(y+zM4tB8{P-*pO`_sISS!-f@5aEt0O!s<$McZx8ZByv8}Bk< zgVC312%NEIS4yR*hb^{ut~Aj0b#HiznWns?0VNPC2G|2ZBP`@PH9*} zpE<&T9d_c}^_z&2qNAc+Als)%EmP+rQQxfJxf@?Uu)MRYb;FjcU*ocpUa8kthncFR zU0H5j?dJKeNVCmHTYz}2)pDn3Z@ z9{-SDB3WFYqqe)#Giv(}RW{+Byx6$GigGEYb`|mcOtXZb_oB&~`}~81=79aCX-eOH z2Gi*Z5bz^0zDm4%^x=~}@tzD}u%}4{BQt9}xuAMd57Kjd>WFh%_3&1yMQtOoWZ(dd zjDSt-J5_!%Z52(|J^%ZSAm6(y-E-wlzyA;i{JnXSQEsRSxV!v$PJ}&7Zgznld^hv% zZ)@H{ml%DvdZ;0KP6DkVJ1o-YKE(3)lla{|*96cYJ8^KrB-Dc0ZD`kIm$hdNtY_@q z(p;XLARB&cb$^CDLk1qg1VwAYJ|gh=G4M5h$Y{a1*n&IIHi+Q3SmqKS3j*A(dw})l z{W8n^eh9<-dB{xrNx{>M#5JK5u5JqV0N7{Lya!df~2!1%@L<{oq^L2hY;Vetf!zIsQA~Qf|H*zF^%5;baL&>55_jOtYlHR}B%CE?+XK z9e4)7wkLTg$&iUp!tGR&?jV6>y^Ha5#+w(%mpJg_r!EMl*n^&WXhu*fQC^Cau5@J) z(!DsESlJZpH{2CQoMoPbA3F9cOng6<)?kWiZmlcR54*%@8lsCn!X`l;ZR`@2DJ z)dNOPPBNB=D_Htl7oK5Vc{7|@xSs_OGs>QP2<;v=*555?Lamo_>wEvosu&$nID}Fy zuITdZSI|Svz|1)HdFoEmO#*WeV*W)7+})h1hH3VpMA`RQt#N~vjYM9I@iGg>8l;z1 z`*dxkk^=M~=>wLhq_5}baGpY=mJa*lF~N!xHC(jgL2X4oZnEQsV33}woK!l8lLO^V zNZV2Zie=Zc93Vq&` znG4*q7$LT~AYB+{!2A!~)Z`N=r~3KU2YiB7n~*McL8`@he$Gw5C4PNKGhx32?7Ab1{OZiQ`sg4%o_g%d~J|^%xjP~pvn0TmE84-Wl zNxXoIx^UB%ZGpwt2Q)2wjH$lo9}$dcOG;&`J{Atfx3AE9nXk}$+U;*t`dp#h6D7pO zdMu%2M@T6u?WIXzjZ{P3-NIksGlpukRPIIvwf__R9YldyGN6l;HuJ}Ck0wXw$=^2X zeHFItTOJi6KO?3mT(Ii`TQ<$)UbnU6RY?BK!hGQ_))e3Q*izaNMhPBIXO&v zRJ4_F9Ru{2$T@F-yO}jmH5k6cmGL?b-=Whu6Uz?Oovp!E2zs+@7O9|8kO$=&v`%*K z+ORFwo+w%^Crf{%LON?o^n+r%QJtR*gJCbn=%Fhs&L2r{ZoqQpRk0LGZJ`{0r9dy0 z?}fu*N8hMY%Q#l(CjOt`Gk<|Ut>_+fSbyUU0G=UqhRe8!~AVE;R>Wm4P^=>#+{feN|r?LHNbhW@G~D@}Jyivm&-o zhYSrqCyPe((gYH6rESr{gXc>KT4?!I;Q4E}DJ&r8CKpBi0bxfyFYaCcN4WnlS$L$9mL-bPS76lC#FR6~ z%p3zR_28jQ^Ex#QDy8g?c9;?UT+v!}m_A3BwV#_)RQ2_RsB)!3k)b5q7y^3&0{r@Y z{-Kto2m@&8&j;~;yWvSfc;t|MklC!3PGbF#fPU#2T*5%`xzt9oX4s&zPBy(n~d*$lh&k)eU;p zUB|Z)4M#xpiv5!%;NE98DgjGbC*;uZry_oeotjfMIyMKLK|nohSVeK%K~$KSkCzfz)h!2TuTq3qN1P9hb5b=zb)0V6`ztn=1n!JW{xm$g}*nw zVUI-R^?FD0&hZx*K}G-A8dPSnKWA!eqLQXRg>HGOs2WC?N%7T#h8URFiAoL1I0}_; zCagd&J}jAPACMf(pUy}XdXBZCM_AamKN_f4??)AcvJv__f)F~0o?u@LOvEk)tK&+i z!;8HT7RF=_a}rQmVD0dhj`3@dYJI8*n+wDCq1Q1dnE>)Z-I=#nfwgGIjI&>5nu0Y- zuIfI+k3K{8hEqF7xutT>P}LigHOLK0V>u(;s8)kT7~@79D1yzeNUXKpsm?A{ zI5mS#WmWi!B!~YY7&ceTG0((61(QUl_Ugzqbw~7`4VP`8Rc!8EgCYv)#8OReiVIIS zjb$fsU43AGiX>8j$2;vw{SgcKkWcdcFBSR30uEEyLq3i_#JJg_kOxgFvbm}z5abuq zbl?3iC{9(r{A}ZzZ2Wro+PFWtoaipOTb=el#Q-OEN9A+?wlwAPk(}Ql77?!*LyHg7 zM*u^M(}DO+Vgktw^-=!5Ru_{s%l1cg#47EdM7eqFgn3GsY;c$g+io zUXA@sKdOwN>Y;T14XR)zH}%o{v8p%`(?6x$N;t!Pwvj52hdUllNJcmjFDs`O$-_i| zX_Wj8L2f@UK{@XT=s!?ptHmWo+4v*eBFJyw&sUOHrn>s9|1kflx0xC45=uJCcl6Hi zI%cSk&SMu{0Loo(R|vp9=~8#BNZ)r7#fKJ4GtG+=Sg>3XM*KEUQI%nsIy+%1&tLid zD|u1Qa^T~@C{|)pN*~r^LaJreoQwIQw3gU(nPjyI4mk z`P#f|y2PCa2Fy#0I`ovZ$2jD%QtnHR8BsQROuoB34QWL8As?HyY(|l zQkIC&vYVo_l&-dt($B?QtfS+cqZ-}sFx#kULP`~wA|%zL$eu`H6m}*y0;}cjN@iH* zDGO-y2gv7z3EFbR-Xql*B1WoYS?^3FbI)HP_7hrIB2Foc450x2n|ewiednC8zPG@! zT@{MJ?V6yED7XuF!PRWV?>@bTJ_act{`6@&`YN}8_8_1sw@?d^3+I6IJI(BSmq;2- zWPgdBM1QzS$n!fz7)~=25k&%iGm~HjzNSMiUQrQ@epn4XkXRwmFUD?^Is7R+R{f9nYQ(q;*1p41S40!&-`LM8=tC8V9{S^MUsqw#d zBr+AXR5k?By-BEk^zrJMg)hxngV=S8n+7(a6!d)uXoXRS;CRe3Qq^0UvaH5Sn%)AR zaXap}9DdZIbzTJWlnuMoQOa%*;?OeOPrW4`yB&*tdOp3v{pNVy;zw+LWH8hF<_u4x zSC;(UaoNY(yJYz^*3OPxfMr4cvjFN;&?)(M{$8W*zynsv6)o=YqZNkr88<*&7I7;H zQp?MlO`+^a7*<7jQ^lV2R0+DHEl$^-E-L4pdM z0qwTnrSWJ|x3Z&bB*-3=hvAkjlW>!2)M&)$FWY9(Fx-Vv?tps*pjLcnu64WO6jFcC zwp9z@v<0sxHf=>rPH_j46Wm!*{c-Auq4TO zq&f`55@?yY{UIY|X=|w;xvybq=NSwYwECAzF{t*h>^R*6P{ZWIFv}4U?KCvkMx|^K zuL4G?45}?E%CbtZ4Db4gPk;uStP9?n1$iM4Z8*lLhaxY3DGU^UvD#SUr+U+rIdeN8EMZe zfR%raV4TIj`LeBxlYfk$Oz9l3r4ygEY}y$_!*HFqoiY?d?;l|Y-(-hdKLqM2cq@4? z)e@D?t8ZirLscsbmF-Jkr*ME#{AeKAKzF5(Sw;10YtMZbGDBCj;luBmyLaI}D!eGEPK+XUW%-|F@Z4WAqfIFA1c3_O)2?qfBQlJ9@G;JO!F6R7x z{?EgjEhtgJ`}Nhx3I6X6E9ZZBSjDUzjlK*Z{=X+x`P2nj3FYsk#ah8>4cM6RYz>Rl znzUN@Qe**@?@AdpS3wtsKo2=TD3#(jFR>kiL2iSb1#;7{g&+J^~0w{g`8i z>ZM^;=-AcR+S(GgZv#?%wP%}-OC>?&iHJsV0d>!D%3X#?6f{dxo2`m{cO$k`QENt~ zVKWkGo66%pz3R>6FadV0FmH zqDF1Ttx*9`g%z?j%#4Jb_;^N$){Su{f*?{%_Cd;X2jV9F{f-KK&q&aQt>c$P0dkC~ zsA*}l7X>+%;zUO3oq~$hBF!#KaVTAKbD5{nXg1arqyi|)$qR?Oq8b^A1-S4C&Dcp6 z#mqROdAh2cn5}j^I0?51I7zpVIEl3g(WA06LuBsl@IP~%OT+`%}LvJ0t*6Gk1aK!Vk>fEQTZaQYMp{zazk1a%v6n=WR(pw z!&t~@Pz2)%DW{cHlzvc4Q?Cddg)iKaU@Oj>Q75M}HOBGBhIAd?h!cwLe2FS`Y zg~yf1zP*-A5HrDA(P-l|Ekq)q%GVDP7M@VfN=#?ek1{8i>i+#B+*8YmxM*T{Ak;?x zVp%08vYEG5G*d})2#5t*3v%RSWlw&-G@YVrh9*Ktpw8bv!*F9jP={>cMC zUv;wry9B4^-WDKW^r91fch)74Le6&ws=wAK;|;VURq}Vx-^x(iaOhj3ydhlc#GJsx zCQs&nyyL%tkrlh)|C((}RsOi~^{KJQuYKhtdadP45b^@O5gLb(#i_*Q6-o$?1_R&W z!Hb|OvZLSwS&x>pMB}#vlGHxQ_5alU`SuNM&TWM;JlqxvPdRyy!LL^(IUW>N!t|0* z3@*}+2>N%^e3XEqel}hW=<)K~&-^(2KlI<-e(cZsm!wGBmi^I%6(L_v4!@u8F`m(( zui<<@7%_E*L|)a??1|C^=Lk~&gAZ7>1ECX#ml7mauT|t#3X?^p>bD0`>V+nn{!MZF zsr|bx{^7ZD>I6?1k9e!m*}0kf$!VaA#1~UZExL z_cI>;pV^Es>>)*{x0Ys@0SSaP1(@?!bdX2fAvajh*(QiZEVjw~s61mU(>1&)9_{U( zQ0%^UEPdn)%z)MAfGZBN3lIKh(6rU)mXMDCIxb@5>&BGBy^hzP1-AgNz|dX`IGLjc^HqxoU0`I9a3J zd43|HFc&toPLp=vx~7|2Lq8%4#ksUt_h`DWXhA zYt`>1Ir)U&r{d`FLL}(S!DnVg7x2PLX6ENdnNCl$9v*(|g+X5VVO~+@wQ##eLGiJj z#KU*J9Khd9y5MS+T}g_H=G|h+FnFCSt1MPtmuoQJ&u4FEfzB91GNg zZX}Baf&!!9UVSIO!a!6+cZXjw4dG7xFu9-V@^F|NbeyiQ3EsXX*pahh9}!MUXr8jg zxkjS@F6&HpsDUAI23B(zbK-$RkJ!NgA%)N@7dHjexfq;F=ki+R$coEjLXUAjZHu=E z*gf3D@4gv_3w3z~g*8fmGwn806q#gzR!Ej+q|q8PMwa^4rlx@} zwJ$vE&_@H8jy#VHJK14j(v)t!IOX=?eZ284Zk<`M-n(~+u6y|QqHt~Z4mhgc_ zQTrw=*4A?3Y|V)xlO|oR@@`$12DKYj0+r#^{RQ56T4I<~l2*NO?V#Nyxi;k|IhGx| z6J&VMv!-b&`r}@#OfUKD>d=u_lGab`?R}|0Pzn#Y(SZs3MW(3iRl^`-8)hmM0il>9 zQ&aS+7om6a61`i?B1M4AqO?H$C92}9qa{%52UKRwKe!v@ zi>+(Rl#)=>fr6oxo#6s_DE56=Gx7m_EgI)q}{JEaF00TE1i z#<(?C;nKvR1a*!xN-4(1xr}_J8_zsu@oXnmnKw$YSI|gt2eL$KTz_F)JET~}H?q7_ z1=E?D#(fb(_9?+=!qmJ%+ck6{0}-M}H~?V>ZgAG#Bx{=RtmpT8g|F@~Y>q|f7S?^8 zGb%|=l*RIhFsyjAC9%2EAms`4X#OwRFVD)k4c=)uQ(`+b^+D?fC}asCoMb2%?7ZL7 zdW585En2KC60muA;w6yq5P{LUJ5@>l=;fp1r@-%W z$KCHCg4u8tHu+Ly6q+;0o>R%fq(gQT6-=)1+wB`VA+Jh-mEAVWW1r9sA~jS=XAZn* z4l>Se7DwVJZ8VhKVvMm@{uo+GCt#sTnQ+6{#0mKb2^;hOGML$uf$ZMjLX)^zJ-L9?K#Bx6Xf7O6zz z5^X@Hcc%<(WRG&97;RPIA|5vIjhXIW!zcZS?qS=n`lj-4>)Zb=d-(s@zW>)hN~M~I z=fYxb_NwLc<>wtJKfnI>&>ufES@3D9xxink&SwNF*efz<;?=PBS`lsG;c@<`-3o8xVWaVudH;54EbwcjGy6@%+hHn$ zjnUy~Vj^|5WBG%6AwEup*#IA?ENJ>BE<6~=)- zMx?hF%$*PA zBneItJ-+4STauo0Ban#;90g=o%o8~u)dm+s4xR6bn_MP>A;zj7?v=8uI7AN3JC+@t z*1c%}vh(&mwIfYV9HBjg=1OU@$T^Hdhl)o{n!+u#I7NP5LDX|$;jOt>{HXAky_(V9nU76&DmKAT{Z+gww7SkP$EJSt+Q z+46Q;^{<>~&5dxX_wElYk6}*DBWY)z`7@OrT8FqJmrpK^e@k<-Fkj8IC>&|Ar!v5g z8CDLR{|I8bT4JqGXtsMJM_3YIY)s_IjeF_c1-~bSwzA;zJCWs|k@V{|uhDd&(i$ zamC|ouNW*@MWoa7F)_4)hF)d8icSlsRtFT=GTGNgp!u_+1KX3GUfcy63{Xq7yk(b& z*%vyZ#9)Nj_0T1URRKhMy|R&b{<~9YB#n?fC~ZD%*G8;kalQNh~2aa3{8=aL+ySo{$e5w*N{U7WAYfzX!)VLb{*d5JwMI* zcM{0ce;fCLcdsJD3-{d&rQ_*CLLhuRri{p|vi=@skRE}^Z>UnUc-?mhAam(#)TVP) zvkOVu zO%-Mp*%n|1oi))6sHf*&4Dh}4T|%vjo_Wpku6^bSt9%10B_yV2^x#76dbTJrk2Fw7 z!v~|})l_Tn({4a6gWKV)R8Gh;grIA0>D@~8xPZLJT3?KaJ%g@fBe?kfh48D*5``sH z5uv+)0R!UCZcZ=}k4NHOf^Q-zG!?sYsa^OqL<&zt0E-Av*xi)L)MiDGX05%S+}3R>9k5*KlSRNzo5$v`@qMj9Pf z1cv;*pQ|NG(5U)4#WBaP0$ozJ2XYuq$}i{^gGq7z@zysjX*fELf9heU7~7_=fd;jq z^F>kuy4ER6(Wx{k3>zc#aYYtYJch+mUTQ>$kLd&;t)Gy5i+%k$*|P@VACz83Gc@k~ zIqj!7{x~_f7!P>K=^Eo_oXrjda*j(t)A+x)?xuxLSea+^c>AlVO*tSpiQ>B8X z1~-`fTBd>waAnxNiz*Fi-AHy5}cLRTy8);R_MiajC zK1Z6_XHsyU0UqXOJvq*9ke4okZ$gxLwrOk7^nHYM9zyj*i)ksR%g=KDK10#;Qk-Yx zj7)I?7Yrxx4>F>q4oUsV? zWrF{euffP2zMZ?!R4F4fo3U(JYya@s3mU-WB3lp>BBt zv1FRti>pq^dMm;}p_y2DgdTUWDp?=Q?~dckE4oOIDgW$L42}6xz2E+gPPt@_;_rH0SvU*Xk_ue@R-D8Vk3~4ls(%Lz zc%DkwWl0-L%4{1<=m_10@{MIij)BL)vPxM=LhrIDaS&WMVlN%BFLC%CmF9($7gx0i zjGsXH*LFTa-JpqY3_3WCNf;TJ24%cBhcbfs#;jXG5IJn< z&|>L{u@PxrWf#{;MTE=suC)n&rEmJV%(9f6BFp@!&^5dLUiVC8@2aGV*i&Q53dT8M zqMfpdMJQ8u2tP$z2v^($`x6;{2_`(?a@Ui`6*9=|*A}qTCwt>BY@hBJ?$-|sb1FwC zGhD$8>s7{{1g+JyuEVYfc}x89HYEdQeW)5`4GzzBPAK?P9QIL-^{C}GRbF4H`OC2tJK_?&ucG~PPvlvpG5-~AWwxRQ+NOk_ak)pB{n^BhPsay{S$D#svBoI zD&lX&d&G}7#?D$N;{(xXB>c{diloCBP|7XqtOWc{fSRq}8L{R(B!&*KZ(_~uA&!K^ zk0Jn|n{9HE5iL>1qtCfP)DEEuMa_aNn525NCk)gb;bP(|VI=bW+GX)PqmVa#8LRfw zkFmBtEDO~zpFHgJC)JCMO)S(Y+kP7AO(jm`VRIQnzdSRgDydCCT5C$3xQqG$>dDK@ zd{vn2#SFrav$*k=QmOTR*7@t9r$6UPdEKc9cpG@3^GxIrtdjjsI#2}f!A@dGg!xcj zp!2H0um~$Lb@AB>cR@bwCb)C;5@1@E)+N}s#<&h1k}h*KZC-EyN7VLL_>Dylk9&@SCcN21zSewo2%=$VOo zkckxtf2G2f&uXvq)w-`Ia|>r6B{Wb0&V(xKL( zR~MpTmkE7T4=}5}YEX4iuUP-JP8%^Ri_;KD?Ms&nxU38yA0jH9SpOtSJn!OC+@f^} zkbnDr(}rMtL%uq=)YWh?`vA)Yvz2O}m48gl_HHsa(le$fI@>hM<>i=X^=3B-hI&7i zZNJF=H2195xM4Ra-6-P4Vh}L>t)WMw=P@d&kgH$eb z<>goxAJ$0l^vfZs={`NV?CWHU30Kni%YW?b50G!CeAgkn2@9}T=2x|=af%cZ^yJlU2#; z-!y2;+LG1!382;vwfoG62*L9|?NYo!w7}U1&L4s7Lqeigt@X6B_{pYGZm?A>jWl%f8}*RM11)OC>NKO+{aZ;@ zXHHi#z`QXdwTd8lg!SfIB=w@9;z618oS$dQXP-lr{q2+ux6<<+QD!2KP>dP&FQbxb z;!^rR9ZudfqmX(%NR?6v*j<2SND-`b6-wQUsK9SH?K*Xr(l-PtYNz5ANzr<cUm*%J$xtPnqEIO@($77 zJw9b-+Oy$5JAcRMoYa})_!+t76j2PH+xr$Z`k--mj* zLT88T)1<-^Bv%JV;N&_}`i0zuSKM)owx9`6{_XIa&>c$ib$;T{vO3{%EXM=#zYxXm zFq$XT;<|y2^qC&virh_e1%`UV-(r0N%9PVA`?=Y4Ll;zv-5KK#6{@%vyaN=M$da`j zzqd1r|Bh=|KuX!wBYu=%tt8GJ>T1ypt<(Zlpp71@QLdU4PR_o$oWw?;fS|OSnq=#o z2)|&quV}7lv$#_FX;{&3fQ#XZ$(e+u32sAC?AfD_lgtu@>~i09ry_S>qvieUD}^xF zFXc1yv%d{&So0*PER6QyD*rwujRNNM9XnxLq

eMM~O)R;vc41;%A&g!``1<7qBU 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

dR{Ql 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�Zo1hG)dY<{{BF47r&gE1+gjlSbR(&D&u!^C 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

GTX!6Gu37R01 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 z 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

>?x^YQ=B;AdR`BP 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

o69cS>=LhM=#kwX|{G*r)IQGAOb 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 z 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 TwFoP@khFzVVcx@=#tyNlrOV@Qj=2lGFoF_rX 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⋘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�~@*H|(sm?yY#KC@Jysd;jrPIDyTd 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⪴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

(*4n; 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%0ASb69m|GL($_H9 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

K0RLmdiC=ayj@@y-h*!!--B!y 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 CLklDT diff --git a/lib/uploadUtilsSrc/binarySavot.jar b/lib/uploadUtilsSrc/binarySavot.jar deleted file mode 100644 index 175bfaf7b60d030f0600a80f826bbf338c8c19c3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 41844 zcmagFb95zNw>6wjM;+VfxZ|Yb6Wb@YZQHhO+qP}nM#nb3{=Ij6K?4G7rXo8JFyTh!)g!(B(I^(zSDyGt@OOw4>Fx(6zVERDsb^Uv&NSQh!W5Oh;T7 z!#(Ch{T@oQ&Hy((LP%JfIlu(;y9i+W=BKB*4qazZa7I$3D#Fp`lrZPaQ3nrgnVeWl z+F=khobMjMZ`$sdzv+Ia;}G41bFtJI5$E~U>NZ#>FWfWNdYpCV{dLFjH5+ct^#JSR z_}eU35)n77Xnj7GSghHT8q0C-LKBI#jx;Hxfb1HX zhd@=Jc<)%M`xW&$K>wg<9Ey0rghfli|z(X&+$`pIb|OENyZUia6wHv}h3{1fgLM`i7+qi?->(pbyvM`fc^O9p1tdvb)@ z39m5du2=e_k68oH0TP~>BbeH6`BR9NQib5kxGN0v-)aNS3Wnt2dDgkets>cPOr&+w zh{|5|i9lulVGWX!jhMvll2WF%N%!)fEdxQ3u6{611ISZTp|t`)v!8aqE=~MMf{AWA zV-+N`>*IHekP%Xu#)S(G3T>6T_nk^SeJ}L|rdn|Hqf||Dn{W9x6TMM#fR<&SYJ685 z;Oji;jrY|Y*!o3GNfsHZh7qcUYd%m)8Oo5fxJn9Pnt*0*zbI#mFv>0;iZn^~&EX;a zQZXs;hq=Ym!Uu&K1&7#WN5kMqxE5iZNVXsZu=T0xDT~kaMo?ydmerJKm=wPK*o1hS zc0CFs&)>`xCuV7m@}<|>2E4_Qv<~wKW_gUBEm}2J z@T14Gi+zrMRNf4MsI?;8H>IH>M#AwRW3%Pow906=4D}gK9UVF&NlOrN2;8Y-*pep- zBO^Oo_;lv=xvChKQZqzbf{fj$Vdq-Dc%~`dzu~~hj&PQTQ3qNfw1mG}w9MjWY zp-k{N|MuaSDJO7Vdj|1JJa-zw0-nyQEHl}A-+93hATLY{uKh*|j~Do8k(kMA#U-Hw zLlI+jAwiB7@{b)bu!;EXFBP?0|1;W!9M=MN$6uw8fS}eEfm{51honWca;gJeS2Ule;+_r(m7+Bq|2h*Cy2X2>5n> z`1K-+tB}{=FbL>);X)}aT_uoh%vJI?0Y%5wt8Ar7isp}osuIWD7W;&m%1zr@c0tUg z_)RzM&s+{I{n~OQ7capm`(ZY(~pNaX*jY(>U;NUs#lR|Tva(g#c5pj!Rjnd@& zBfZ{#WG=0Y*}w~t%}p3i)*f4WFpI!<0s(kfwhpgCqz~0U`0Jm$O^kFBJ_?Xy*%iC8 zV4yu4nTyWv;~)-6-%G{c7<<$1B*ve3(LXtrYgJ{1st|V6T~xun(giix-W?KGUz7a~ zTSsZ)tLoVk!z8ond7`?TZdlpNU6XBA5lvvfriQln2~+|zmMSHAwdL=8X}) z6S*?ut>n>H-zreqY*-mKyvT(7#qT~96LfTOb(Y#f|T$iLL*1X zN~g+*BF(h7!fy|IdSxbFV1CJDS5|$NwKZ2i&z66t!}*HLmU$s5rCIJ@V4YmdPzkFO zSuB?{G`~dMs=4WQiEp9w2Li7Xn7dTd;^Ud~uA?Al)Dwd0JIp4#`p_?r;|Q&Vmy_4` z=2e!^^;AnLdt{KV<63n-_Ldm~2Hg%1lfYKo&rHi?#hcyhFVrWLYtswFEsg@NmFf&0 z@he=bwO4Fkg{`6^)#du}TM#{gjn?bi^>4VLFawX{{AqRMa?g^>y(}7)iR%qaOsDiCm4N}D*`Y39t8uJn(CeUfv8Bhkp+{hZX>qHd}ARH!EtW7 z4w=seHN+{B*Wt%G8Uq4~v_|0B>-m#x07K@~Amlw1{ZJ-W$d5U}D{NH%m@G_eqEtBP z0gB}w*;Y03*rShDI#-9gSW)V#2E!j$&QvgLTpjOGf_vw+8~i_B3nPQyrV5tfOPFIf zJ}Z2)$o2pDF$NYlYty1t@Jw?%!Sx>4$at6hDgo+pzW(Z`J6i#CsF%Ka2~RT$M@1lMol7#JDS)QMJJEkzQCQ1OgSX46?vJAn_l|1*rW&m! z;bN)IWIxD@f%EQTF{31Ppr+0;o6Gp_M?24}w}0d#yQ(%=b(sy6=PuxBe#g`uI4z#Up* zZ?PQEZtem@_j}f1)>j5iLXbnM&rLJLYCGfcw(USX&U*N1R7>3|#&bh}h-0r{!`HxS z`gFBJmU3WD+wLetOPLp<^JK$7q8s*F7Jkhx{L#kOAq%*uKikT+BowWXGJ&w;XlsEr zHvxT5PA(@9<@`vU_A)=Hwun-`f}MC*?U&J=;q4Vw4wAsf7zA$^tS@xf}1Jq~G3m%)F{pz$7}+Lf05&Fgz-z*-Qq z5&FhT2w7Ab`{NCT0a$0ccv(rAjD&fsWUgaT42kV?IDbV7+#WoC$24z1N8qvV%xt2AXzMq1bx?G274U9zflB!nN>nq|*`4v9 z?_ezGGRHUq+*6L(IcGekF?3H(hq2~&BaN;HYt(g7RfSXB`Ta-8cvibA7b=KmcMc|# zI^)#XoT3wS^gp^(4Bq@YAYf_|uLdHTkSZr46fmYAFN9jSAfDW&N;b@+bw~Au~2PeX2AE5Q0 zLK-2}vf~)DzSZ+s+rQ?#+_l~=gpDhse+nbzF9ox!``6WPXj$=5--n~m@qchx87+); zNi8F4SgCk{?l&APF^2q6hG-7-OcN9V>TKt?s023$Q(;pU*$(fZZe{mYZ-h(DL%{*} z{rwxeSppHx;@Zu5hN5A)83LtwaH?ETvl*&+lD$2%p8)>T5-TD(bX*7$K%yLh!HS)o z=Ke&uMB{520aD`#h^O>i;4;MT=S+876YOFL9LnNr(G_TBm5qB72x*!9Fh&<|0ila4 zV|q`zs6%Zolq$ARm6dGeX9UH3@x_zqm)V`^orq#pe{X0y&hbVQ82^6y8&s6p&Y=Jwjs#oQyFPzka?bg?~gJ^;smUk#xuLy))@)k&*LCv*N=pY|DikcgX~g7MUa8@#=u@$ewZp#7o)cMX_M|52uV0m> zG715od(74DR5Sp8lw8&sjXSb3nvQ!2P{Xnn)GX@V5}>`{w=_c@?o8uQ@qPcKyw|dk zWo7Ws;G__&j|IdyY3nc2JnJLTQVVXOFAHOJ2p;QkQPpq#^syTL(5oSos|tc%cXulP z9Q7*Hz|t7;%ZESJv!o@_#t3y?ON>x4Hqjdr>%iuvP($dRtfaEuB#P3d(wEj!aDbw_ z8#Y%F`e{MbV9%uU@@SsLgZM|it5aJ{tlxdE+AD+fV~*A%(XTf|y>{Ew4V8n&t`1Vq z3ASkYw>l=TmFHDun#RU+8Eks?dGyje8u2IRc;evfVUNm1fNGg!U+QK<#ry6p9J#H` zHk~|?W>Zu)4r5)%wq&I{{xU6xFAP6-p`q6~wKtpcwIgyt?D+3F74l>Q&Isd)`7}`7 zO^UjS4ws#CPowUu-3|;Evuz^yJ7>(s-+mBSgTL7&r3d~5lCX?ewkXM+Br#Q?U~nA# zB76D#ev7|BIvkmd?*3@l+2H3<89_6|x>!>2(ETI3q2c#nLpB|oXot?W=@jpE%s4=F zfv-}&j^!-pT+-di=s0v^qTS&r77M8}yvZY_X+uh#+b{eOzhOkgw$NkZ#LQhrRTClo zovymFlrK_)EIIka2`xE2iZmgXN}-$oDBVefTPUAflZ1Wwa$!W#&M~wlSZe6Ia!HG+ zDi0ee9!HVk_ion5Bx@3a<{%{l19@S<6?HP&YR*RtSFnXSdnRi`Y$+r=-^p}u(p!zp zWpQoxG1r$@MIt0t%O34PScN!C+TmuMzZva8WxKLdT9ZTQ52aSyLC%EEKnRH?^^P@# z`5;ZUl*U|S!#WQ0!EaNh0~p5{dw5QYT21H+_nh~JWiK+aS=3X?^Lk3!w9?cGnlqNR z`jL;%^*+Kcz~*XBj0P5M`hcmCtkjd@5U(~UPeXjqL)iOF@fl{fuXJ!O{#C0Id5(BTK8L}aDY`OC($mbcP^@TT3B%5lhK9a|22 z^dA@$gxGoKk*^FD1aB}jyLuO{ho*o-EKnW^iL8A8Zn=asb{pMN`{amsf`9Z4+LAzF z)W4zw1L1$@8~?M;K>9!E4D$ad8UKh4|5;|RjGYAb@uCJ!|7-~33GwlcX(_}>Z!rwa z5krY1qoLA-yI4txz?+8;sXpq1yv~hi4)N>J`)%LFVSkzF&eOfc1G150nAc;27Lb~G zQ#$up4;|R>@xF1K=I$3v#4KyThJQ!VOe#&Zju$DEBZUdO$R3%v8BdAgK>{wofW$ai zGv9=dIp$o?+i{{oni}dEw08A<_#>PiGj=A+&CsNwa=6#6C@K_Mv2`FSfoN}TStA@R z8MzgfE68Xa+u~zx=8QhYp`5&6BA$Jz$p9|s5!rb@p_^Uom(itQyL7WzT)?XM(pX=0 z`U@a3-lYEt(i#2X0ZYx*!`;`jP3duw1a81HgSEbkCH!n78C&|JP%)1Jn+X@Cz2H$4 z&6Ey>y=NDieGD?>6q}vTk`ESDCc9of!?MO+K?M&F{kYa=7}2GC(~dEYN4!-tD|+ z^6c`&Z^Ah(|M7>B!x7XtpgggHRYkewOgq^`x@DD_mdd+pAW5jKUDajqL8g z?}BUrJKmveR(Nnm#2fM38p%LOsAsmTmry+^~Vp>KlW2&b^2oQV%#QA zC!6+JPp;Wdb#pwQPsP&TV0WLe*T5z$sxqk6!?UtAuI-!%*{xodO7_(X$ETBUw`onG zU@Y2oXw*ji4v*rCk7!p^wfo6@xp1a|o>#&_{ibeFwy9o#4a!il2( zsIzAgF#Ib^VAD6%UCH%N(d`N!Zd(d-49{WvaclrsP?qjkEg@vPHi&eBAkojJhJ{#FuT`FIkdn;me8c5-f6tJ_0!i&$!+VATtVze z>4m_O)Sg)3E;2oo$Aniva5x`x?mDGMpt6Aen(Yw=^C0&2B9Nha?^QfLAxPIZmri?Y z?gIsaMIiYS0SmKN*Qn%Z$5eYP?o`6s;sy!MBIvS+t4ia9%_kosYp8@*Bq{dw5l{^( z-wF4k#UWYQC07r5=chMLWGU+7WAiNb!y@ys@_aB}NfxadA`Rap2^VhreZvLD$AQy7 z9)iqPoY#H~1xDzrPP}gws%tPTtA6b9+N;%ktp|1rvCpek4{fZp<1F!n4+*9gCM#WYLm~5wCt^q>YQJCST~srN!%VJXOh*(ykjp;7 zL@>Qip!8%-&9mU-V=U+T84`9RlafEqEo7q@<;91fk9Fv~CM$iNA+-9>Bn7VlXrdlBGi8mf8Q#kD1GluU5(isyq?gx+m{(><>AdOL=5X{=aBeg> z;)Rz(0VU*Nv(p8VJl16K8${%fbZ`kWNgShGl9!XEQs4_~(-@S8u6X3v9N)WRt5cm4 zjZ9y9dfeyoq}>zcmbi~bnl+Dyn%8m^;JNN`_lof>p~hpUw&ir$)lJv?9B~eEj^BGd zHMXV4$+ntnAkBJH=J6@`!gz+10ZJ>W=Y#$bH*PU)Y+niE)ww_?_-5A|DEimYgQr5&hFP9ZXpKaL3# zL$wGfm^}{#2pO}ybD=NRg1o?XRvf$A=Xg5PolG`^iCln)>9@)l&&q+veX+h z;*+lCZvEYuF1CIcmz{#3HB`&`)m`t5T}K0@vat-TfuXqzVzyYsPtG7 zI>NJ=LN5T(VzCf7dN5qBFtaXt#%Mg9;S!4PdaY9D20h(vu2($fx(+CCE~&a2hM3Eu zXJ|oAkq1f3g*r7rZ^Z5)xVlwfI0;_CPyVv(o9QV}kG^lSGphX>rJwQC<~Yhx!@hji{C*1S4dLhNg01X5PWq>0`q0w+2RH`nSG^06@d2 zqR&0{Jwrc;T^C{NcQD`NE-*?AuIZsX={;t!AA9f^cYQs-qxFzozU=C)FsTW6jtqOM zUp=Vjl}pT{FZCchoYF~|&KQ~|s~_?#d%0jX+#Lbo=%4Y$<=3LII#z|*m5rgeRv8C+ zl05BCL}GS(U2TL+?aaTxb}UkQbcD)Y2Se1VEZ`3`1 z%HTuw*)Wg6#wN*Bt|y(xas(+f$x;GkVu@?WDr40WjOaJHeRQgNy6fwPk z@lNwKq@*+G1>$D82Xn}|VLE0EKZL%23#(j|LuDOp+KjW>yKItCxc=1aerV?bF=k@h zb9p(4;2E`DIJQV?kdnh>jE}c>-9Gl*GJiYf;rRgFLG6;8A&mKzbY4(dshn|OR#=c6 zAN1!$X>qQVtbRN#aUDQ)4U^#($3H~RY`*W8n=G+@57tHoJ8we<4#Ubgya1;xJwbIoJQ6n`3ci;_t7bynkc2hLLgf6Qkf!wAd6nj)bw;$L#<=73 z$#S&8GF$WyROaR2n`w*ChMGS!s!7J|)L&7D0ZBx>kucjRvF^tmaq@CIHR8@p9g`Tmqb#!V9cVC9WR;+vaDt*y#vfGxv4+AWN`7@bc&wAvNaDns zq_<376EfY6>(OC)BFPw^a4VR#cQOz?)ly`LGza}wESHSIhG;3IO9MeqvCmn)M^l4^ z)}N(T3Ba++sUA;uR+!ia;3cxmRWR_p7<_N(owFb?jJ)6Oe}lHV6gBGvRWQLMWk4@e zx8Os~rP2WrkTNXMlWoh9xeaR?+wD7t#4o1zL0j@N2+5zgNXUdcL&M3e!F#luj>t!| z)?6yv`t@ga5^vJ&*IE;$J`5In-%1?IiF1r#QKQ^wWD(D47regapykHR(3VCfv_P_< z*u@ zE58|xhTEQp%q6D;r{PDJ2U9-TFI&egKw<_Hc|s; zlyE47(*YY?;e5%wP}93mj)4;j;j7^tu`OnLHXa0@olld|E}wk;M#4y-t{n*%fnSU!XYHK#^%4(2)a?$g_B#X26- z+OSbEo^x^YXIhbuqc(%)&pW$lc+uieFyyH^o^fN4xhRt6mpL`6)Jc@8af?XfnBMiC!OUUV$CN=1uV0l>C8PaO#>d6Sh4q#F&a}7JD|ioLm(tA7 zne53{Q)TV?PKVwptd|B;*C2TZSnm1#rc71`s=MttznY#IDu$v$4um2};_USR6ccO) znt2>F6G>{LID639(Kprnlhmd|$`Ggwx6WlLc-9`!-5+V?Ghk_Vk7cQgcQ?#j+Q0Ll zben^O&ym~isIQf84jIm zFdwZ_t9p9wzfdevLQkXed2A+wA#UKpv2G`@RQ0OajC@sJ&W9LBrQSh#`jHy;;lP8z z)XZ6!9dhD3jHT`nl7<1vv4t%?$q$EJK+sU7;zi3QEnlhbQx{dp1Uh~ zXo?T(5LM~Awkfhi+1T;~)feqAE46})?QBhUx+L98( zSfYky0BeAZSy`R-vUCt!GaY6w`gJKNuA7DoPgb^Q#E;o13_WGtqBepH!e%wv!$-~2S;U%1^Wgh4vFtw)R>ODWn`B)JHWCD|C#Cayh z%cT2`bk#<{# zw)mpyNT6_gn+3<)kheO^B|}a2T*hZgF%)N(D(X|F>UmK^qk}iUz;3ajcPvp!Q>ol& zkDayIcCVW9OLl27h1IL_j>-b~Ll6A?=I8F3aDL{Hb5)zN`LOtAEu(s(9QX$P$iQHe zL6AFqYlT2*9(5j~WmnZ7OMu%_a`%0ZfCNod-gbAS`-p9eoU$JqAoQj*jx^!_j_MWo zQb;tm>Yl-RjrT`t&?zuG_eI2uDJO864BNL}i5o#760u)vTNCW9Ci{*5RB`{FE(rSW zgKg2a1C6fx-KpUYGF0t0c5xe2gwqAVX2KTMPUDabr$QS1tiJbE{Q6?_s+dj1E3B&S z7f&$Y3wQIbmU#Nw$rwCTMH~DB4VJtAZx;pj?L7NH0Ph+SvbJ9Kh9J3}f(f|DJF0tm z0;`-yggo1Y#5LyCjwTsauRumG{YT_;Xn?kkSOEVy*BG0)e|?wJu?Jk#%PGB!MMa0y zTmgY}xfSG1UtM?;428j@!3TcfCvX0*VlR<5l#2r+UNNX7pV^y=4b2bVDBlQMU*MkI}m6QhG!#ef_uC?VnwM_Khs)~ z_qY9=GI%k7awYN#`%G2x4Wwg^{(6nCjgKg+F;7Ua;&dVG!HOGtr^H^(6jJS4IU>CV zCo(72+5%abY4C4kChN4&`HxCA#kilUnFz7%Qod>s-6HzqJr=J14^}*{n6mrS8!u8^ zn~l9C;Sm5VzS|RlU$=215a9f}R)pa1%cE#?t=(Pd@JoW@n4Yf8xY56hBFgTI0F=Kh zuRwR>e+DMqReW?}yC5i&^cHCuIeHu4V}iS>_yZwKvdo<%z_Tw-eD-0!UJPftwLf6~ z0cu^ta2}DrK;7~$Kt1wbiaYjy1JsIE_NK;Gh6enu4u<~?tuqz>1FeBjl2HD79X~B8 z`N~PaBKbQPa%g00Yj+Cy)Wod??0k9+$TCx9E(EgQziWE#Y0o5|lxcQyI#zy`G;OC2 zkQneTQBqG$WOP0?cYj=X!vAd{9JdB3x>gVn)oD|)W2~w*bWaV_j5~}lb)72(#F-z} z9;TfoQ6FZx{@&hNMnq3>lh>JQfIVK#pcpTVV)O07n*ZII4%#`I=wT-I)9u()R*7rA zNdxW!R2g3Q0>lCwCct`@3sh65X2~E^h4kW=n~kUl9Xw2i!Q6|K{`g1&oH>{LHSWCH z^eZR)bb3;Uc6o)k@vI7sgzrj+CexTikbc?KkX zTsFO-0dy(2GP`ZDQg+aF84YzFN<`cwt(i^8Q?H=Hr#1@Due^T)owcvo!5Soo5pHx1 zhZQ$p&fVrB_-rUvvNKa)bx2t$=%`w=lx7VC)D6#KUv_b&8RR4C6a#fsb~e7DEFs-i zMv=&!%B>L#BBACyB6m3Efwv!_9%huDhF{r#*OK`K>b&jZe)&XBSn<(V=0%BXh<59Xm|NAZm=6fmuA*}61KFYIGz*%SziXPi(|HD3J)I4p1 z!M7Rr4O!C%2ai{%&yU^WWVY7%K=+AZDW-R|764Fpq4}&Kl7T2m)SuEC}yL_aUhD2ab}|DH#R-f&i&^)+bD-9{C5ka1)Z)moL%-cWq?9M|O$+obhyJ z_O9JmnduR5FW>^|KD%_Ep zD!F=%)_{3b-#f8cJX(7a9M*7cj7x?vc#%}WzN12$JC%n85yGa? zq0`TITxW%?t{=A*mjqnnWtqGVWmuE(Jy<3En;1Uc%-)-3z(eQ%V6e5tM+J3%wFJh0 z0Y(>{f7KJ{{|zwmn>zeAJWN!!bdfj4>e}|moWDoqCye0dfS1fM^=Yy?6NbUF^i5rr zZAwBvA>)vOzwl)W9W~9x`Mm&eWFw!4(}k*&D!+qqfM7Y&@v;CodQFaRf8I9p9E0zD zWEGgsK~$}t0j#&8kKbCek3HYEytiFodWLU&L6dm}n37Bxda8!0~;rD^J>mBOH+YeXR2nYtEg%S%2t|EYR(2yZtgNvY<*G`Bj2w(%SeM z5S#T2Sw#o^+96gWkW7ZbV6`n4h=bzQ!XWLBaR;6xqxGF$IG?$(s*pid?Jyhc?-*8;SLyMT!vQ7X) zNUH4ZkBahXECW&;;?RW@sdP#G1{x+6gW`xBq{KYb1t@B2`g>H5Cjp(*Ahm@bCjCX5 zglLa)xejd|K_LgKReZzHL*eeVQ$N(PTN*e8NSIbAR4-?)87h#W1Y0fB)kWkV1M-17 zww?t8*IBtAMhiuY<}ONG#60{^R3<4-<@YN6Y85ClLy^xZtM$@=c{EC@V3WFnTF~CQ zrTIaAWH2!K9Fm}0c=uqt?y6p()$wl>{iq7Th*FZqLF*8L=s_UN5O3ZHSFl3VDPF!HPcKOW@s#QVb3p%h z0tBA|PqTEuE2)rz0Ib3xqZ|bzU&=xff1RLadYGsX@~Jd7DeK3vGZ!3-Dp!xk$QPH zq;*`wYHlXDXNL7KR)ES`)9)q%$lA$h@@p*c;MHG7d*@hTd zf!uJ}$tp4x5yTpU7%T3?`K1PA`n3lR9Hqw{vyp;Tn`agSVw*FJu#i6rHnkV7Nj*|E zTdbd=D&5;T0L@OxL1YA+Xu~Vt@6n*Ma?-u|%q_BHq1 zFbd+S+0T02F(6C5gcJ?4uz)e^M7nQ#Ti>%zatZ!QI?c+QF+c$t##v7fsI-p0e{Kk} z-i2k*lhpoBieWQPV@)Rput3*djq2Q9vVo_&ftYZVQNpH+QNGp$-=?5A!BV=TA9ZvOFsX6XIC%!(HFR>2UUEZC(0^xPjl>GG@epXGIotfz+kI(|1=|;`WuN%V3p1vvr^XFte&JkcW}jHa{aJSBhDKdm)DzgCWq6 zX}Ps2?3-RCo6@}PAk@(O(;89+qtSx6b3%+H+TgRlhl^BJOHXHoWnbbQ}tgw1$Q}?(;H-ye)&dRYf z4nMUjP3bEGXTw(TM+=SuOX&A4%Aan0{_93)&v^fM=!{3xnT&+S#G7&m|4B5k+^AGHr`*2|H5-exINANXL(Np)G)bZQ_bzW`mIQn`GR@fB9BR zD~#=qYmh}8&!0Sw( zJi(GK2QxFH+0*s8mW9rO+UV+7C-muKJ{%6aRbCQ(!Wk5_t^ZO9bby~zuWr5HqTfsF z)ECM?7(#CAr@#X!-U;=XZF2m+3%Rv<(wk~pTqA4Z&~aS^+wPDBsC?>g6TzO%Ri^~9bj z-RfLUNEUeSP&<6d$UZy-o{5}{7F$qqPRKh|Egdw9$!G zaa%$T64gePe0t^5Ajm4?e#~QCBYJkEYjIE5l0keA#Dt~)lUV?v!q<-B}slTV>z<)jm^wI80}YmM-q7!@pcwnUFM zzBv%HfN}%wM)@hn--mHZ&laB+8{?MWuvB!0RU6zaQfJ|$^l`Sv4#KErV3u%%4jQ;J zXzWk!os)KC3_car%&Lk#ROgib{0v6D!ZoPG`s&`@U`|sG~WD0|JZ95^_#2^-UoceE@TN*>P$9pVVGXXoNJcMxRGAvwV;%Lht}y_b%J0Nz%DZJN-hhh!qA$caLLRJJU}=WjWim zCmWF%WQx5`dmPUCL>l=9H?QE30mvBA`lQ1w3Jk-5%lxHDL19RVdX)AWaCGw#vR4G8 zui=m{6v3}*)sibc$gN?4XGo-b>8|Imz|3dXSKG*uR$so|L#i(1=rC}Pn(e4gJ-qEk zb$SvfB1$JhGKgB~6;2M}w&Rx)FC<7@T<)y?DJFRK_P?J%#IVJOIOK|RhBx0x>?vXd z-%w3)LWROszQTq7(R|-D735a4=_}P2Dsc(=WD-CNcX=6qlqXD*UE|1jFI^#Ub6j^+s3W%RR*Nx6R2ea97F1&Ux-hTh)eef(xvdS8J&)tdc3!n8~I?_fOHL>WL z%I)g<;p(K05+#_-_*TyNw*K{3n}me&_(q2x@dE_J75P7Fm4*MM1pfVRn4Y|ewcY=~ z^%T^VaTHNMfygO@u%uG031KV^;K-n0xeIGElrwU&w2ME(T>c2E`06|4BlhvF*TE z<)!1En%8NH`n%{53pmQqVr0kBv_IM1CWDBC>76+kVftq(o_+q8#nS2zCh63qKbuG< z;#JedO5k}<_{WiP#%t)d4Nx0w>*ksLX|=otNvD)dzDmk{Qn)99p2T7FQ>G^T?L9(4 z8Y_zk_mX_gO^oc*43!rZ(RrS|4H1POdqKMuCph2&Kf82}ANtBJQ*JdnBV0I&sOa0S zz2g!|#Wrp4$*W!#avV)OwQ?-yN9J#R_0dN~bEsOm8MDho$U6=4PkM2nX_0jOMFr@# zP-Q%l#??XbMm1)%cr8Xk7$-V98xOJZtqq#KVkI-rvZsm{;3cVZPO35#{4k`?;jTlA3}9mp?+^ zPP%Cer>&mDh(&EnS-fNEdnw#I(WSb=go}oPI)x$8be2|O-&c>KSP@Dk8=))5G7~B9 zQ7s;sj=eQVL#9fX2x~5{b3kqN08&R;G;$;EXK6rwA0U0G#1H5q#_L|*xiUPF{}5t` zR0XeiJyfPle9WrP%)4CNiVSLR8Ah!mK!;miY=&DEo-=A2=|k>h+7ZrJSn15sVH9msV#aU#paH;MKh!QBh>ZC ze2P1+mKU8r=#6sw15*4aVpG$vQ*}0Cx&@1ISGUjEJgqYBPnLebqd~N@|8{xl1J-H^ z4)(1Mir@!dgBl+Su*eSdSlR_se2@XI4X2nF%?TB>D2Ubp1~y|>)sYMwPORX-hP_vi zAFYPj!#_A6t^O&qgTYw5C^M4SLLm)i!~0@`!}UNqlb(2Hctm$3drT804au)hK7bre z_R`9pV6WNm7oN2xfPY<@GXQaaO_J2cIm=vG=!dPw^=4zv_sj&C)N-GB+^63b#%<0- zMclwAVSHf5aQC*?)4sh1rv=o^E~Kaetl2lf4La@HEpY7B1gRHI~IEi$qil*HtU+}?%!P+SKNk@6(x z0t7mGd`C!j)?{Zac2V;A!5-DJYtekvx+S#w?u`)_yD$~=`aBd~q1B>y=G`)}M@m!v ze^7-yVy!7aDC$3~7+n*blsH*(7(|Bt2jI$7}?x4AEM7?&B$uw26Q} z1<3U|qwCX92E6c0C6{t>OLK7v#r=CZMoh!g&=Pb*R4mdu8}NEqMROf;VGG^IS4^qv zuk&lz^_^8WHho?|tPRds5Rg9e=PQd-GxT}@ij7BSIvfeQnWsKqaov}SI14Wb|Da&o z+r(Va-+K4?FB1O09UH*EK|w)lM?DKe0c%Se3qzOxUim7i+si9rYPPfg5fkuXViHO) zGYcd$m8ePZv#gA3GO5L=m7!MLWn!m+oXd#GQ0Rftx~`gz;AZPbsRrch4pH77`0vN_ z^kwgyz=oJ#q42L4Xq?nccX@7WZNDxxJ9~Xy6`z5$-6Tccg*ww??>H-Cz2_OXa+2C5cg)A50T8Oh~!~X7&s<*s)s_ne^G} zoqrxz+sx3(@aP!Q7|ZnFWa33i!90sh6;5R6udez^Tvz{IIZ- z6_&F)@Nz+fEZ>TjO`7P{3$Z&X2dCt${d$K|_^B^<(jKr_m*icd${2I&&%0jVdG3oczBS>Dz zk7u$t@;F8j;Uchs5`xMTs2mhcP@D~1Nu1QY(i5zz?q`IBZt-BE6ppKwnZJ#WTs+$J zc;7lR*nDZ2eHe!*hD9^6Sna}u@H#Wp=rg~^h37`d=JeCdCieX8X~rq42&~vey$e7s z-(sLw-*4gjE!4`gs7CBd!O?DTgjY1EbSyDU#R8nlWRVAYzm(+Z8zP&nrzqN(a6jj` zLz)rAum1Hx*g3C;p|oafV`7(U;CF;%aMDo2#D_|vl2djX17oPwu$CGT_CaaHi19`# z!9~trL|Y4z47g!pO9+DLDH^}(c-ho6Wou1a_Y*;iYywi5?bsR9(O2*_NlO%qAK!Y; zbU2qVM%RD?Arm+h!%D`^B5K4aP5O_vOOaf1RY4(&GKy9h6AkV5Xj7WV1LepAu;K0K z6OO&2r2x{Wd;=bKO<8^5TJpx`UvvFQ?_+(prqtuNNa| zVY5ZBu-(IBDg})@Y&ctDxs9wdcl5RppY~2Nxec0zVQ9ldI+?_rm zj5fz{z2ux+=SFey?vF%+xCgIR@3Z1wL0Q5=RO{M%VCANoo4g{acOUcPHPvnwYy_Ha zM37kw=HoKA2f4tmFUq3`!{8U|6Jk{YP9UO}4}7<@X8^N@p8EoW-0PWJ z6JpGVe?H+XEhIu%b(asTXEzFMDMyr~F1p^~JgSPXP8RZcxu=ME*|b2-NK%TS3@ zi3s9N!=UVwZ2qupy}tS zj}TzYtu;@HsVBvS$D`R*`7vC*8>yYl(3g>!a(!>6jC2+^G2sH}SNlP3o#SXGtgp?X zik}$6?Iczn&p5Z3xb97f%nbf17A82f=mR!bpMLMac*+&9>5tlNEEWY@F&wBySmz^O zgDLkNcy|Tse@d*4^~mXx)Q%xm6=b2xdP&<}N2OfE0W~eMZUqGVX4MeY8rM>;O`O?V;X! zgV6=eUC~eN*I@H2lXUeHWg0-;uS;I{YSdC~rTut>v>Ixgq0R2CS^|G|uhZ@=da7M5 z@=itu4iIW77-xn@^w7UDs9X*S$bVfH>yrVZ%hP8l_GC zT%yiiU9uCo#Fr^Mzterx|BtYD4vs9`*0^Uf$;3{_wr$(CZFlTRGO=yjwr$(y#Kr_) z&N;X0yXSuQyQlk)uIk!dwQKL)YrSi&-}Ah;VrN=-`jot-by10xSbI2ZHQfPn%g#NY z+2xPn4iw%meE&#cR;o41T3?7`*MA|7|APGA+)4foROJ2-DePay82>GIiMSb=*gIL+ z+5VSJMvRQyIzK|t_Gq#~8cG^7MLINQQ2N*oC$ zhB&z=Z3}}g*AHV}@4EwHML(Aqf`sm{RC|>j7hb*(eCvQY^4+IG$d~uRyI6j;fIX3% ze({qMy0_8^!y80`CjkpUCgNPAX?9GjK9+mIO`R{dF2Yi~tp1_9>yMH_ypHROTYQMA}sz3g()y;pZ@kFid4E~oIPvx%? zmI^u#nj))pa?(#t{@$MrG2k(4mNr-wNNWq`zD@X&`)5c=PV2^Om$oPUVrPHfeJ1X^ zOPDax&&=OPF+Nc~A%?HE3J6AT(Sr>fU9X-pJ-hBQJ+HPuKVG?fiJlb%p!>~TRzo?w z79EVhtTd*FVdfVv@+#+Js$~K?P?Zen02U)ZfqiYK7g86ef(v+j=^fTgswPQqGOq9Y^Cr#|6=h*{bJekQ? z#%rh&Btij?MX*ZQ6o4cr&n&t_=OF&IvD-J0X4o6u_Y+4t47mR1CIHJg`jZi#&~Vr@ z_Jn4jp>B6k9)QPRh)#$OD6V%@&Yg5@O3Qk@7L*!}Oalf#qxmVv4e&le>-lh@yd!{Z zpAiLdSU+0E*$VUL8gUm!e*RK%EgbEWtrUqe=`h^G%_R-BDwU1Xl9urD3S3mZo*@x(O;#n zmjFO90U(_}-IAzu1%b}Qt7T(r6LEj|g2McN*%`L|Q2UdEe8Ut=ZLG~Nu8SML; zA@bIHVe^0z5Rr~zgdTeXqSX4jzyz&DiwnTq5uqk7_$9KuWalo1mU52`-LpC<+R4Fh zEFhq&d#iHp#-cQR!{>Umr6nphq}{Cp$eu;N5OD#*y-%3DJAmIv3e?IAQHLhSwF%L zoHh*h&Id;`YxfTyY|W5iDjvZ=%IOq;GNy)m<81*|o~z^d@{oa}r=~2F=r6gSELrmPc6|utqX_%J_hv zm0vsZCrbFo<`^cN@MWGI5ElN!-w*bHCr%HZ;SWg#Z>UdBoSFz){%;5*TI9%^3dbx2 zP*Jr5GZ^8&<8CbxyUr0+`y-iaj5cxCz5(#6eOA{x>_KD)UT9JKpeE1aQS9%sBX4Af zj7D+aW-!?oVYrlwC9D8TWZ1jAukfYRsB|Kqf}Z#Ul#hPxPciO3lEGA*C~R zBGck4L7k591;d?I7HP3@cC-{aBZGzrkypqL$!NxB%%sl}us?}C+8FMLx38iQv*rhpeOR4)9@49Zsnbyiz!LoRQr+68`{I3CSGo0e_D2<_7i^j-^wku=68!0 zWQTYOKC79aDshpGl4g6L;+C!nheAYqEo}&q)#W>OLJ!qlclGR?48e$AhJ$VNQKY0a2)N_tK)ddYkH z=_A}D>SvUjHm`fsuR*>YvgoT$WWf0s`nYi| zNU2gxmptQxMMJ@Pn?>q$N*!d2{5pAMvZZeEYewwtF_Oa5uvX@((m}6yJfR%(lfZ^F zB5)6^QR~21f_c(oVxi-bBCAJ^pK`>U!#myfRK-a)lQT4s--G7YnJoLP*qPj><6H4y zxJ<-X25X4oGIzMn$IRP2N)H@GnjA^kAkm8@QN zb?p8n0T2{zKoLmoSlOt$c7t-d-^~hi8P5@Sg*i&ayNYq1#>Y_|^M9XBIQEb>TVpW4sVP zNHkXSBV|amLr4rX0Y9*@FpE;Fvt%T%Hkt6pFT+AY>2u*^uZk-Cr{^HFqTWI!Re z4hr5ukpsj1@~}R#(d(JvLdY+ztuqP=EPppB;0&wol$jj;aIwJ)|V{ zKI)cwMHPEUQdscHABep{uH_cfmh;C+cO(w6S zZplxdo*iG;Q9952nSTh0_aM|tC-Rm9!E!mZYE=y_BpWIFc9pHcyPmbxi(nrZ(P&(0L@LJk!4SK<_h@S&eU<|8?& z;RomWuIq6pR8&0QFm*yZ+|m7f%(HcB;`GUXg#$6ys8kVHldi??a=%0rb1LtgtCM>S z*Y-5Hm%Ei?hw)yka-Oa?^v-9XkK@!>oH=PYfDBTlrTsSF;>o<%1 z7xBlpki?Cp^GMbeN4O==UCE z&SfaB8RD>FxU)mB-Mv>fi3zsqq0fMBG>R#P4{*`TKD199qBjn2G_w96fE?({4XnXTF#(lOL8Ex<7U7oH$SLCS~PC& zFZF9I!k}P;9)msJ2%ZH8@DWl;Gmgw7X5VO*QB|7})xbz7f}xSRuym9&8U^hw{++M@ z>x86|9UUZU1Q@w1U!qmjJE(%pAY!O@3Tc7bnXp}VC;ujuCV82F@k zvp<^y(4awwOXPW7F1!kK$rU5iM#(BQj6u?4ep`%fn$bQPy!Jg^L?A9svpwPF*K|9% zi=A!hFl3!zcCDfKesm))+nqf;ZT?e%Rz8KNp!*fRC;!jz{hv_7f3Cs*{~L_!UrHV9 z583+hBA_2L^G={8C6t6mXp(>05+#K)VPTG+WhB(Pu@>sQwRF>>NB7SA$IkD6_7~an zduzkEUv}7epT(ZZ*!N*VwmBeANlH(Irn25Iu3q1}wmDu7ude7o(0j~&u7SE7-zEwV zYT9=WWTZDkLl&Q`9xARLrJ+-`B(eRON`!j6J2h$jE~CG*4WlGGOGy{S;X=^eC)<+| zi~F?Bp?Tf->#c28Ed?IZ8r>xi;EB}98e!$j7w{tVrPZR{Ya)$y1tb^9W(FXwtRvb!6P)BaR0e(2#_K9rvnEEFs3});z+$vyN>s&;`Crllu`W?o z%3#vZjU(Hog%OdjuUVkka-vM$M1-zI7Z%bUsfb-`%}+3SnH4^{WT_3;T#@6Qs^hHX&Oz0OvFAt z2E}SYTN0~%0H`JxRjGS%Pw&${qTdAt6&?j;lGsM7xXp>O5LK-CH{_9#8+KBJDAs9h zYeh@xG&h*&`cl}en6@lP4H^CiwY7B}t-GQ(Pb&hG3TyVn8Sjgr2_=K`C zq6fcFc7QWX3xG>cNjue~TY6U_wNDAeyE0Cj4PfEkHz1*&0Xkjjoj`5Nnrr(F1M-(T zZBrY~uVhlAH|MgXuGZA4Wjs{CZ40b^Y(&I77mSgRP6=ZF93w$kwEP*0^x+^91tIDR^e<^h7epwapx1AeOp>P@J7Brd`u6n@Ws5RBP~sD(L39+88%hu-C|4 z7GAj%zC6#oX|&Iu>mE&NwM6s9i;vo_e*)tr53rswHnqz`f?xSQL3Vi~<0iEWiEmz# zTNGYXZv!u1;ats%8fI``GETUr5qJ}CD`6l=i|3m3?D zL0K*h#vEMf45Ax0}6s-1c1lU!(z}0qPDs~!FhP{zEA=6wo#y>Pu%JR5~TE!!s{yAUV z285(Zez+)#E-glZmO7z*62l`zY*x4;aPVdi;;O9Z(=7gs0ie(i$E~@apzhX{5F1bf zkBMx5H-s|?woc`xsTSrQF7CqhxnlhIIj2?=Tiz1SirqnK3#&xfdZO3oNbPk~%(6qK zcD=YgGOIVOFd|`d`$G2QU|+|BMskEja+I4)+}372!sd3k0RPe773;W?kGUXX+LgB8 z<@w-va()5zDi`<=&t_rZ{Zz};2BF8q>G=@N#5-G<-T7*D#i+Iq1Ld-fUxOnaTbUo= z!}mHGC=j9G;2X~wMp37h#Hc7$sUVqDNSx|{*4-iSvpM!m0<12Y*M(fwswMsWS3IMR zA6C#75KQ2Q*#N+4MC_W*8tO9tR7=MEJdem71RcmxZ&z*7EU$sMwd2EjvPpWE`S8;w zB!1+o(ylYQ@;PUeQ?=PP`boRb9)MSM=Gz^;_$uL@!21sVj}DUrdi8+oSMzc6-qb%i*ot31ce$F6_BT1?0v z&rii>-=_BeL5OmvmlIR1nT|@LN^>40pF z+%{C$)7zjttVZ^Q+0vR*x$EF3(3&*9<(Xp6RsFhGcZ~?^S-t<%Cn(&bCpdUv=_oy9 zQ{Rpxs`H8Fr#maYvP{=xtOhK{VT?0}Sy8l;bepEFsSdj3)aO~bYvxcDn`vAmS)hc- z?}!__8*;88(NYz6l)Upi8GSPVBLjsycZSqQ5CRF?oZKku*&UG)!~9-LcqVJlCRtt% z*xW-~9Z0i{9G@b+(<~%3(h|Cej<^^>w-T9LqnB8-C>}m>@NBJ3@DJ!A7zSydP6|tJ zvtR*cM>MccR&FfTtB%cf`y2S_b2Y}r-+kBa5qVT!nh2xKBuSjo3E5J%*Vr-t+cyvt zJAGOdhDa-mHdZJ(n8VqpN}+wl%zZFSqMhiOu}1ckckf2!%g`a?-{_!0cSK&HhQB`l zEs#a_5H66SOHmzKide0m@v30Lvyw(9|F-M&Eu{|@>60AIn+`b{$$#KI5oLbwbITU!*Ak4S!2m1OJ=*F$#8vMp^rC zPX86qFjG%uWU=bWq}l3Y;B-33Bi6(>Obf}^Rw}Y-5KYV?-W(i4PVg{7WITj(P)AO7B=Aa+_A#Kq=@kc30 zczx65Av1m!M2W7GR__gq{!a(Kn^i3GU)!^tiGQ$x=I-XD*N!f!dSJu+6L$;U2agjM zId@`fwAefAX4POod5?uRbDTZ$dS{NhNZ{q(N%WvlZcf|p%SUYjE;YHy@+_&TsJ!Ge zL8PmZS2iOOD5fpf-E>>*PfGSWz}4MG6;{6%{J!^A5Fj?G+90s7khwzFq7;^jyi$K& zrExN-+VgDJ=-!U49k2Xp)53MFpVt$ZwvRDI>(DXKMaC|>$8HPE5X0j=cEfrnw@;Ye z#SA00M6TX1m<$V5nm8gnVhXX3SaUXFeXVDu^W{R>wk2{bF)`YBXuzcq1`qt@&~AM5 z@O*V0cJW+|?gq;80sZrU`vrn1gA8vU7SiJga-oo{r2)mm5nOFgB_e36)R0hN&>1fG z>AB3%6ujBTmUC(un@PosSciAhx%NqUFT?Oh-NXC>VaU8t0k7e?U`Cnv%o%oPoUtK# z{afd%iFN1b3MWVBWJK{uOc0=NVr}j5ne!MREz&x`bj1IOWdlD9Gdt}6N6C5gLR09Q8jo_bc!xLkncW+MJONJX`uXUU?cyxv*&-JEz2zex)s5!C%`SO|X zL|z_2%zwY2QExdpDw{?$4c!l^B`Y$yPBUr=v(1XO-8?MRcP3v42gnX%XH-97I~q2Hu9IzV})?dg9agLVB$V-b)>%0c(1fD_Fp z581cKkAV_|8SM0lM3*Gp(-KUL zoR#N}n+o7G;+YjrBZDPr7mN+yqF?A$^<-|Khzk>0+n<%EMhp`}-b{_mJtZL*`<=GO zEFkhQ(z#qQtCMk=v#>c066``*TN|?e-u=B06q-RmehEoRSnc7VK}@OKnF(siSS;Yg zo-G;ql?lseV5%cOuhu>QA(kY|uOy$nzL0GV$s&3PmNMPkMv{aSozKYxZoJaxm9O@^ zZ?PPWddb{;T$q!$gK^HvY1KaDddeKU5u%R+dN__*-cBz_G)mM1b-~&k19t}6x&kS| zGhgeDUV8)eOjelDo5C`~i#T#a2J+px>o;qKDT0d_lWv4%2_S2!!f}aRJsjQvXN=SF zC{5W-&~Y+|6zdEOIf$sZln(VOqR77`M!->ku&p(@ILsv|skzD=CQ%;;^ZwWnJQ-gQ zTEyc^4=(#t-}!C?V(e5CK^XiyN*F~odCy|4sG9Pk6XYLgf@ktStWZ(Vg>0mM_cAf@ zJ#=A1om&=qgIOwmrxtLDUCiy{`w>Ua_xh!lgc=zs5r(eQ?g(c(r~XDU?^Q$tJ&q#_ z!PTtr5SovvKRSW{VQ8e^ugw`fMT6hOUcY=DUYikiYL}XcI0Qp%Tpa>n2H^qi;WVwi z5&{zW=+PQB9*1iehq`Ati8yhz!_Umoe+oyH z4g39^pl|9ZXUCWsML}i~MMh0{I3>$zSk%d9rkb)r%e7jbcjL#Yw#~v?E>czt<=ih@BO5 zNOJJ)nLlOajwmieJX!}={C81+MhnB-0TZqTkt@%nK*k9eDfiMNhU!X#ETg4koQ@n) zS{!{g}J^*6T&ZPFs`dmAU#`Tf1FH#~+!h@BlA zu~WiGII{*mt~hET>`td@*l*vekxX$mo@){JS4$j-*->prbj$^Wl)V(ph&RG3T&_9I zFUDcwl>81;blwwE&@um1W%a@Y9>f}F6r94r)!fwkJztIw>{VH48lFb1y&uE*_~}tX zx8DfpN*<}V9E}X)lm$0=;B?P594~u3b`VI#10Q~NPG+8!Y;!(mB4z2lsD|D=!F_yr zK%m0tHMq1M=*5Vj?M1`+1mJ<+Z2KRUrxrNJuXQjL3tiOOMj5|~3Yo(lBYKj{Y?-?v zbHxV*v@vg*6Fa{Jio?;soo;>n*`axZ--cv{jp-{vc-!nkTP z>LoN?Bi$paz=IzYY29nq7E63kXs%hBUjA8k>|#Tf zF_BXQ^k#C86Wy^?Vh=)SppFVp6!y4BrWIzRCj>ID9OcxY zCe?6W!H6Z5-1$#L&RZF!MiS#|{25`?kR_lxX-GSR$g-wQVk%rsst;1F)S8nv#^%tP zB9P86Rrbk}ASU|>!bnb~0Z|!QfV8l5qTt+_Tb#vzsX$oyoKo@_5Cr6D43v`olxe7P}TFKHX0{Y?H7Y@m_ShRcF>R? zd6UIs+mIQyFt8j?^jC^1I`Z$B6}s2SedWzdMS595O%iMro3BsE51Qx}J+2qTTucr* zU2Iq=X?;ZBzNbaU<`!O8U3BSWd>b+jTY4d8D#|~#m^Tn&-pIjJCx2m&NDc1t7H#E7 zJ!v|uA7JGCvY@0KeW^qmf&w6Qh42@5gqZ%Qsi-p!r+EQ9Z-9 z_!?$hMi!?>2L{7eTfw;&SdRDCoZ6xCMSr)Yr1w6_sG_Ge-|*$7&*Jm^FcPOO z`>>q5I(q@?z(BXLO{lAKAMT{ZKf7##owV0xv`bmcNkqDvOqRCemO7KBM7O04XzbD8 zRxrYsIobj58wq?ZO{oOF=B{vuD;Q?w@1&TO#{PZqY;sRZJw~@g|GlqoP!W53DGY{x zRbTlo7vyT>tErgRCFcs(pFo1K2$p0 z;()WMxw||}0{c<oP5gSrFL}}mVH6=a6Eoh9gO*F?XJn@#>hTM9i0Sg`emEY9- z*nA+T_UcoA;SHYjr~K@OtpzqsPRL=^Mf?gbCD*e}by?&nf+lJ^Ko}D#LQU2RcE9Y= zuP(o#PsSx=b{w*db%WDvS@=e&nwLlujwkp??y)MQOYYS``ySrb zLszltPi&aJr8qq8ZmYezy7}1Tu=wd-9x&!{4>mIcvwDT^XWi22anesasZMLlYf#mU zh_aJs!GHUVyZMY?Z}Vo=LqJZ!+!pNHL(&%i#5>ORrD^dmuNqoF^cg>RsRZ%+O`iCh zycuZnK)}vg73Uypy{}9)odPv-kzjs3rEGLDjN(0$p~e?(rMfihwj|`-)vDojP5|~I zv{g!$n)xdU=mTx^;F#h&osZN^7+`Z`OrTckT9?`ypE%E(jbB&M(K$V_{+C=GT)|>L z&9&86?~6j-t}SrC{zd{`v=M~-xaYB4pIu-Ej{SQ2J$UPRBH^OO`>#LOLa-9+C3IEU zVMsdW8{p%$7&7@42jGLo3h=?1YsZ1+uJQ8uM%{emXM*)>qZWAaI*0Gl;of{~b13Wj z#~R1;39%UN$GqChBfjYlf#P6)Nk3dh*swT&3uCDreW)D+)zdtZ%p$v!IIuW!^te^^ zM%oA8%)w^UsK<#i^T6MNz;-|l1`?)imp=<@3?an;uTL06{QPN!;FTt!`;RZ9?0n&x zm5eGg7~ z>~>D!V@VnryR8GNob7n2x1n2tce33!5q*~&s(Ya4%Xgw(m7zf^b__ma=FYJnZz(}9 zkgPk!Ef9R;Ow#RUDK41j7>-FVZb^tH3gg(^!PWEh1ZiG_^!*LT7)973B3DLGL}N#V zTAGinqNN;2;z#C^qmNiEykg4Rj0`RWe-r$NgX>qSe`$Dm-r{Z(?i}4Pyz$bl60?HXlBnq3KC(0QBy{Kaw-Q?(F)VP}48rJM}GIwWF$4ycvF0 z6FbX@?W3)%u0f^dd}*)`PjtbAXRNml=i?>VmiD&3Zk-?W!LQjUQ2dc$%gccr$B~=Q zPeVNmHq8Ev(kA%f+vmN%jBoVb)y=n@Dy#LQim&CA%{iIXKhB3{y=;yxo>)*G^7B}= zF-pkC_9m%6k_Owg!VB_7COZCJUvV$$Edkf)Wi&gpGgZF(RiQ3R@?}U#A4rYFXB1PpcWD`q0+jprsnUxg=RhOxxLo>46OC*-0WA zm?4gIBWn5owBAlORHzZLuTK~ufAxzs{HAWqeUY~_iT!O;;dOf-5H_4|yntc+!jRW} z_srHmban^N=oaOPMt1YFao{+Jnhu{K?Ja&ZQ0RvA?(X{N>G6UsBQ~1tmo3_`f}Mrz zmBZ9r*Io?)N3Z>%RHTEN08sWNRfyOOW3ju3?(Xsh+t{Rm)=)=GXV8nUM176Mgld$_ z#JS_%L|S!hdFi6W8%|~2p6;~RGVzK+nT2vx)+p%Y7LIA)!M1(PScLkyLmBdMY-2$8nSi!Q zbwY5p;@t~O=b_V&+Ja= z2|hUJ)9s^=CsDzep757-i(-ZeJkg;cl7urCFxv4JjVji3_ zefGnfLGP*#@~8e&xCp~6y?(zif-SBW;f?$*x+eGq!d@?se6MfnBS!qOz3 zW!gD<94gSOwE^Lrk|;T8oRUN}zqMX_%2{uUjSj7aIX$tFBXb<{N6{fgTTGbtxx;ht zEkGx(x`an>mbysmm}1~P({+#9k-OZvu((diF7`W(&Wk2%8{OT+cwv@g&?x^z$EU1K z>wpY-(naDxTifPbdzqdkg$%s-Uu6%DLO_BoZj5g3T)FDQK7FAYym+<3=`i^_&)0Te zF}5mSj=^1mr({11*h=n|5_m;UiyakJ{SvmsE%*xAU?Vy;3Y+CUmiIL{v>ns0S}!&3r}&Cl7enu?Q`5xGUuI5o+jtKK0b#m&5>yo^~cITUNr+v@4Rb$pZc32Z)G zu7LeT6nU$qK}%L1^((u1#Y%3VX}eD)K?=I>vSD+d0ev##PxgHtZT-&Qd0`#D;P#Xi9dQ^QLb%hU_;CwZE1A)b$D&ZT z^L!ShTYr!=8li7(Yhq;yUQ$)!ATK5RM3fccQ_)pyT8<{rU#;RTS>EiL(5Sq6kClne zlnImqhJ?#ehqatbnoJGgOp3J}Z~v?hN{`_<+Oed(3b-{-ux7d1)hL3(K-_+4*q zW+(tB>kdm!kXl@rO_8;gCd=2obC>ESYU*&7sB@I&a#CC@jG>8 z%!ik^*^D`RD*^G+qIwEnK9fVFA;`emCf5YK#OQ39p!j4*$zMT#q7S|id5g)RIhV)i zy4cK98h7;cwb`7>Vc%)}_VGT`=sSqS_TH+WL~{2SnqM`Jci;R)8i77l;wF|;d?DYj zF#x7amry9<*Ci|l^SzefUACU|a8hR2zLn@VIr?Qn#JQ}3EBflFaD8L5#CnxUz5eNC z)8qolIt%GCms>}s0t4a9`lE=7yV*k=9gMVP$QUkcK1mqLa9w zCwmQiK8wa-*h`kc|+UU zpuHd^3ZZw~)l5puJ94wL3C{tu2GzT2Y;$XF{}HkvU+=lP=opQ5FXCOjHG!pw z7!#Q76%L~8DXsIvZrA_PN*_mPC^=l%;RU0S`0_tSDmC26^&1a|`U!YE34J&#K;c8+ zDOaIX2|_5f4e9h&v*vZcRKFw^dSm}hwZe|g5TN?SVt{1|6wVzOt@MN)i;^)+ z-;Kt-5rDRxg2u5EP5PvHl4UWL(G6EV;$uPd!gfcIX;=6m%zyr63wVwpSh|fEf4jmY zEsyIN)j-^M4U*f4)>4Ps*%p0PN@*)iq9waxP1^or%mY@(%KSLT>A)9`)K5l zKqw-%Tp|Gzjc=BTdvpc5i0Cdj$v z0YypOqF#2X(*0qDS;ZO$>dP)_%eFJJ`SZs=s0JK{qVykMQ?lv*a>4rg_X7!zhd7_!t6bDEps3&3uKNf^Me!X#iG@i}E)4M{5kM^uO`b?JR>OG$HhHdft6B(yAxl@!G@ zckk%Y_}+L~Qn8>@dwem zqp9&c6jNpXr9gdOioqL6a^l1hld9I5jqw7~?A$j4cy*d%SU6iL zUest!47w^62%i7~r7<8Z=0R<5wA>D7QcbF9%ZX%-i^XP40pNhu9mu1^Hh98XFB)t@ z0!zWqnwgl3TOV)N&%ENKaEp%Ul8S+-UAbfyRt6WC+7B2MD=AQzqHq?C967nqhu*Z_ z)E*;MLf7I-NM-r;V-MH*&vY}d)|y=m9%K#kTkZMUrTWxZ(IiCvp95VH1)mPsIjEh1tsOV9SfbUvU=mv1hK^cVLxVqU%+aAO{NxNdR!@uME{hX z#E2xwxRZNrVpy>_Ow%qYJg7r?kUJkO{)p?l4J7 z9BbQn^m?t3efHh{zQ59QN5fL*y+02CZW(x84hMg!mckqpllbEHR;Dbyr^Fb6tDMr& zw$9jQ$X3V9<>8QIFbz+X#PVenzeah}4iAgVJ(0?WZzDl!^cWDYU}dGnyS%j8*CSR> z;(&KZkJ4H?HC2&l1j=aIH@0dRj~&7Bhsm;#+f^czk5&fdqac%(hgix zBc+y! znJaiD-N7=)n;=fjtH&g$MT6`krIE1fm3)v7JsRxkFR3idS7{8d29yjDSB?jd(vyo0 zfI;tV<+9&6dY5#;vk(jer4xsp+$UnhbnM|pv-scJ%2K^kyg5<0PkEv^92n(rBXe`O ze6XQ^U)~#`V`#dcOm!<tzivGCl71=P2bo14Dz_*LzlK7;@-QD)# z4l}z(_o&4xt_ANVJ%V>b`ZcKZ3=X%$!(EBZUNUt&lO1NJj9lyvouy(0R8MV@E4<05 zN{-R_7U)r`j0J>VS27@^uv;|YQe)X?6@=4~iKoBquG8hqw-4&R-5Qy_je^UN$-IN|{`3v6%i!`gN;pGjet{n(02gG04e?%;x1 zv`0=?EjbD|o~_B|Ks51g@%(m3$+eJOBAQt;)SAfN{w~jegZx0uEg&ZAz}Y1}TU>I_ z%=MB^QZ&KW+zDf&bcT?g28l+RoByFqcZp#?TY2bpEL60E<^&~t(kbNOf{JsPpyoK( z?!kj`sipS7kLQvFcNqi(fnUvhg773*2_G(Vd171Nn(uv8`in>z$2}`omrZQA!!fYf zzT<#hMw|G8f>8!OUO{^HeS!vwXH(9phWr6TP;<8ahgX#7v49hN!I08Of(~Y6m9G`N zD0oKu4h0v4T911P=S~D`+mB1%GYPjJwgPRKJe-*aCGbBpuzqA61KD?A+*nyToueQb zhQFIVeAGvO0a}w18PmsSFlSgWN9ov+c8H%p;GXgJ4BtYCKFF}o_(aHjhV(G_hH;+3 z;%}u;`>Ng0+_2~zX*vV9cZQ#7a<_M%-u`m^z0!G8XuTn0!TqHge}e|-TQB?mYu-ZF zq_~oAGT@vU+DX_ojbF^UxHDi0g&i+KTDw@z(>kyGyQft5oV- zo<^eDoh(|okLsuMeHg_H`f%JeXqkKkZtyD_T{PJ=`JPx zEwADlSo-t8>7ptQ?TJALQM_le_ZKue!YiaFyQ|;&bHkD(@{cez z6A0vH)z;_s*~pf-Z3kUzg~J~}QMuYokCidHG2#9P>m#&MXUyuGV#rqVcEC1%r?3O{ zmyDZzO3D|O#hzHbNIrlgRgRcT6lOe<;%nM$!4<^rm zE~e>eF*A&GeR9`b2oFIwKyi4MFq-2rFKajtSDhf8*7gOODi$ZDDCufL*_oHIeg2bx-j-$-{q;rDRiga&h77a) zzdPpA{QocNiT|~>{~vw}k#Xy?|L|M*^jj$+NVvx=!7{&O zGXaqfmrbpA7WNDdi* z*-aL~?XllIKA~#M>jiyalSg=wE1c4esuzFkqG?lu_WQmatKK@z=su{}VMsx84JV&4MHL>j4xOLDyazKCoedKQ|*pb8mm zgWn$OFg^a*MlgTrg{RjYiE7jC1F^LwEWa9ctHQN1M}*2e%XurM3vi=G z(+eqVsXnnM4l^v~G_gxZ-65%7`YzN#}Lvb-Q6JwN=U;1GIS#_(%nc&OLvDC#i2_C-se==edg>v zYp?S>yA?ebC=4`p+lMN_revg=aXEH|qyV44MM3)D~0> zWEb*NF9})EPruL}RR=#JyXSOdNTZ-Z7C@{Tw^lhW86ex@D zF&8)I1erP5YQC~s?+GWW4q-@AeuZuB%)UnRQZ_^a5#XeIisA2-5#WQ0~7;(+Xb%`~4 zI(o6^kI1V#NSjIVDS;_Tc^y>?S|e$Nz#X21Uvmk&d*Zc48L`%C{4(c;G~}KzLafcB z@_Q|eKO~->)`hL=H8Pog^zUf%1j_>?)`Y|el-7l&dmO8C54M-+1fvZY#?)xN?V+-J z4<6>cakk^6jhb*Lwk|!_Yl}TF4i@^Zmu0}D(bpep{4?Z>$YbSk89K-MKrI|zau66;dFUTUNR5)ykRu4$H@@|4QZ$!D|!>YA(O4c=*>F94e5?h4#| zpre42+e2%b;h`GF^gzw}*w*tM*P$#qLb8-fV{xxO_u*h1uWpHhf(75U zAlwOqYS!@%6;?Gj$T(S_)2!6HkSKk(y4B zi$$7PzNg&}?$cnHw_{)8b+meFX*L3ZNOzv%JF?r~Ob9M#s9Sx#g61guInWr;_#P`X z3cRjl7&rxl(72J_5F|jJVm50K82gU2)SxGNzUplnPV_0(F z@Ag76E^TR*@c~*XbLG3i0{^0aLLaE&P!$q z6HHDO(aU?5(VF-5+NSv=FAd5&HhWBlQEI}*LNlm1>(^E0=c6!ua z;f|ENlycwxVjj}aAVZ;$7$-F+C5RJtOf>8EZ&s1M|^I0k3xF0!JN4;c-8yl2Hpljm-fJC}mS3rLY@h-_H^ zC`@olR@MjWbZo{1&~|V3gymY7TfI#dlz8emS;1?)JnrtxZoL`S)63Zl89f;0Urul3 z3^!)T%~Mw9Gt(nAxKnix$A3#;cF*F23uRe)|EJ>R)MAv-foh~?O@XA) z903O|w@1oJuND+-pCe5qx9!dn8fkdkwu(p9*-jDbW{|mu&){TB){)r##o$4Cz}4@Kk^ql^t+*}xT2RP zoXvPA*-Y`qF9nFl=^z63-XVCDoT>SV4zi&%;`aF^-rEXlGU znm3g;*W&lnrH=QfC8%WvM(fCK-d5%#TJq`i$}=g5QBc185pIFwUWT-U)y`# zh;Cm=S`94k9)|QxXBYycI&9Kf`UwTss5=jzHHg2$L$phlwCdtH3p_QrV< zKrO<+*DEl+A1^!b1IPBURbaG{Q(Udi5vF>1E%A8z46*&^TDn2Gjvl)06E^4YzI00r zKg@j`!XVV;wsx^F6D|)GT8;5~T+tYHTj4Zf$>UQ7@|C$$_LJgeJ*J5RS5dIFjCEs( zt)?goRFA3mC<58VN6_($GojTk&3#6p zgwa|W(YUXSBp^xGj|boG43;x&8IhEwIuUPv%fcwzbJ~66p4gLYm0|+K4}PB#U|-eY z^HNCoW0mj65MJ|8RFzZp35e3n5Kau0l@I4x|Rj^s3?WTm&wYt zz&@R$)Y30p4I|CA9TG843Z;^JyN&Tn9gqloGkkM82b(Fm75(0)_#w;-#1Dn zQE#qq?lFAP;ExcY8rF&rU$LU@9~s5Ppr0@plau_cO?<|?;{7Uqp61BLabf9Mz4?i< zrFQo*mAb20Juk{u6}zwC8YFFIFztTm!C;ZPk~2nqqP5WQ9c0C5VqfxgpEcwrHetf% z6d?Mb+=FA$q~h)!%9QS9FwYv;b)2V~w@|0eJ+Q5TbFn~Yizl@6gP9`jr}!eSqeT7==G z^6o2M86V@&h@~#h1l4r9z(rRZCg-#c)nRi8;9(9+sW)oV%fi^UB^GkN@Wn7$qfs^1 zz&Epd<$46~*m9~_mDkW~Slp{>QdC3AWn9!el2!H0;_Qg%!BQ{OvQ1m~8&_y{nPgh< zH`e~zyY2b3B}*tsdv6xeMUfD(ctQAn*CMso4a=4TfgV%hR9T{04$LG=&;jNH!%y@` zO~c)vm#`Qb1h@K~2Dt}u9z9unq4wnnJVK!{BwQLpv5j=bv9x>l!@AhONpl81MXANl zR)>ASY>={~JE@A>P20;imf9q#Fn;1C+6P-lKXPn$ zjAkmrm+hc)sC;(^ap+W+dTgto9z*F6$@@iBPpv_WLSn85Z&w9(Stan>^gz#_Ac>W?^w zhW@yqAwlbKtw%-;3&YFhiPK;`Eo*e|!fE;ke(O|Fy}I4Whv4hwh==$A|yml=<6k2;B*8sJYzH}^wEmR6TcFRSae|f;;4gMfLIeOW57bwxkNsYT>g@1+k5U=lxkCu36_Z8Q17}bhXsGP zlWTXSWy)uyGhQY;ZW-Hx=t1pDl+!@k`@=|c0f{5;&^NUjAzGSwN*VO(wDrS!0cK@7 zGY}3Wzsc3iC);3Px##^)d0C{CesjdrgO7U()xKrcYEd0N>FiF-5id9Gt!ySv+%b?a z1oc1XZ%a4=P+;sRbuBUGUB~hVoa$3p8_#4se#l{+(T0~GC}3a zkCt5~`>JZR)8b|dB73*JZ*SGOwQ`lK4pz-R17|rWxTY6ukJzM^GVmc)48npopu_{{J8`eE2>o?(jaZWu?I)9 znVL&^E+Q^>VmQ6~CvuEc(QS7e>R#{M>}Aj`;ybA8kKxeix^F*#Y9xL?qo~Pt)kIJ?iNoBdcEH z-yDc%csqO27_rcu^3i|foeA@yecf%lEfYXDg8j=LmxP5N!9{nUU>~xpUkAyz)d~8;frBbJUM}w&j}{ z(EcY($>12{Ji&;w1hufhpfN*EZwX!?ql?IaOg`oE-tvkK;NX^fcK1RBEG9Tp7L_LSw{-V}RWfdnsLJBlQ_#LsX zFJ(=>ExV4x&*(vn+cWEy(&OQYgvpsm$;Vkk#}hmRSa+SQszDNy!@8nCA_4Q!Vxm8RtJ&?S>$!M0X4PVB4D1 zB_fq#wt4Pqu{g_@N?}DMXy5b(L%yLQqGPKI4;w-~&+cI|V*G}VeVKwd(k+vD#Mzr^ z`dDVURo@}gWdD2aP!h8MkV8D$<`gB@KaH9M5Qc@KuE1$9hZd5tczh=L)Ap-N;seA& zIo@87_}Ol3`V*HBcdWOJKM2*HzOhLUvs-+AL-j|>O7rAMb`iaxhgO}?zDr!WzrHl~ z8$zmu>WVU6j0a;Uq}hW!hRNR%=S1_!e37hy!nx}$W@Vp*S_N+hWq|-}?cHm|inf{a z?JfrlCQ9VvYO8W7jco>0eD>7eH~WF|4HLEbZniS3kU^XXp)=m^kC{canMIq#7jgr9 z8>aB-rbEcpQM^(HH*XSpVyw86-f{r)E@ZNA?U2oOlFWCdz1z_BetCMV$e8a1`Yt0A zaO{qKeybeW^R*so_)T4%V3bPyjmhYy>gumuK{N!G8tX&tr`o406hvzj{F7}$^-(sy zq*~LGq_Kl@lKX+!(=GEi8hW<`MZG^9=<}i8)JCRw5E*&?ccpMG6Ox|@{pbZ$X+<#^A>xfifS;2s>f@Fe1A_S&V%|f9Rps<%ASD~UAS3;^gS~&mdC}EIV z&V!6a&}(2C%9n9Db9`w1_;<*Q%zwwcY7FmP%AAn&-R_yk}Yq3|MkFygKYdkB3GUzfnOilOjfV2XS23N0|jy$cg(xjxLF^!MP= zVJa{1$_Fq8;0t>M9s28%__O8NJX0Ku&hx@1Wv|D-g52}>0Ptizm#^Em zO4kRtmVORC42<~)o@3(j``bn1`Y?aw0fL8z5mmraB)FxazijdFqw*_!Z%w#=@)ppfeksIbHJmzedB0l?JTU5PrWB)@pcR znSXM>qVW*^biuZ=;5WHodz}~N766~je-G*x>YsN$e}{&z6To)KE-WGFO6Y&D5nzP{ m_!SszwRK@7F;_zUyUd^{ivpd(5fI3rUyW!82pJ&QtN#G!1vmcy diff --git a/lib/uploadUtilsSrc/buildJar.xml b/lib/uploadUtilsSrc/buildJar.xml deleted file mode 100644 index de5beb6..0000000 --- a/lib/uploadUtilsSrc/buildJar.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - Création du jar (${jarDest}/${jarFile})... - - - - - - - - - - - \ No newline at end of file diff --git a/lib/uploadUtilsSrc/cds.savot.common.jar b/lib/uploadUtilsSrc/cds.savot.common.jar deleted file mode 100644 index 9966915834caa384e344a8668a5f0f2dac02be09..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1780 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�r4-Xas2Hq0!2dB`@(qAzJKYqu2}GU}A1F>}hm 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 diff --git a/lib/uploadUtilsSrc/cds.savot.writer.jar b/lib/uploadUtilsSrc/cds.savot.writer.jar deleted file mode 100644 index 92113cdb854ccfa625a7f70f37975fdc5b5feb73..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12704 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 Z013QYm;?V{ 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. * - * Copyright 2011,2013-2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomishes Rechen Institute (ARI) + * Copyright 2011,2013-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; +import java.util.Map; import java.util.Stack; +import adql.db.STCS.CoordSys; +import adql.db.STCS.Region; +import adql.db.STCS.RegionType; import adql.db.exception.UnresolvedColumnException; +import adql.db.exception.UnresolvedFunctionException; import adql.db.exception.UnresolvedIdentifiersException; import adql.db.exception.UnresolvedTableException; import adql.parser.ParseException; @@ -42,14 +50,38 @@ import adql.query.SelectItem; import adql.query.from.ADQLTable; import adql.query.from.FromContent; import adql.query.operand.ADQLColumn; +import adql.query.operand.ADQLOperand; +import adql.query.operand.StringConstant; +import adql.query.operand.UnknownType; +import adql.query.operand.function.ADQLFunction; +import adql.query.operand.function.DefaultUDF; +import adql.query.operand.function.UserDefinedFunction; +import adql.query.operand.function.geometry.BoxFunction; +import adql.query.operand.function.geometry.CircleFunction; +import adql.query.operand.function.geometry.GeometryFunction; +import adql.query.operand.function.geometry.PointFunction; +import adql.query.operand.function.geometry.PolygonFunction; +import adql.query.operand.function.geometry.RegionFunction; import adql.search.ISearchHandler; import adql.search.SearchColumnHandler; +import adql.search.SimpleReplaceHandler; import adql.search.SimpleSearchHandler; /** + * This {@link QueryChecker} implementation is able to do the following verifications on an ADQL query: + *

    + *
  1. Check the existence of all table and column references found in a query
  2. + *
  3. Resolve all unknown functions as supported User Defined Functions (UDFs)
  4. + *
  5. Check whether all used geometrical functions are supported
  6. + *
  7. Check whether all used coordinate systems are supported
  8. + *
  9. Check that types of columns and UDFs match with their context
  10. + *
+ * + *

Check tables and columns

*

- * Checks the existence of tables and columns, but also adds database metadata - * on {@link ADQLTable} and {@link ADQLColumn} instances when they are resolved. + * In addition to check the existence of tables and columns referenced in the query, + * this checked will also attach database metadata on these references ({@link ADQLTable} + * and {@link ADQLColumn} instances when they are resolved. *

* *

These information are:

@@ -59,35 +91,242 @@ import adql.search.SimpleSearchHandler; * * *

Note: - * Knowing DB metadata of {@link ADQLTable} and {@link ADQLColumn} is particularly useful for the translation of the ADQL query to SQL, because the ADQL name of columns and tables - * can be replaced in SQL by their DB name, if different. This mapping is done automatically by {@link adql.translator.PostgreSQLTranslator}. + * Knowing DB metadata of {@link ADQLTable} and {@link ADQLColumn} is particularly useful for the translation of the ADQL query to SQL, + * because the ADQL name of columns and tables can be replaced in SQL by their DB name, if different. This mapping is done automatically + * by {@link adql.translator.JDBCTranslator}. *

* * @author Grégory Mantelet (CDS;ARI) - * @version 1.2 (04/2014) + * @version 1.3 (05/2015) */ public class DBChecker implements QueryChecker { /** List of all available tables ({@link DBTable}). */ protected SearchTableList lstTables; + /**

List of all allowed geometrical functions (i.e. CONTAINS, REGION, POINT, COORD2, ...).

+ *

+ * If this list is NULL, all geometrical functions are allowed. + * However, if not, all items of this list must be the only allowed geometrical functions. + * So, if the list is empty, no such function is allowed. + *

+ * @since 1.3 */ + protected String[] allowedGeo = null; + + /**

List of all allowed coordinate systems.

+ *

+ * Each item of this list must be of the form: "{frame} {refpos} {flavor}". + * Each of these 3 items can be either of value, a list of values expressed with the syntax "({value1}|{value2}|...)" + * or a '*' to mean all possible values. + *

+ *

Note: since a default value (corresponding to the empty string - '') should always be possible for each part of a coordinate system, + * the checker will always add the default value (UNKNOWNFRAME, UNKNOWNREFPOS or SPHERICAL2) into the given list of possible values for each coord. sys. part.

+ *

+ * If this list is NULL, all coordinates systems are allowed. + * However, if not, all items of this list must be the only allowed coordinate systems. + * So, if the list is empty, none is allowed. + *

+ * @since 1.3 */ + protected String[] allowedCoordSys = null; + + /**

A regular expression built using the list of allowed coordinate systems. + * With this regex, it is possible to known whether a coordinate system expression is allowed or not.

+ *

If NULL, all coordinate systems are allowed.

+ * @since 1.3 */ + protected String coordSysRegExp = null; + + /**

List of all allowed User Defined Functions (UDFs).

+ *

+ * If this list is NULL, any encountered UDF will be allowed. + * However, if not, all items of this list must be the only allowed UDFs. + * So, if the list is empty, no UDF is allowed. + *

+ * @since 1.3 */ + protected FunctionDef[] allowedUdfs = null; + /* ************ */ /* CONSTRUCTORS */ /* ************ */ /** - * Builds a {@link DBChecker} with an empty list of tables. + *

Builds a {@link DBChecker} with an empty list of tables.

+ * + *

Verifications done by this object after creation:

+ *
    + *
  • Existence of tables and columns: NO (even unknown or fake tables and columns are allowed)
  • + *
  • Existence of User Defined Functions (UDFs): NO (any "unknown" function is allowed)
  • + *
  • Support of geometrical functions: NO (all valid geometrical functions are allowed)
  • + *
  • Support of coordinate systems: NO (all valid coordinate systems are allowed)
  • + *
*/ public DBChecker(){ - lstTables = new SearchTableList(); + this(null, null); } /** - * Builds a {@link DBChecker} with the given list of tables. + *

Builds a {@link DBChecker} with the given list of known tables.

+ * + *

Verifications done by this object after creation:

+ *
    + *
  • Existence of tables and columns: OK
  • + *
  • Existence of User Defined Functions (UDFs): NO (any "unknown" function is allowed)
  • + *
  • Support of geometrical functions: NO (all valid geometrical functions are allowed)
  • + *
  • Support of coordinate systems: NO (all valid coordinate systems are allowed)
  • + *
* * @param tables List of all available tables. */ - public DBChecker(final Collection tables){ + public DBChecker(final Collection tables){ + this(tables, null); + } + + /** + *

Builds a {@link DBChecker} with the given list of known tables and with a restricted list of user defined functions.

+ * + *

Verifications done by this object after creation:

+ *
    + *
  • Existence of tables and columns: OK
  • + *
  • Existence of User Defined Functions (UDFs): OK
  • + *
  • Support of geometrical functions: NO (all valid geometrical functions are allowed)
  • + *
  • Support of coordinate systems: NO (all valid coordinate systems are allowed)
  • + *
+ * + * @param tables List of all available tables. + * @param allowedUdfs List of all allowed user defined functions. + * If NULL, no verification will be done (and so, all UDFs are allowed). + * If empty list, no "unknown" (or UDF) is allowed. + * Note: match with items of this list are done case insensitively. + * + * @since 1.3 + */ + public DBChecker(final Collection tables, final Collection allowedUdfs){ + // Sort and store the given tables: setTables(tables); + + Object[] tmp; + int cnt; + + // Store all allowed UDFs in a sorted array: + if (allowedUdfs != null){ + // Remove all NULL and empty strings: + tmp = new FunctionDef[allowedUdfs.size()]; + cnt = 0; + for(FunctionDef udf : allowedUdfs){ + if (udf != null && udf.name.trim().length() > 0) + tmp[cnt++] = udf; + } + // make a copy of the array: + this.allowedUdfs = new FunctionDef[cnt]; + System.arraycopy(tmp, 0, this.allowedUdfs, 0, cnt); + + tmp = null; + // sort the values: + Arrays.sort(this.allowedUdfs); + } + } + + /** + *

Builds a {@link DBChecker} with the given list of known tables and with a restricted list of user defined functions.

+ * + *

Verifications done by this object after creation:

+ *
    + *
  • Existence of tables and columns: OK
  • + *
  • Existence of User Defined Functions (UDFs): NO (any "unknown" function is allowed)
  • + *
  • Support of geometrical functions: OK
  • + *
  • Support of coordinate systems: OK
  • + *
+ * + * @param tables List of all available tables. + * @param allowedGeoFcts List of all allowed geometrical functions (i.e. CONTAINS, POINT, UNION, CIRCLE, COORD1). + * If NULL, no verification will be done (and so, all geometries are allowed). + * If empty list, no geometry function is allowed. + * Note: match with items of this list are done case insensitively. + * @param allowedCoordSys List of all allowed coordinate system patterns. The syntax of a such pattern is the following: + * "{frame} {refpos} {flavor}" ; on the contrary to a coordinate system expression, here no part is optional. + * Each part of this pattern can be one the possible values (case insensitive), a list of possible values + * expressed with the syntax "({value1}|{value2}|...)", or a '*' for any valid value. + * For instance: "ICRS (GEOCENTER|heliocenter) *". + * If the given list is NULL, no verification will be done (and so, all coordinate systems are allowed). + * If it is empty, no coordinate system is allowed (except the default values - generally expressed by an empty string: ''). + * + * @since 1.3 + */ + public DBChecker(final Collection tables, final Collection allowedGeoFcts, final Collection allowedCoordSys) throws ParseException{ + this(tables, null, allowedGeoFcts, allowedCoordSys); + } + + /** + *

Builds a {@link DBChecker}.

+ * + *

Verifications done by this object after creation:

+ *
    + *
  • Existence of tables and columns: OK
  • + *
  • Existence of User Defined Functions (UDFs): OK
  • + *
  • Support of geometrical functions: OK
  • + *
  • Support of coordinate systems: OK
  • + *
+ * + * @param tables List of all available tables. + * @param allowedUdfs List of all allowed user defined functions. + * If NULL, no verification will be done (and so, all UDFs are allowed). + * If empty list, no "unknown" (or UDF) is allowed. + * Note: match with items of this list are done case insensitively. + * @param allowedGeoFcts List of all allowed geometrical functions (i.e. CONTAINS, POINT, UNION, CIRCLE, COORD1). + * If NULL, no verification will be done (and so, all geometries are allowed). + * If empty list, no geometry function is allowed. + * Note: match with items of this list are done case insensitively. + * @param allowedCoordSys List of all allowed coordinate system patterns. The syntax of a such pattern is the following: + * "{frame} {refpos} {flavor}" ; on the contrary to a coordinate system expression, here no part is optional. + * Each part of this pattern can be one the possible values (case insensitive), a list of possible values + * expressed with the syntax "({value1}|{value2}|...)", or a '*' for any valid value. + * For instance: "ICRS (GEOCENTER|heliocenter) *". + * If the given list is NULL, no verification will be done (and so, all coordinate systems are allowed). + * If it is empty, no coordinate system is allowed (except the default values - generally expressed by an empty string: ''). + * + * @since 1.3 + */ + public DBChecker(final Collection tables, final Collection allowedUdfs, final Collection allowedGeoFcts, final Collection allowedCoordSys) throws ParseException{ + // Set the list of available tables + Set the list of all known UDFs: + this(tables, allowedUdfs); + + // Set the list of allowed geometrical functions: + allowedGeo = specialSort(allowedGeoFcts); + + // Set the list of allowed coordinate systems: + this.allowedCoordSys = specialSort(allowedCoordSys); + coordSysRegExp = STCS.buildCoordSysRegExp(this.allowedCoordSys); + } + + /** + * Transform the given collection of string elements in a sorted array. + * Only non-NULL and non-empty strings are kept. + * + * @param items Items to copy and sort. + * + * @return A sorted array containing all - except NULL and empty strings - items of the given collection. + * + * @since 1.3 + */ + protected final static String[] specialSort(final Collection items){ + // Nothing to do if the array is NULL: + if (items == null) + return null; + + // Keep only valid items (not NULL and not empty string): + String[] tmp = new String[items.size()]; + int cnt = 0; + for(String item : items){ + if (item != null && item.trim().length() > 0) + tmp[cnt++] = item; + } + + // Make an adjusted array copy: + String[] copy = new String[cnt]; + System.arraycopy(tmp, 0, copy, 0, cnt); + + // Sort the values: + Arrays.sort(copy); + + return copy; } /* ****** */ @@ -98,12 +337,12 @@ public class DBChecker implements QueryChecker { * *

Note: * Only if the given collection is NOT an instance of {@link SearchTableList}, - * the collection will be copied inside a new {@link SearchTableList}. + * the collection will be copied inside a new {@link SearchTableList}, otherwise it is used as provided. *

* * @param tables List of {@link DBTable}s. */ - public final void setTables(final Collection tables){ + public final void setTables(final Collection tables){ if (tables == null) lstTables = new SearchTableList(); else if (tables instanceof SearchTableList) @@ -130,63 +369,136 @@ public class DBChecker implements QueryChecker { * @see #check(ADQLQuery, Stack) */ @Override - public void check(final ADQLQuery query) throws ParseException{ + public final void check(final ADQLQuery query) throws ParseException{ check(query, null); } /** - * Followed algorithm: - *
-	 * Map<DBTable,ADQLTable> mapTables;
-	 * 
-	 * For each ADQLTable t
-	 * 	if (t.isSubQuery())
-	 * 		dbTable = generateDBTable(t.getSubQuery, t.getAlias());
-	 * 	else
-	 * 		dbTable = resolveTable(t);
-	 * 	t.setDBLink(dbTable);
-	 * 	dbTables.put(t, dbTable);
-	 * End
-	 * 
-	 * For each SelectAllColumns c
-	 * 	table = c.getAdqlTable();
-	 * 	if (table != null){
-	 * 		dbTable = resolveTable(table);
-	 * 		if (dbTable == null)
-	 * 			dbTable = query.getFrom().getTablesByAlias(table.getTableName(), table.isCaseSensitive(IdentifierField.TABLE));
-	 *		if (dbTable == null)
-	 *			throw new UnresolvedTableException(table);
-	 * 		table.setDBLink(dbTable);
-	 * 	}
-	 * End
-	 * 
-	 * SearchColumnList list = query.getFrom().getDBColumns();
-	 * 
-	 * For each ADQLColumn c
-	 * 	dbColumn = resolveColumn(c, list);
-	 * 	c.setDBLink(dbColumn);
-	 * 	c.setAdqlTable(mapTables.get(dbColumn.getTable()));
-	 * End
-	 * 
-	 * For each ColumnReference colRef
-	 *	checkColumnReference(colRef, query.getSelect(), list);
-	 * End
-	 * 
+ *

Process several (semantic) verifications in the given ADQL query.

+ * + *

Main verifications done in this function:

+ *
    + *
  1. Existence of DB items (tables and columns)
  2. + *
  3. Semantic verification of sub-queries
  4. + *
  5. Support of every encountered User Defined Functions (UDFs - functions unknown by the syntactic parser)
  6. + *
  7. Support of every encountered geometries (functions, coordinate systems and STC-S expressions)
  8. + *
  9. Consistency of types still unknown (because the syntactic parser could not yet resolve them)
  10. + *
* * @param query The query to check. - * @param fathersList List of all columns available in the father query. + * @param fathersList List of all columns available in the father queries and that should be accessed in sub-queries. + * Each item of this stack is a list of columns available in each father-level query. + * Note: this parameter is NULL if this function is called with the root/father query as parameter. * - * @throws UnresolvedIdentifiersException An {@link UnresolvedIdentifiersException} if some tables or columns can not be resolved. + * @throws UnresolvedIdentifiersException An {@link UnresolvedIdentifiersException} if one or several of the above listed tests have detected + * some semantic errors (i.e. unresolved table, columns, function). * * @since 1.2 * - * @see #resolveTable(ADQLTable) - * @see #generateDBTable(ADQLQuery, String) - * @see #resolveColumn(ADQLColumn, SearchColumnList, Stack) - * @see #checkColumnReference(ColumnReference, ClauseSelect, SearchColumnList) + * @see #checkDBItems(ADQLQuery, Stack, UnresolvedIdentifiersException) + * @see #checkSubQueries(ADQLQuery, Stack, SearchColumnList, UnresolvedIdentifiersException) + * @see #checkUDFs(ADQLQuery, UnresolvedIdentifiersException) + * @see #checkGeometries(ADQLQuery, UnresolvedIdentifiersException) + * @see #checkTypes(ADQLQuery, UnresolvedIdentifiersException) */ - protected void check(final ADQLQuery query, Stack fathersList) throws UnresolvedIdentifiersException{ + protected void check(final ADQLQuery query, final Stack fathersList) throws UnresolvedIdentifiersException{ UnresolvedIdentifiersException errors = new UnresolvedIdentifiersException(); + + // A. Check DB items (tables and columns): + SearchColumnList availableColumns = checkDBItems(query, fathersList, errors); + + // B. Check UDFs: + if (allowedUdfs != null) + checkUDFs(query, errors); + + // C. Check geometries: + checkGeometries(query, errors); + + // D. Check types: + checkTypes(query, errors); + + // E. Check sub-queries: + checkSubQueries(query, fathersList, availableColumns, errors); + + // Throw all errors, if any: + if (errors.getNbErrors() > 0) + throw errors; + } + + /* ************************************************ */ + /* CHECKING METHODS FOR DB ITEMS (TABLES & COLUMNS) */ + /* ************************************************ */ + + /** + *

Check DB items (tables and columns) used in the given ADQL query.

+ * + *

Operations done in this function:

+ *
    + *
  1. Resolve all found tables
  2. + *
  3. Get the whole list of all available columns Note: this list is returned by this function.
  4. + *
  5. Resolve all found columns
  6. + *
+ * + * @param query Query in which the existence of DB items must be checked. + * @param fathersList List of all columns available in the father queries and that should be accessed in sub-queries. + * Each item of this stack is a list of columns available in each father-level query. + * Note: this parameter is NULL if this function is called with the root/father query as parameter. + * @param errors List of errors to complete in this function each time an unknown table or column is encountered. + * + * @return List of all columns available in the given query. + * + * @see #resolveTables(ADQLQuery, Stack, UnresolvedIdentifiersException) + * @see FromContent#getDBColumns() + * @see #resolveColumns(ADQLQuery, Stack, Map, SearchColumnList, UnresolvedIdentifiersException) + * + * @since 1.3 + */ + protected SearchColumnList checkDBItems(final ADQLQuery query, final Stack fathersList, final UnresolvedIdentifiersException errors){ + // a. Resolve all tables: + Map mapTables = resolveTables(query, fathersList, errors); + + // b. Get the list of all columns made available in the clause FROM: + SearchColumnList availableColumns; + try{ + availableColumns = query.getFrom().getDBColumns(); + }catch(ParseException pe){ + errors.addException(pe); + availableColumns = new SearchColumnList(); + } + + // c. Resolve all columns: + resolveColumns(query, fathersList, mapTables, availableColumns, errors); + + return availableColumns; + } + + /** + *

Search all table references inside the given query, resolve them against the available tables, and if there is only one match, + * attach the matching metadata to them.

+ * + * Management of sub-query tables + *

+ * If a table is not a DB table reference but a sub-query, this latter is first checked (using {@link #check(ADQLQuery, Stack)} ; + * but the father list must not contain tables of the given query, because on the same level) and then corresponding table metadata + * are generated (using {@link #generateDBTable(ADQLQuery, String)}) and attached to it. + *

+ * + * Management of "{table}.*" in the SELECT clause + *

+ * For each of this SELECT item, this function tries to resolve the table name. If only one match is found, the corresponding ADQL table object + * is got from the list of resolved tables and attached to this SELECT item (thus, the joker item will also have the good metadata, + * particularly if the referenced table is a sub-query). + *

+ * + * @param query Query in which the existence of tables must be checked. + * @param fathersList List of all columns available in the father queries and that should be accessed in sub-queries. + * Each item of this stack is a list of columns available in each father-level query. + * Note: this parameter is NULL if this function is called with the root/father query as parameter. + * @param errors List of errors to complete in this function each time an unknown table or column is encountered. + * + * @return An associative map of all the resolved tables. + */ + protected Map resolveTables(final ADQLQuery query, final Stack fathersList, final UnresolvedIdentifiersException errors){ HashMap mapTables = new HashMap(); ISearchHandler sHandler; @@ -200,14 +512,14 @@ public class DBChecker implements QueryChecker { // resolve the table: DBTable dbTable = null; if (table.isSubQuery()){ - // check the subquery tables: + // check the sub-query tables: check(table.getSubQuery(), fathersList); // generate its DBTable: dbTable = generateDBTable(table.getSubQuery(), table.getAlias()); }else{ dbTable = resolveTable(table); if (table.hasAlias()) - dbTable = dbTable.copy(dbTable.getDBName(), table.getAlias()); + dbTable = dbTable.copy(null, table.getAlias()); } // link with the matched DBTable: @@ -220,9 +532,9 @@ public class DBChecker implements QueryChecker { // Attach table information on wildcards with the syntax "{tableName}.*" of the SELECT clause: /* Note: no need to check the table name among the father tables, because there is - * no interest to select a father column in a subquery - * (which can return only one column ; besides, no aggregate is not allowed - * in subqueries).*/ + * no interest to select a father column in a sub-query + * (which can return only one column ; besides, no aggregate is allowed + * in sub-queries).*/ sHandler = new SearchWildCardHandler(); sHandler.search(query.getSelect()); for(ADQLObject result : sHandler){ @@ -230,31 +542,77 @@ public class DBChecker implements QueryChecker { SelectAllColumns wildcard = (SelectAllColumns)result; ADQLTable table = wildcard.getAdqlTable(); DBTable dbTable = null; - // First, try to resolve the table by table alias: + + // first, try to resolve the table by table alias: if (table.getTableName() != null && table.getSchemaName() == null){ ArrayList tables = query.getFrom().getTablesByAlias(table.getTableName(), table.isCaseSensitive(IdentifierField.TABLE)); if (tables.size() == 1) dbTable = tables.get(0).getDBLink(); } - // Then try to resolve the table reference by table name: + + // then try to resolve the table reference by table name: if (dbTable == null) dbTable = resolveTable(table); - // table.setDBLink(dbTable); + // set the corresponding tables among the list of resolved tables: wildcard.setAdqlTable(mapTables.get(dbTable)); }catch(ParseException pe){ errors.addException(pe); } } - // Get the list of all columns made available in the clause FROM: - SearchColumnList list; - try{ - list = query.getFrom().getDBColumns(); - }catch(ParseException pe){ - errors.addException(pe); - list = new SearchColumnList(); - } + return mapTables; + } + + /** + * Resolve the given table, that's to say search for the corresponding {@link DBTable}. + * + * @param table The table to resolve. + * + * @return The corresponding {@link DBTable} if found, null otherwise. + * + * @throws ParseException An {@link UnresolvedTableException} if the given table can't be resolved. + */ + protected DBTable resolveTable(final ADQLTable table) throws ParseException{ + ArrayList tables = lstTables.search(table); + + // good if only one table has been found: + if (tables.size() == 1) + return tables.get(0); + // but if more than one: ambiguous table name ! + else if (tables.size() > 1) + throw new UnresolvedTableException(table, tables.get(0).getADQLSchemaName() + "." + tables.get(0).getADQLName(), tables.get(1).getADQLSchemaName() + "." + tables.get(1).getADQLName()); + // otherwise (no match): unknown table ! + else + throw new UnresolvedTableException(table); + } + + /** + *

Search all column references inside the given query, resolve them thanks to the given tables' metadata, + * and if there is only one match, attach the matching metadata to them.

+ * + * Management of selected columns' references + *

+ * A column reference is not only a direct reference to a table column using a column name. + * It can also be a reference to an item of the SELECT clause (which will then call a "selected column"). + * That kind of reference can be either an index (an unsigned integer starting from 1 to N, where N is the + * number selected columns), or the name/alias of the column. + *

+ *

+ * These references are also checked, in a second step, in this function. Thus, column metadata are + * also attached to them, as common columns. + *

+ * + * @param query Query in which the existence of tables must be checked. + * @param fathersList List of all columns available in the father queries and that should be accessed in sub-queries. + * Each item of this stack is a list of columns available in each father-level query. + * Note: this parameter is NULL if this function is called with the root/father query as parameter. + * @param mapTables List of all resolved tables. + * @param list List of column metadata to complete in this function each time a column reference is resolved. + * @param errors List of errors to complete in this function each time an unknown table or column is encountered. + */ + protected void resolveColumns(final ADQLQuery query, final Stack fathersList, final Map mapTables, final SearchColumnList list, final UnresolvedIdentifiersException errors){ + ISearchHandler sHandler; // Check the existence of all columns: sHandler = new SearchColumnHandler(); @@ -272,7 +630,7 @@ public class DBChecker implements QueryChecker { } } - // Check the correctness of all column references: + // Check the correctness of all column references (= references to selected columns): /* Note: no need to provide the father tables when resolving column references, * because no father column can be used in ORDER BY and/or GROUP BY. */ sHandler = new SearchColReferenceHandler(); @@ -291,70 +649,22 @@ public class DBChecker implements QueryChecker { errors.addException(pe); } } - - // Check subqueries outside the clause FROM: - sHandler = new SearchSubQueryHandler(); - sHandler.search(query); - if (sHandler.getNbMatch() > 0){ - - // Push the list of columns in the father columns stack: - if (fathersList == null) - fathersList = new Stack(); - fathersList.push(list); - - // Check each found subquery (except the first one because it is the current query): - for(ADQLObject result : sHandler){ - try{ - check((ADQLQuery)result, fathersList); - }catch(UnresolvedIdentifiersException uie){ - Iterator itPe = uie.getErrors(); - while(itPe.hasNext()) - errors.addException(itPe.next()); - } - } - - // Pop the list of columns from the father columns stack: - fathersList.pop(); - - } - - // Throw all errors if any: - if (errors.getNbErrors() > 0) - throw errors; } /** - * Resolves the given table, that's to say searches for the corresponding {@link DBTable}. - * - * @param table The table to resolve. + *

Resolve the given column, that's to say search for the corresponding {@link DBColumn}.

* - * @return The corresponding {@link DBTable} if found, null otherwise. - * - * @throws ParseException An {@link UnresolvedTableException} if the given table can't be resolved. - */ - protected DBTable resolveTable(final ADQLTable table) throws ParseException{ - ArrayList tables = lstTables.search(table); - - // good if only one table has been found: - if (tables.size() == 1) - return tables.get(0); - // but if more than one: ambiguous table name ! - else if (tables.size() > 1) - throw new UnresolvedTableException(table, tables.get(0).getADQLSchemaName() + "." + tables.get(0).getADQLName(), tables.get(1).getADQLSchemaName() + "." + tables.get(1).getADQLName()); - // otherwise (no match): unknown table ! - else - throw new UnresolvedTableException(table); - } - - /** - *

Resolves the given column, that's to say searches for the corresponding {@link DBColumn}.

- *

The third parameter is used only if this function is called inside a subquery. In this case, - * column is tried to be resolved with the first list (dbColumns). If no match is found, - * the resolution is tried with the father columns list (fatherColumns).

+ *

+ * The third parameter is used only if this function is called inside a sub-query. In this case, + * the column is tried to be resolved with the first list (dbColumns). If no match is found, + * the resolution is tried with the father columns list (fathersList). + *

* * @param column The column to resolve. * @param dbColumns List of all available {@link DBColumn}s. - * @param fathersList List of all columns available in the father query ; a list for each father-level. + * @param fathersList List of all columns available in the father queries and that should be accessed in sub-queries. + * Each item of this stack is a list of columns available in each father-level query. + * Note: this parameter is NULL if this function is called with the root/father query as parameter. * * @return The corresponding {@link DBColumn} if found. Otherwise an exception is thrown. * @@ -386,14 +696,14 @@ public class DBChecker implements QueryChecker { } /** - * Checks whether the given column reference corresponds to a selected item (column or an expression with an alias) + * Check whether the given column reference corresponds to a selected item (column or an expression with an alias) * or to an existing column. * - * @param colRef The column reference which must be checked. - * @param select The SELECT clause of the ADQL query. - * @param dbColumns The list of all available {@link DBColumn}s. + * @param colRef The column reference which must be checked. + * @param select The SELECT clause of the ADQL query. + * @param dbColumns The list of all available columns. * - * @return The corresponding {@link DBColumn} if this reference is actually the name of a column, null otherwise. + * @return The corresponding {@link DBColumn} if this reference is actually the name of a column, null otherwise. * * @throws ParseException An {@link UnresolvedColumnException} if the given column can't be resolved * or an {@link UnresolvedTableException} if its table reference can't be resolved. @@ -431,19 +741,16 @@ public class DBChecker implements QueryChecker { } } - /* ************************************* */ - /* DBTABLE & DBCOLUMN GENERATION METHODS */ - /* ************************************* */ /** - * Generates a {@link DBTable} corresponding to the given sub-query with the given table name. - * This {@link DBTable} which contains all {@link DBColumn} returned by {@link ADQLQuery#getResultingColumns()}. + * Generate a {@link DBTable} corresponding to the given sub-query with the given table name. + * This {@link DBTable} will contain all {@link DBColumn} returned by {@link ADQLQuery#getResultingColumns()}. * * @param subQuery Sub-query in which the specified table must be searched. * @param tableName Name of the table to search. * - * @return The corresponding {@link DBTable} if the table has been found in the given sub-query, null otherwise. + * @return The corresponding {@link DBTable} if the table has been found in the given sub-query, null otherwise. * - * @throws ParseException Can be used to explain why the table has not been found. + * @throws ParseException Can be used to explain why the table has not been found. Note: not used by default. */ public static DBTable generateDBTable(final ADQLQuery subQuery, final String tableName) throws ParseException{ DefaultDBTable dbTable = new DefaultDBTable(tableName); @@ -455,6 +762,473 @@ public class DBChecker implements QueryChecker { return dbTable; } + /* ************************* */ + /* CHECKING METHODS FOR UDFs */ + /* ************************* */ + + /** + *

Search all UDFs (User Defined Functions) inside the given query, and then + * check their signature against the list of allowed UDFs.

+ * + *

Note: + * When more than one allowed function match, the function is considered as correct + * and no error is added. + * However, in case of multiple matches, the return type of matching functions could + * be different and in this case, there would be an error while checking later + * the types. In such case, throwing an error could make sense, but the user would + * then need to cast some parameters to help the parser identifying the right function. + * But the type-casting ability is not yet possible in ADQL. + *

+ * + * @param query Query in which UDFs must be checked. + * @param errors List of errors to complete in this function each time a UDF does not match to any of the allowed UDFs. + * + * @since 1.3 + */ + protected void checkUDFs(final ADQLQuery query, final UnresolvedIdentifiersException errors){ + // 1. Search all UDFs: + ISearchHandler sHandler = new SearchUDFHandler(); + sHandler.search(query); + + // If no UDF are allowed, throw immediately an error: + if (allowedUdfs.length == 0){ + for(ADQLObject result : sHandler) + errors.addException(new UnresolvedFunctionException((UserDefinedFunction)result)); + } + // 2. Try to resolve all of them: + else{ + ArrayList toResolveLater = new ArrayList(); + UserDefinedFunction udf; + int match; + BinarySearch binSearch = new BinarySearch(){ + @Override + protected int compare(UserDefinedFunction searchItem, FunctionDef arrayItem){ + return arrayItem.compareTo(searchItem) * -1; + } + }; + + // Try to resolve all the found UDFs: + /* Note: at this stage, it can happen that UDFs can not be yet resolved because the building of + * their signature depends of other UDFs. That's why, these special cases should be kept + * for a later resolution try. */ + for(ADQLObject result : sHandler){ + udf = (UserDefinedFunction)result; + // search for a match: + match = binSearch.search(udf, allowedUdfs); + // if no match... + if (match < 0){ + // ...if the type of all parameters is resolved, add an error (no match is possible): + if (isAllParamTypesResolved(udf)) + errors.addException(new UnresolvedFunctionException(udf)); // TODO Add the ADQLOperand position! + // ...otherwise, try to resolved it later (when other UDFs will be mostly resolved): + else + toResolveLater.add(udf); + } + // if there is a match, metadata may be attached (particularly if the function is built automatically by the syntactic parser): + else if (udf instanceof DefaultUDF) + ((DefaultUDF)udf).setDefinition(allowedUdfs[match]); + } + + // Try to resolve UDFs whose some parameter types are depending of other UDFs: + for(int i = 0; i < toResolveLater.size(); i++){ + udf = toResolveLater.get(i); + // search for a match: + match = binSearch.search(udf, allowedUdfs); + // if no match, add an error: + if (match < 0) + errors.addException(new UnresolvedFunctionException(udf)); // TODO Add the ADQLOperand position! + // otherwise, metadata may be attached (particularly if the function is built automatically by the syntactic parser): + else if (udf instanceof DefaultUDF) + ((DefaultUDF)udf).setDefinition(allowedUdfs[match]); + } + + // 3. Replace all the resolved DefaultUDF by an instance of the class associated with the set signature: + (new ReplaceDefaultUDFHandler(errors)).searchAndReplace(query); + } + } + + /** + *

Tell whether the type of all parameters of the given ADQL function + * is resolved.

+ * + *

A parameter type may not be resolved for 2 main reasons:

+ *
    + *
  • the parameter is a column, but this column has not been successfully resolved. Thus its type is still unknown.
  • + *
  • the parameter is a UDF, but this UDF has not been already resolved. Thus, as for the column, its return type is still unknown. + * But it could be known later if the UDF is resolved later ; a second try should be done afterwards.
  • + *
+ * + * @param fct ADQL function whose the parameters' type should be checked. + * + * @return true if the type of all parameters is known, false otherwise. + * + * @since 1.3 + */ + protected final boolean isAllParamTypesResolved(final ADQLFunction fct){ + for(ADQLOperand op : fct.getParameters()){ + if (op.isNumeric() == op.isString()) + return false; + } + return true; + } + + /* ************************************************************************************************* */ + /* METHODS CHECKING THE GEOMETRIES (geometrical functions, coordinate systems and STC-S expressions) */ + /* ************************************************************************************************* */ + + /** + *

Check all geometries.

+ * + *

Operations done in this function:

+ *
+ * + * @param query Query in which geometries must be checked. + * @param errors List of errors to complete in this function each time a geometry item is not supported. + * + * @see #resolveGeometryFunctions(ADQLQuery, BinarySearch, UnresolvedIdentifiersException) + * @see #resolveCoordinateSystems(ADQLQuery, UnresolvedIdentifiersException) + * @see #resolveSTCSExpressions(ADQLQuery, BinarySearch, UnresolvedIdentifiersException) + * + * @since 1.3 + */ + protected void checkGeometries(final ADQLQuery query, final UnresolvedIdentifiersException errors){ + BinarySearch binSearch = new BinarySearch(){ + @Override + protected int compare(String searchItem, String arrayItem){ + return searchItem.compareToIgnoreCase(arrayItem); + } + }; + + // a. Ensure that all used geometry functions are allowed: + if (allowedGeo != null) + resolveGeometryFunctions(query, binSearch, errors); + + // b. Check whether the coordinate systems are allowed: + if (allowedCoordSys != null) + resolveCoordinateSystems(query, errors); + + // c. Check all STC-S expressions (in RegionFunctions only) + the used coordinate systems (if StringConstant only): + if (allowedGeo == null || (allowedGeo.length > 0 && binSearch.search("REGION", allowedGeo) >= 0)) + resolveSTCSExpressions(query, binSearch, errors); + } + + /** + * Search for all geometrical functions and check whether they are allowed. + * + * @param query Query in which geometrical functions must be checked. + * @param errors List of errors to complete in this function each time a geometrical function is not supported. + * + * @see #checkGeometryFunction(String, ADQLFunction, BinarySearch, UnresolvedIdentifiersException) + * + * @since 1.3 + */ + protected void resolveGeometryFunctions(final ADQLQuery query, final BinarySearch binSearch, final UnresolvedIdentifiersException errors){ + ISearchHandler sHandler = new SearchGeometryHandler(); + sHandler.search(query); + + String fctName; + for(ADQLObject result : sHandler){ + fctName = result.getName(); + checkGeometryFunction(fctName, (ADQLFunction)result, binSearch, errors); + } + } + + /** + *

Check whether the specified geometrical function is allowed by this implementation.

+ * + *

Note: + * If the list of allowed geometrical functions is empty, this function will always add an errors to the given list. + * Indeed, it means that no geometrical function is allowed and so that the specified function is automatically not supported. + *

+ * + * @param fctName Name of the geometrical function to test. + * @param fct The function instance being or containing the geometrical function to check. Note: this function can be the function to test or a function embedding the function under test (i.e. RegionFunction). + * @param binSearch The object to use in order to search a function name inside the list of allowed functions. + * It is able to perform a binary search inside a sorted array of String objects. The interest of + * this object is its compare function which must be overridden and tells how to compare the item + * to search and the items of the array (basically, a non-case-sensitive comparison between 2 strings). + * @param errors List of errors to complete in this function each time a geometrical function is not supported. + * + * @since 1.3 + */ + protected void checkGeometryFunction(final String fctName, final ADQLFunction fct, final BinarySearch binSearch, final UnresolvedIdentifiersException errors){ + int match = -1; + if (allowedGeo.length != 0) + match = binSearch.search(fctName, allowedGeo); + if (match < 0) + errors.addException(new UnresolvedFunctionException("The geometrical function \"" + fctName + "\" is not available in this implementation!", fct)); + } + + /** + *

Search all explicit coordinate system declarations, check their syntax and whether they are allowed by this implementation.

+ * + *

Note: + * "explicit" means here that all {@link StringConstant} instances. Only coordinate systems expressed as string can + * be parsed and so checked. So if a coordinate system is specified by a column, no check can be done at this stage... + * it will be possible to perform such test only at the execution. + *

+ * + * @param query Query in which coordinate systems must be checked. + * @param errors List of errors to complete in this function each time a coordinate system has a wrong syntax or is not supported. + * + * @see #checkCoordinateSystem(StringConstant, UnresolvedIdentifiersException) + * + * @since 1.3 + */ + protected void resolveCoordinateSystems(final ADQLQuery query, final UnresolvedIdentifiersException errors){ + ISearchHandler sHandler = new SearchCoordSysHandler(); + sHandler.search(query); + for(ADQLObject result : sHandler) + checkCoordinateSystem((StringConstant)result, errors); + } + + /** + * Parse and then check the coordinate system contained in the given {@link StringConstant} instance. + * + * @param adqlCoordSys The {@link StringConstant} object containing the coordinate system to check. + * @param errors List of errors to complete in this function each time a coordinate system has a wrong syntax or is not supported. + * + * @see STCS#parseCoordSys(String) + * @see #checkCoordinateSystem(adql.db.STCS.CoordSys, ADQLOperand, UnresolvedIdentifiersException) + * + * @since 1.3 + */ + protected void checkCoordinateSystem(final StringConstant adqlCoordSys, final UnresolvedIdentifiersException errors){ + String coordSysStr = adqlCoordSys.getValue(); + try{ + checkCoordinateSystem(STCS.parseCoordSys(coordSysStr), adqlCoordSys, errors); + }catch(ParseException pe){ + errors.addException(new ParseException(pe.getMessage())); // TODO Missing object position! + } + } + + /** + * Check whether the given coordinate system is allowed by this implementation. + * + * @param coordSys Coordinate system to test. + * @param operand The operand representing or containing the coordinate system under test. + * @param errors List of errors to complete in this function each time a coordinate system is not supported. + * + * @since 1.3 + */ + protected void checkCoordinateSystem(final CoordSys coordSys, final ADQLOperand operand, final UnresolvedIdentifiersException errors){ + if (coordSysRegExp != null && coordSys != null && !coordSys.toFullSTCS().matches(coordSysRegExp)) + errors.addException(new ParseException("Coordinate system \"" + ((operand instanceof StringConstant) ? ((StringConstant)operand).getValue() : coordSys.toString()) + "\" (= \"" + coordSys.toFullSTCS() + "\") not allowed in this implementation.")); // TODO Missing object position! + List of accepted coordinate systems + } + + /** + *

Search all STC-S expressions inside the given query, parse them (and so check their syntax) and then determine + * whether the declared coordinate system and the expressed region are allowed in this implementation.

+ * + *

Note: + * In the current ADQL language definition, STC-S expressions can be found only as only parameter of the REGION function. + *

+ * + * @param query Query in which STC-S expressions must be checked. + * @param binSearch The object to use in order to search a region name inside the list of allowed functions/regions. + * It is able to perform a binary search inside a sorted array of String objects. The interest of + * this object is its compare function which must be overridden and tells how to compare the item + * to search and the items of the array (basically, a non-case-sensitive comparison between 2 strings). + * @param errors List of errors to complete in this function each time the STC-S syntax is wrong or each time the declared coordinate system or region is not supported. + * + * @see STCS#parseRegion(String) + * @see #checkRegion(adql.db.STCS.Region, RegionFunction, BinarySearch, UnresolvedIdentifiersException) + * + * @since 1.3 + */ + protected void resolveSTCSExpressions(final ADQLQuery query, final BinarySearch binSearch, final UnresolvedIdentifiersException errors){ + // Search REGION functions: + ISearchHandler sHandler = new SearchRegionHandler(); + sHandler.search(query); + + // Parse and check their STC-S expression: + String stcs; + Region region; + for(ADQLObject result : sHandler){ + try{ + // get the STC-S expression: + stcs = ((StringConstant)((RegionFunction)result).getParameter(0)).getValue(); + + // parse the STC-S expression (and so check the syntax): + region = STCS.parseRegion(stcs); + + // check whether the regions (this one + the possible inner ones) and the coordinate systems are allowed: + checkRegion(region, (RegionFunction)result, binSearch, errors); + }catch(ParseException pe){ + errors.addException(new ParseException(pe.getMessage())); // TODO Missing object position! + } + } + } + + /** + *

Check the given region.

+ * + *

The following points are checked in this function:

+ *
    + *
  • whether the coordinate system is allowed
  • + *
  • whether the type of region is allowed
  • + *
  • whether the inner regions are correct (here this function is called recursively on each inner region).
  • + *
+ * + * @param r The region to check. + * @param fct The REGION function containing the region to check. + * @param errors List of errors to complete in this function if the given region or its inner regions are not supported. + * + * @see #checkCoordinateSystem(adql.db.STCS.CoordSys, ADQLOperand, UnresolvedIdentifiersException) + * @see #checkGeometryFunction(String, ADQLFunction, BinarySearch, UnresolvedIdentifiersException) + * @see #checkRegion(adql.db.STCS.Region, RegionFunction, BinarySearch, UnresolvedIdentifiersException) + * + * @since 1.3 + */ + protected void checkRegion(final Region r, final RegionFunction fct, final BinarySearch binSearch, final UnresolvedIdentifiersException errors){ + if (r == null) + return; + + // Check the coordinate system (if any): + if (r.coordSys != null) + checkCoordinateSystem(r.coordSys, fct, errors); + + // Check that the region type is allowed: + if (allowedGeo != null){ + if (allowedGeo.length == 0) + errors.addException(new UnresolvedFunctionException("The region type \"" + r.type + "\" is not available in this implementation!", fct)); + else + checkGeometryFunction((r.type == RegionType.POSITION) ? "POINT" : r.type.toString(), fct, binSearch, errors); + } + + // Check all the inner regions: + if (r.regions != null){ + for(Region innerR : r.regions) + checkRegion(innerR, fct, binSearch, errors); + } + } + + /* **************************************************** */ + /* METHODS CHECKING TYPES UNKNOWN WHILE CHECKING SYNTAX */ + /* **************************************************** */ + + /** + *

Search all operands whose the type is not yet known and try to resolve it now + * and to check whether it matches the type expected by the syntactic parser.

+ * + *

+ * Only two operands may have an unresolved type: columns and user defined functions. + * Indeed, their type can be resolved only if the list of available columns and UDFs is known, + * and if columns and UDFs used in the query are resolved successfully. + *

+ * + *

+ * When an operand type is still unknown, they will own the three kinds of type and + * so this function won't raise an error: it is thus automatically on the expected type. + * This behavior is perfectly correct because if the type is not resolved + * that means the item/operand has not been resolved in the previous steps and so that + * an error about this item has already been raised. + *

+ * + *

Important note: + * This function does not check the types exactly, but just roughly by considering only three categories: + * string, numeric and geometry. + *

+ * + * @param query Query in which unknown types must be resolved and checked. + * @param errors List of errors to complete in this function each time a types does not match to the expected one. + * + * @see UnknownType + * + * @since 1.3 + */ + protected void checkTypes(final ADQLQuery query, final UnresolvedIdentifiersException errors){ + // Search all unknown types: + ISearchHandler sHandler = new SearchUnknownTypeHandler(); + sHandler.search(query); + + // Check whether their type matches the expected one: + UnknownType unknown; + for(ADQLObject result : sHandler){ + unknown = (UnknownType)result; + switch(unknown.getExpectedType()){ + case 'G': + case 'g': + if (!unknown.isGeometry()) + errors.addException(new ParseException("Type mismatch! A geometry was expected instead of \"" + unknown.toADQL() + "\".")); // TODO Add the ADQLOperand position! + break; + case 'N': + case 'n': + if (!unknown.isNumeric()) + errors.addException(new ParseException("Type mismatch! A numeric value was expected instead of \"" + unknown.toADQL() + "\".")); // TODO Add the ADQLOperand position! + break; + case 'S': + case 's': + if (!unknown.isString()) + errors.addException(new ParseException("Type mismatch! A string value was expected instead of \"" + unknown.toADQL() + "\".")); // TODO Add the ADQLOperand position! + break; + } + } + } + + /* ******************************** */ + /* METHODS CHECKING THE SUB-QUERIES */ + /* ******************************** */ + + /** + *

Search all sub-queries found in the given query but not in the clause FROM. + * These sub-queries are then checked using {@link #check(ADQLQuery, Stack)}.

+ * + * Fathers stack + *

+ * Each time a sub-query must be checked with {@link #check(ADQLQuery, Stack)}, + * the list of all columns available in each of its father queries must be provided. + * This function is composing itself this stack by adding the given list of available + * columns (= all columns resolved in the given query) at the end of the given stack. + * If this stack is given empty, then a new stack is created. + *

+ *

+ * This modification of the given stack is just the execution time of this function. + * Before returning, this function removes the last item of the stack. + *

+ * + * + * @param query Query in which sub-queries must be checked. + * @param fathersList List of all columns available in the father queries and that should be accessed in sub-queries. + * Each item of this stack is a list of columns available in each father-level query. + * Note: this parameter is NULL if this function is called with the root/father query as parameter. + * @param availableColumns List of all columns resolved in the given query. + * @param errors List of errors to complete in this function each time a semantic error is encountered. + * + * @since 1.3 + */ + protected void checkSubQueries(final ADQLQuery query, Stack fathersList, final SearchColumnList availableColumns, final UnresolvedIdentifiersException errors){ + // Check sub-queries outside the clause FROM: + ISearchHandler sHandler = new SearchSubQueryHandler(); + sHandler.search(query); + if (sHandler.getNbMatch() > 0){ + + // Push the list of columns into the father columns stack: + if (fathersList == null) + fathersList = new Stack(); + fathersList.push(availableColumns); + + // Check each found sub-query: + for(ADQLObject result : sHandler){ + try{ + check((ADQLQuery)result, fathersList); + }catch(UnresolvedIdentifiersException uie){ + Iterator itPe = uie.getErrors(); + while(itPe.hasNext()) + errors.addException(itPe.next()); + } + } + + // Pop the list of columns from the father columns stack: + fathersList.pop(); + } + } + /* *************** */ /* SEARCH HANDLERS */ /* *************** */ @@ -462,7 +1236,7 @@ public class DBChecker implements QueryChecker { * Lets searching all tables. * * @author Grégory Mantelet (CDS) - * @version 07/2011 + * @version 1.0 (07/2011) */ private static class SearchTableHandler extends SimpleSearchHandler { @Override @@ -475,7 +1249,7 @@ public class DBChecker implements QueryChecker { * Lets searching all wildcards. * * @author Grégory Mantelet (CDS) - * @version 09/2011 + * @version 1.0 (09/2011) */ private static class SearchWildCardHandler extends SimpleSearchHandler { @Override @@ -488,7 +1262,7 @@ public class DBChecker implements QueryChecker { * Lets searching column references. * * @author Grégory Mantelet (CDS) - * @version 11/2011 + * @version 1.0 (11/2011) */ private static class SearchColReferenceHandler extends SimpleSearchHandler { @Override @@ -527,4 +1301,213 @@ public class DBChecker implements QueryChecker { } } + /** + * Let searching user defined functions. + * + * @author Grégory Mantelet (ARI) + * @version 1.3 (10/2014) + * @since 1.3 + */ + private static class SearchUDFHandler extends SimpleSearchHandler { + @Override + protected boolean match(ADQLObject obj){ + return (obj instanceof UserDefinedFunction); + } + } + + /** + *

Let replacing every {@link DefaultUDF}s whose a {@link FunctionDef} is set by their corresponding {@link UserDefinedFunction} class.

+ * + *

Important note: + * If the replacer can not be created using the class returned by {@link FunctionDef#getUDFClass()}, no replacement is performed. + *

+ * + * @author Grégory Mantelet (ARI) + * @version 1.3 (02/2015) + * @since 1.3 + */ + private static class ReplaceDefaultUDFHandler extends SimpleReplaceHandler { + private final UnresolvedIdentifiersException errors; + + public ReplaceDefaultUDFHandler(final UnresolvedIdentifiersException errorsContainer){ + errors = errorsContainer; + } + + @Override + protected boolean match(ADQLObject obj){ + return (obj.getClass().getName().equals(DefaultUDF.class.getName())) && (((DefaultUDF)obj).getDefinition() != null) && (((DefaultUDF)obj).getDefinition().getUDFClass() != null); + /* Note: detection of DefaultUDF is done on the exact class name rather than using "instanceof" in order to have only direct instances of DefaultUDF, + * and not extensions of it. Indeed, DefaultUDFs are generally created automatically by the ADQLQueryFactory ; so, extensions of it can only be custom + * UserDefinedFunctions. */ + } + + @Override + protected ADQLObject getReplacer(ADQLObject objToReplace) throws UnsupportedOperationException{ + try{ + // get the associated UDF class: + Class udfClass = ((DefaultUDF)objToReplace).getDefinition().getUDFClass(); + // get the constructor with a single parameter of type ADQLOperand[]: + Constructor constructor = udfClass.getConstructor(ADQLOperand[].class); + // create a new instance of this UDF class with the operands stored in the object to replace: + return constructor.newInstance((Object)(((DefaultUDF)objToReplace).getParameters())); /* note: without this class, each item of the given array will be considered as a single parameter. */ + }catch(Exception ex){ + // IF NO INSTANCE CAN BE CREATED... + // ...keep the error for further report: + errors.addException(new UnresolvedFunctionException("Impossible to represent the function \"" + ((DefaultUDF)objToReplace).getName() + "\": the following error occured while creating this representation: \"" + ((ex instanceof InvocationTargetException) ? "[" + ex.getCause().getClass().getSimpleName() + "] " + ex.getCause().getMessage() : ex.getMessage()) + "\"", (DefaultUDF)objToReplace)); + // ...keep the same object (i.e. no replacement): + return objToReplace; + } + } + } + + /** + * Let searching geometrical functions. + * + * @author Grégory Mantelet (ARI) + * @version 1.3 (10/2014) + * @since 1.3 + */ + private static class SearchGeometryHandler extends SimpleSearchHandler { + @Override + protected boolean match(ADQLObject obj){ + return (obj instanceof GeometryFunction); + } + } + + /** + *

Let searching all ADQL objects whose the type was not known while checking the syntax of the ADQL query. + * These objects are {@link ADQLColumn}s and {@link UserDefinedFunction}s.

+ * + *

Important note: + * Only {@link UnknownType} instances having an expected type equals to 'S' (or 's' ; for string) or 'N' (or 'n' ; for numeric) + * are kept by this handler. Others are ignored. + *

+ * + * @author Grégory Mantelet (ARI) + * @version 1.3 (10/2014) + * @since 1.3 + */ + private static class SearchUnknownTypeHandler extends SimpleSearchHandler { + @Override + protected boolean match(ADQLObject obj){ + if (obj instanceof UnknownType){ + char expected = ((UnknownType)obj).getExpectedType(); + return (expected == 'G' || expected == 'g' || expected == 'S' || expected == 's' || expected == 'N' || expected == 'n'); + }else + return false; + } + } + + /** + * Let searching all explicit declaration of coordinate systems. + * So, only {@link StringConstant} objects will be returned. + * + * @author Grégory Mantelet (ARI) + * @version 1.3 (10/2014) + * @since 1.3 + */ + private static class SearchCoordSysHandler extends SimpleSearchHandler { + @Override + protected boolean match(ADQLObject obj){ + if (obj instanceof PointFunction || obj instanceof BoxFunction || obj instanceof CircleFunction || obj instanceof PolygonFunction) + return (((GeometryFunction)obj).getCoordinateSystem() instanceof StringConstant); + else + return false; + } + + @Override + protected void addMatch(ADQLObject matchObj, ADQLIterator it){ + results.add(((GeometryFunction)matchObj).getCoordinateSystem()); + } + + } + + /** + * Let searching all {@link RegionFunction}s. + * + * @author Grégory Mantelet (ARI) + * @version 1.3 (10/2014) + * @since 1.3 + */ + private static class SearchRegionHandler extends SimpleSearchHandler { + @Override + protected boolean match(ADQLObject obj){ + if (obj instanceof RegionFunction) + return (((RegionFunction)obj).getParameter(0) instanceof StringConstant); + else + return false; + } + + } + + /** + *

Implement the binary search algorithm over a sorted array.

+ * + *

+ * The only difference with the standard implementation of Java is + * that this object lets perform research with a different type + * of object than the types of array items. + *

+ * + *

+ * For that reason, the "compare" function must always be implemented. + *

+ * + * @author Grégory Mantelet (ARI) + * @version 1.3 (10/2014) + * + * @param Type of items stored in the array. + * @param Type of the item to search. + * + * @since 1.3 + */ + protected static abstract class BinarySearch< T, S > { + private int s, e, m, comp; + + /** + *

Search the given item in the given array.

+ * + *

+ * In case the given object matches to several items of the array, + * this function will return the smallest index, pointing thus to the first + * of all matches. + *

+ * + * @param searchItem Object for which a corresponding array item must be searched. + * @param array Array in which the given object must be searched. + * + * @return The array index of the first item of all matches. + */ + public int search(final S searchItem, final T[] array){ + s = 0; + e = array.length - 1; + while(s < e){ + // middle of the sorted array: + m = s + ((e - s) / 2); + // compare the fct with the middle item of the array: + comp = compare(searchItem, array[m]); + // if the fct is after, trigger the inspection of the right part of the array: + if (comp > 0) + s = m + 1; + // otherwise, the left part: + else + e = m; + } + if (s != e || compare(searchItem, array[s]) != 0) + return -1; + else + return s; + } + + /** + * Compare the search item and the array item. + * + * @param searchItem Item whose a corresponding value must be found in the array. + * @param arrayItem An item of the array. + * + * @return Negative value if searchItem is less than arrayItem, 0 if they are equals, or a positive value if searchItem is greater. + */ + protected abstract int compare(final S searchItem, final T arrayItem); + } + } diff --git a/src/adql/db/DBColumn.java b/src/adql/db/DBColumn.java index a803717..c987e06 100644 --- a/src/adql/db/DBColumn.java +++ b/src/adql/db/DBColumn.java @@ -16,7 +16,8 @@ package adql.db; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2011 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2011,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ /** @@ -27,8 +28,8 @@ package adql.db; * and corresponds to a real column in the "database" with its DB name ({@link #getDBName()}). *

* - * @author Grégory Mantelet (CDS) - * @version 08/2011 + * @author Grégory Mantelet (CDS;ARI) + * @version 1.3 (10/2014) */ public interface DBColumn { @@ -46,6 +47,19 @@ public interface DBColumn { */ public String getDBName(); + /** + *

Get the type of this column (as closed as possible from the "database" type).

+ * + *

Note: + * The returned type should be as closed as possible from a type listed by the IVOA in the TAP protocol description into the section UPLOAD. + *

+ * + * @return Its type. + * + * @since 1.3 + */ + public DBType getDatatype(); + /** * Gets the table which contains this {@link DBColumn}. * diff --git a/src/adql/db/DBCommonColumn.java b/src/adql/db/DBCommonColumn.java index fbbc73d..44c6642 100644 --- a/src/adql/db/DBCommonColumn.java +++ b/src/adql/db/DBCommonColumn.java @@ -16,12 +16,13 @@ package adql.db; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2014 - Astronomishes Rechen Institute (ARI) + * Copyright 2014-2015 - Astronomisches Rechen Institut (ARI) */ import java.util.ArrayList; import java.util.Iterator; +import adql.db.exception.UnresolvedJoinException; import adql.query.ADQLQuery; /** @@ -33,7 +34,7 @@ import adql.query.ADQLQuery; * in case of several JOINs. * * @author Grégory Mantelet (ARI) - gmantele@ari.uni-heidelberg.de - * @version 1.2 (11/2013) + * @version 1.3 (05/2015) * @since 1.2 */ public class DBCommonColumn implements DBColumn { @@ -54,8 +55,13 @@ public class DBCommonColumn implements DBColumn { * * @param leftCol Column of the left join table. May be a {@link DBCommonColumn}. * @param rightCol Column of the right join table. May be a {@link DBCommonColumn}. + * + * @throws UnresolvedJoinException If the type of the two given columns are not roughly (just testing numeric, string or geometry) compatible. */ - public DBCommonColumn(final DBColumn leftCol, final DBColumn rightCol){ + public DBCommonColumn(final DBColumn leftCol, final DBColumn rightCol) throws UnresolvedJoinException{ + // Test whether type of both columns are compatible: + if (leftCol.getDatatype() != null && rightCol.getDatatype() != null && !leftCol.getDatatype().isCompatible(rightCol.getDatatype())) + throw new UnresolvedJoinException("JOIN impossible: incompatible column types when trying to join the columns " + leftCol.getADQLName() + " (" + leftCol.getDatatype() + ") and " + rightCol.getADQLName() + " (" + rightCol.getDatatype() + ")!"); // LEFT COLUMN: if (leftCol instanceof DBCommonColumn){ @@ -83,7 +89,6 @@ public class DBCommonColumn implements DBColumn { // add the table to cover: addCoveredTable(rightCol.getTable()); } - } /** @@ -112,6 +117,11 @@ public class DBCommonColumn implements DBColumn { return generalColumnDesc.getDBName(); } + @Override + public final DBType getDatatype(){ + return generalColumnDesc.getDatatype(); + } + @Override public final DBTable getTable(){ return null; diff --git a/src/adql/db/DBTable.java b/src/adql/db/DBTable.java index 8e5d52b..f72389f 100644 --- a/src/adql/db/DBTable.java +++ b/src/adql/db/DBTable.java @@ -16,7 +16,8 @@ package adql.db; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ /** @@ -27,8 +28,8 @@ package adql.db; * and corresponds to a real table in the "database" with its DB name ({@link #getDBName()}). *

* - * @author Grégory Mantelet (CDS) - * @version 07/2011 + * @author Grégory Mantelet (CDS;ARI) + * @version 1.3 (09/2014) */ public interface DBTable extends Iterable { @@ -86,10 +87,25 @@ public interface DBTable extends Iterable { public DBColumn getColumn(String colName, boolean adqlName); /** - * Makes a copy of this instance of {@link DBTable}, with the possibility to change the DB and ADQL names. + *

Makes a copy of this instance of {@link DBTable}, with the possibility to change the DB and ADQL names.

+ * + *

IMPORTANT: + * The given DB and ADQL name may be NULL. If NULL, the copy will contain exactly the same full name (DB and/or ADQL).
+ * And they may be qualified (that's to say: prefixed by the schema name or by the catalog and schema name). It means that it is possible to + * change the catalog, schema and table name in the copy.
+ * For instance: + *

+ *
    + *
  • .copy(null, "foo") => a copy with the same full DB name, but with no ADQL catalog and schema name and with an ADQL table name equals to "foo"
  • + *
  • .copy("schema.table", ) => a copy with the same full ADQL name, but with no DB catalog name, with a DB schema name equals to "schema" and with a DB table name equals to "table"
  • + *
* * @param dbName Its new DB name. + * It may be qualified. + * It may also be NULL ; if so, the full DB name won't be different in the copy. * @param adqlName Its new ADQL name. + * It may be qualified. + * It may also be NULL ; if so, the full DB name won't be different in the copy. * * @return A modified copy of this {@link DBTable}. */ diff --git a/src/adql/db/DBType.java b/src/adql/db/DBType.java new file mode 100644 index 0000000..87f6c05 --- /dev/null +++ b/src/adql/db/DBType.java @@ -0,0 +1,150 @@ +package adql.db; + +/* + * This file is part of ADQLLibrary. + * + * ADQLLibrary is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ADQLLibrary is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with TAPLibrary. If not, see . + * + * Copyright 2014 - Astronomisches Rechen Institut (ARI) + */ + +/** + * + *

+ * Describe a full column type as it is described in the IVOA document of TAP. + * Thus, this object contains 2 attributes: type (or datatype) and length (or size). + *

+ * + *

The length/size may be not defined ; in this case, its value is set to {@link #NO_LENGTH} or is negative or null.

+ * + *

All datatypes declared in the IVOA recommendation document of TAP are listed in an enumeration type: {@link DBDatatype}. + * It is used to set the attribute type/datatype of this class.

+ * + * @author Grégory Mantelet (ARI) + * @version 1.3 (10/2014) + * @since 1.3 + */ +public class DBType { + + /** + * List of all datatypes declared in the IVOA recommendation of TAP (in the section UPLOAD). + * + * @author Grégory Mantelet (ARI) + * @version 1.3 (10/2014) + * @since 1.3 + */ + public static enum DBDatatype{ + SMALLINT, INTEGER, BIGINT, REAL, DOUBLE, BINARY, VARBINARY, CHAR, VARCHAR, BLOB, CLOB, TIMESTAMP, POINT, REGION; + } + + /** Special value in case no length/size is specified. */ + public static final int NO_LENGTH = -1; + + /** Datatype of a column. */ + public final DBDatatype type; + + /** The length parameter (only few datatypes need this parameter: char, varchar, binary and varbinary). */ + public final int length; + + /** + * Build a TAP column type by specifying a datatype. + * + * @param datatype Column datatype. + */ + public DBType(final DBDatatype datatype){ + this(datatype, NO_LENGTH); + } + + /** + * Build a TAP column type by specifying a datatype and a length (needed only for datatypes like char, varchar, binary and varbinary). + * + * @param datatype Column datatype. + * @param length Length of the column value (needed only for datatypes like char, varchar, binary and varbinary). + */ + public DBType(final DBDatatype datatype, final int length){ + if (datatype == null) + throw new NullPointerException("Missing TAP column datatype !"); + this.type = datatype; + this.length = length; + } + + public boolean isNumeric(){ + switch(type){ + case SMALLINT: + case INTEGER: + case BIGINT: + case REAL: + case DOUBLE: + /* Note: binaries are also included here because they can also be considered as Numeric, + * but not for JOINs. */ + case BINARY: + case VARBINARY: + case BLOB: + return true; + default: + return false; + } + } + + public boolean isBinary(){ + switch(type){ + case BINARY: + case VARBINARY: + case BLOB: + return true; + default: + return false; + } + } + + public boolean isString(){ + switch(type){ + case CHAR: + case VARCHAR: + case CLOB: + case TIMESTAMP: + return true; + default: + return false; + } + } + + public boolean isGeometry(){ + return (type == DBDatatype.POINT || type == DBDatatype.REGION); + } + + public boolean isCompatible(final DBType t){ + if (t == null) + return false; + else if (isBinary() == t.isBinary()) + return (type == DBDatatype.BLOB && t.type == DBDatatype.BLOB) || (type != DBDatatype.BLOB && t.type != DBDatatype.BLOB); + else if (isNumeric() == t.isNumeric()) + return true; + else if (isGeometry() == t.isGeometry()) + return (type == t.type); + else if (isString()) + return (type == DBDatatype.CLOB && t.type == DBDatatype.CLOB) || (type != DBDatatype.CLOB && t.type != DBDatatype.CLOB); + else + return (type == t.type); + } + + @Override + public String toString(){ + if (length > 0) + return type + "(" + length + ")"; + else + return type.toString(); + } + +} diff --git a/src/adql/db/DefaultDBColumn.java b/src/adql/db/DefaultDBColumn.java index 8496501..a4ed9e3 100644 --- a/src/adql/db/DefaultDBColumn.java +++ b/src/adql/db/DefaultDBColumn.java @@ -16,20 +16,27 @@ package adql.db; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ /** * Default implementation of {@link DBColumn}. * - * @author Grégory Mantelet (CDS) - * @version 08/2011 + * @author Grégory Mantelet (CDS;ARI) + * @version 1.3 (10/2014) */ public class DefaultDBColumn implements DBColumn { + /** Name of the column in the "database". */ protected String dbName; + /** Type of the column in the "database". + * Note: This should be one of the types listed by the IVOA in the TAP description. + * @since 1.3 */ + protected DBType type; + /** Table in which this column exists. */ protected DBTable table; - + /** Name that this column must have in ADQL queries. */ protected String adqlName = null; /** @@ -40,10 +47,30 @@ public class DefaultDBColumn implements DBColumn { * if a whole column reference is given, no split will be done.
    + *
  1. Check that all geometrical functions are supported
  2. + *
  3. Check that all explicit (string constant) coordinate system definitions are supported
  4. + *
  5. Check all STC-S expressions (only in {@link RegionFunction} for the moment) and + * Apply the 2 previous checks on them
  6. + *
* @param table DB table which contains this column. * - * @see #DefaultDBColumn(String, String, DBTable) + * @see #DefaultDBColumn(String, String, DBType, DBTable) */ public DefaultDBColumn(final String dbName, final DBTable table){ - this(dbName, dbName, table); + this(dbName, dbName, null, table); + } + + /** + * Builds a default {@link DBColumn} with the given DB name and DB table. + * + * @param dbName Database column name (it will be also used for the ADQL name). + * Only the column name is expected. Contrary to {@link DefaultDBTable}, + * if a whole column reference is given, no split will be done. + * @param type Type of the column. + * Note: there is no default value. Consequently if this parameter is NULL, + * the type should be considered as unknown. It means that any comparison with + * any type will always return 'true'. + * @param table DB table which contains this column. + * + * @see #DefaultDBColumn(String, String, DBType, DBTable) + * + * @since 1.3 + */ + public DefaultDBColumn(final String dbName, final DBType type, final DBTable table){ + this(dbName, dbName, type, table); } /** @@ -56,13 +83,38 @@ public class DefaultDBColumn implements DBColumn { * Only the column name is expected. Contrary to {@link DefaultDBTable}, * if a whole column reference is given, no split will be done. * @param table DB table which contains this column. + * + * @see #DefaultDBColumn(String, String, DBType, DBTable) */ public DefaultDBColumn(final String dbName, final String adqlName, final DBTable table){ + this(dbName, adqlName, null, table); + } + + /** + * Builds a default {@link DBColumn} with the given DB name, DB table and ADQL name. + * + * @param dbName Database column name. + * Only the column name is expected. Contrary to {@link DefaultDBTable}, + * if a whole column reference is given, no split will be done. + * @param adqlName Column name used in ADQL queries. + * Only the column name is expected. Contrary to {@link DefaultDBTable}, + * if a whole column reference is given, no split will be done. + * @param type Type of the column. + * Note: there is no default value. Consequently if this parameter is NULL, + * the type should be considered as unknown. It means that any comparison with + * any type will always return 'true'. + * @param table DB table which contains this column. + * + * @since 1.3 + */ + public DefaultDBColumn(final String dbName, final String adqlName, final DBType type, final DBTable table){ this.dbName = dbName; this.adqlName = adqlName; + this.type = type; this.table = table; } + @Override public final String getADQLName(){ return adqlName; } @@ -72,10 +124,38 @@ public class DefaultDBColumn implements DBColumn { this.adqlName = adqlName; } + @Override + public final DBType getDatatype(){ + return type; + } + + /** + *

Set the type of this column.

+ * + *

Note 1: + * The given type should be as closed as possible from a type listed by the IVOA in the TAP protocol description into the section UPLOAD. + *

+ * + *

Note 2: + * there is no default value. Consequently if this parameter is NULL, + * the type should be considered as unknown. It means that any comparison with + * any type will always return 'true'. + *

+ * + * @param type New type of this column. + * + * @since 1.3 + */ + public final void setDatatype(final DBType type){ + this.type = type; + } + + @Override public final String getDBName(){ return dbName; } + @Override public final DBTable getTable(){ return table; } @@ -84,8 +164,9 @@ public class DefaultDBColumn implements DBColumn { this.table = table; } + @Override public DBColumn copy(final String dbName, final String adqlName, final DBTable dbTable){ - return new DefaultDBColumn(dbName, adqlName, dbTable); + return new DefaultDBColumn(dbName, adqlName, type, dbTable); } } diff --git a/src/adql/db/DefaultDBTable.java b/src/adql/db/DefaultDBTable.java index baf7140..ccc3752 100644 --- a/src/adql/db/DefaultDBTable.java +++ b/src/adql/db/DefaultDBTable.java @@ -17,18 +17,19 @@ package adql.db; * along with ADQLLibrary. If not, see . * * Copyright 2012-2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomisches Rechen Institute (ARI) + * Astronomisches Rechen Institut (ARI) */ import java.util.Collection; -import java.util.HashMap; import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; /** * Default implementation of {@link DBTable}. * * @author Grégory Mantelet (CDS;ARI) - * @version 1.2 (11/2013) + * @version 1.3 (11/2014) */ public class DefaultDBTable implements DBTable { @@ -40,7 +41,7 @@ public class DefaultDBTable implements DBTable { protected String adqlSchemaName = null; protected String adqlName = null; - protected HashMap columns = new HashMap(); + protected Map columns = new LinkedHashMap(); /** *

Builds a default {@link DBTable} with the given DB name.

@@ -247,8 +248,52 @@ public class DefaultDBTable implements DBTable { return splitRes; } + /** + *

Join the last 3 items of the given string array with a dot ('.'). + * These three parts should be: [0]=catalog name, [1]=schema name, [2]=table name.

+ * + *

+ * If the array contains less than 3 items, all the given items will be though joined. + * However, if it contains more than 3 items, only the three last items will be. + *

+ * + *

A null item will be written as an empty string (string of length 0 ; "").

+ * + *

+ * In the case the first and the third items are not null, but the second is null, the final string will contain in the middle two dots. + * Example: if the array is {"cat", NULL, "table"}, then the joined string will be: "cat..table". + *

+ * + * @param nameParts String items to join. + * + * @return A string joining the 3 last string items of the given array, + * or an empty string if the given array is NULL. + * + * @since 1.3 + */ + public static final String joinTableName(final String[] nameParts){ + if (nameParts == null) + return ""; + + StringBuffer str = new StringBuffer(); + boolean empty = true; + for(int i = (nameParts.length <= 3) ? 0 : (nameParts.length - 3); i < nameParts.length; i++){ + if (!empty) + str.append('.'); + + String part = (nameParts[i] == null) ? null : nameParts[i].trim(); + if (part != null && part.length() > 0){ + str.append(part); + empty = false; + } + } + return str.toString(); + } + @Override - public DBTable copy(final String dbName, final String adqlName){ + public DBTable copy(String dbName, String adqlName){ + dbName = (dbName == null) ? joinTableName(new String[]{dbCatalogName,dbSchemaName,this.dbName}) : dbName; + adqlName = (adqlName == null) ? joinTableName(new String[]{adqlCatalogName,adqlSchemaName,this.adqlName}) : adqlName; DefaultDBTable copy = new DefaultDBTable(dbName, adqlName); for(DBColumn col : this){ if (col instanceof DBCommonColumn) @@ -258,5 +303,4 @@ public class DefaultDBTable implements DBTable { } return copy; } - } diff --git a/src/adql/db/FunctionDef.java b/src/adql/db/FunctionDef.java new file mode 100644 index 0000000..82107d0 --- /dev/null +++ b/src/adql/db/FunctionDef.java @@ -0,0 +1,541 @@ +package adql.db; + +/* + * This file is part of ADQLLibrary. + * + * ADQLLibrary is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ADQLLibrary is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ADQLLibrary. If not, see . + * + * Copyright 2015 - Astronomisches Rechen Institut (ARI) + */ + +import java.lang.reflect.Constructor; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import adql.db.DBType.DBDatatype; +import adql.parser.ParseException; +import adql.query.operand.ADQLOperand; +import adql.query.operand.function.ADQLFunction; +import adql.query.operand.function.DefaultUDF; +import adql.query.operand.function.UserDefinedFunction; + +/** + *

Definition of any function that could be used in ADQL queries.

+ * + *

+ * A such definition can be built manually thanks to the different constructors of this class, + * or by parsing a string function definition form using the static function {@link #parse(String)}. + *

+ * + *

+ * The syntax of the expression expected by {@link #parse(String)} is the same as the one used to build + * the string returned by {@link #toString()}. Here is this syntax: + *

+ *
{fctName}([{param1Name} {param1Type}, ...])[ -> {returnType}]
+ * + *

+ * A description of this function may be set thanks to the public class attribute {@link #description}. + *

+ * + * @author Grégory Mantelet (ARI) + * @version 1.3 (02/2015) + * + * @since 1.3 + */ +public class FunctionDef implements Comparable { + /** Regular expression for what should be a function or parameter name - a regular identifier. */ + protected final static String regularIdentifierRegExp = "[a-zA-Z]+[0-9a-zA-Z_]*"; + /** Rough regular expression for a function return type or a parameter type. + * The exact type is not checked here ; just the type name syntax is tested, not its value. + * This regular expression allows a type to have exactly one parameter (which is generally the length of a character or binary string. */ + protected final static String typeRegExp = "([a-zA-Z]+[0-9a-zA-Z]*)(\\(\\s*([0-9]+)\\s*\\))?"; + /** Rough regular expression for a function parameters' list. */ + protected final static String fctParamsRegExp = "\\s*[^,]+\\s*(,\\s*[^,]+\\s*)*"; + /** Rough regular expression for a function parameter: a name (see {@link #regularIdentifierRegExp}) and a type (see {@link #typeRegExp}). */ + protected final static String fctParamRegExp = "\\s*(" + regularIdentifierRegExp + ")\\s+" + typeRegExp + "\\s*"; + /** Rough regular expression for a whole function definition. */ + protected final static String fctDefRegExp = "\\s*(" + regularIdentifierRegExp + ")\\s*\\(([a-zA-Z0-9,() \r\n\t]*)\\)(\\s*->\\s*(" + typeRegExp + "))?\\s*"; + + /** Pattern of a function definition. This object has been compiled with {@link #fctDefRegExp}. */ + protected final static Pattern fctPattern = Pattern.compile(fctDefRegExp); + /** Pattern of a single parameter definition. This object has been compiled with {@link #fctParamRegExp}. */ + protected final static Pattern paramPattern = Pattern.compile(fctParamRegExp); + + /** Name of the function. */ + public final String name; + + /** Description of this function. */ + public String description = null; + + /** Type of the result returned by this function. */ + public final DBType returnType; + /** Indicate whether the return type is a string. */ + protected final boolean isString; + /** Indicate whether the return type is a numeric. */ + protected final boolean isNumeric; + /** Indicate whether the return type is a geometry. */ + protected final boolean isGeometry; + + /** Total number of parameters. */ + public final int nbParams; + /** List of all the parameters of this function. */ + protected final FunctionParam[] params; + + /**

String representation of this function.

+ *

The syntax of this representation is the following (items between brackets are optional):

+ *
{fctName}([{param1Name} {param1Type}, ...])[ -> {returnType}]
*/ + private final String serializedForm; + + /**

String representation of this function dedicated to comparison with any function signature.

+ *

This form is different from the serialized form on the following points:

+ *
    + *
  • the function name is always in lower case.
  • + *
  • each parameter is represented by a string of 3 characters, one for each kind of type (in the order): numeric, string, geometry. + * Each character is either a 0 or 1, so that indicating whether the parameter is of that kind of type.
  • + *
  • no return type.
  • + *
+ *

So the syntax of this form is the following (items between brackets are optional ; xxx is a string of 3 characters, each being either 0 or 1):

+ *
{fctName}([xxx, ...])
*/ + private final String compareForm; + + /** + *

Class of the {@link UserDefinedFunction} which must represent the UDF defined by this {@link FunctionDef} in the ADQL tree.

+ *

This class MUST have a constructor with a single parameter of type {@link ADQLOperand}[].

+ *

If this {@link FunctionDef} is defining an ordinary ADQL function, this attribute must be NULL. It is used only for user defined functions.

+ */ + private Class udfClass = null; + + /** + *

Definition of a function parameter.

+ * + *

This definition is composed of two items: the name and the type of the parameter.

+ * + * @author Grégory Mantelet (ARI) + * @version 1.3 (10/2014) + * @since 1.3 + */ + public static final class FunctionParam { + /** Parameter name. Ensured not null */ + public final String name; + /** Parameter type. Ensured not null */ + public final DBType type; + + /** + * Create a function parameter. + * + * @param paramName Name of the parameter to create. MUST NOT be NULL + * @param paramType Type of the parameter to create. MUST NOT be NULL + */ + public FunctionParam(final String paramName, final DBType paramType){ + if (paramName == null) + throw new NullPointerException("Missing name! The function parameter can not be created."); + if (paramType == null) + throw new NullPointerException("Missing type! The function parameter can not be created."); + this.name = paramName; + this.type = paramType; + } + } + + /** + *

Create a function definition.

+ * + *

The created function will have no return type and no parameter.

+ * + * @param fctName Name of the function. + */ + public FunctionDef(final String fctName){ + this(fctName, null, null); + } + + /** + *

Create a function definition.

+ * + *

The created function will have a return type (if the provided one is not null) and no parameter.

+ * + * @param fctName Name of the function. + * @param returnType Return type of the function. If NULL, this function will have no return type + */ + public FunctionDef(final String fctName, final DBType returnType){ + this(fctName, returnType, null); + } + + /** + *

Create a function definition.

+ * + *

The created function will have no return type and some parameters (except if the given array is NULL or empty).

+ * + * @param fctName Name of the function. + * @param params Parameters of this function. If NULL or empty, this function will have no parameter. + */ + public FunctionDef(final String fctName, final FunctionParam[] params){ + this(fctName, null, params); + } + + public FunctionDef(final String fctName, final DBType returnType, final FunctionParam[] params){ + // Set the name: + if (fctName == null) + throw new NullPointerException("Missing name! Can not create this function definition."); + this.name = fctName; + + // Set the parameters: + this.params = (params == null || params.length == 0) ? null : params; + this.nbParams = (params == null) ? 0 : params.length; + + // Set the return type; + this.returnType = returnType; + if (returnType != null){ + isNumeric = returnType.isNumeric(); + isString = returnType.isString(); + isGeometry = returnType.isGeometry(); + }else + isNumeric = isString = isGeometry = false; + + // Serialize in Strings (serializedForm and compareForm) this function definition: + StringBuffer bufSer = new StringBuffer(name), bufCmp = new StringBuffer(name.toLowerCase()); + bufSer.append('('); + for(int i = 0; i < nbParams; i++){ + bufSer.append(params[i].name).append(' ').append(params[i].type); + bufCmp.append(params[i].type.isNumeric() ? '1' : '0').append(params[i].type.isString() ? '1' : '0').append(params[i].type.isGeometry() ? '1' : '0'); + if (i + 1 < nbParams) + bufSer.append(", "); + } + bufSer.append(')'); + if (returnType != null) + bufSer.append(" -> ").append(returnType); + serializedForm = bufSer.toString(); + compareForm = bufCmp.toString(); + } + + /** + * Tell whether this function returns a numeric. + * + * @return true if this function returns a numeric, false otherwise. + */ + public final boolean isNumeric(){ + return isNumeric; + } + + /** + * Tell whether this function returns a string. + * + * @return true if this function returns a string, false otherwise. + */ + public final boolean isString(){ + return isString; + } + + /** + * Tell whether this function returns a geometry. + * + * @return true if this function returns a geometry, false otherwise. + */ + public final boolean isGeometry(){ + return isGeometry; + } + + /** + * Get the number of parameters required by this function. + * + * @return Number of required parameters. + */ + public final int getNbParams(){ + return nbParams; + } + + /** + * Get the definition of the indParam-th parameter of this function. + * + * @param indParam Index of the parameter whose the definition must be returned. + * + * @return Definition of the specified parameter. + * + * @throws ArrayIndexOutOfBoundsException If the given index is negative or bigger than the number of parameters. + */ + public final FunctionParam getParam(final int indParam) throws ArrayIndexOutOfBoundsException{ + if (indParam < 0 || indParam >= nbParams) + throw new ArrayIndexOutOfBoundsException(indParam); + else + return params[indParam]; + } + + /** + *

Get the class of the {@link UserDefinedFunction} able to represent the function defined here in an ADQL tree.

+ * + *

Note: + * This getter should return always NULL if the function defined here is not a user defined function. + *
+ * However, if this {@link FunctionDef} is defining a user defined function and this function returns NULL, + * the library will create on the fly a {@link DefaultUDF} corresponding to this definition when needed. + * Indeed this UDF class is useful only if the translation from ADQL (to SQL for instance) of the defined + * function has a different signature (e.g. a different name) in the target language (e.g. SQL). + *

+ * + * @return The corresponding {@link UserDefinedFunction}. MAY BE NULL + */ + public final Class getUDFClass(){ + return udfClass; + } + + /** + *

Set the class of the {@link UserDefinedFunction} able to represent the function defined here in an ADQL tree.

+ * + *

Note: + * If this {@link FunctionDef} defines an ordinary ADQL function - and not a user defined function - no class should be set here. + *
+ * However, if it defines a user defined function, there is no obligation to set a UDF class. It is useful only if the translation + * from ADQL (to SQL for instance) of the function has a different signature (e.g. a different name) in the target language (e.g. SQL). + * If the signature is the same, there is no need to set a UDF class ; a {@link DefaultUDF} will be created on the fly by the library + * when needed if it turns out that no UDF class is set. + *

+ * + * @param udfClass Class to use to represent in an ADQL tree the User Defined Function defined in this {@link FunctionDef}. + * + * @throws IllegalArgumentException If the given class does not provide any constructor with a single parameter of type ADQLOperand[]. + */ + public final < T extends UserDefinedFunction > void setUDFClass(final Class udfClass) throws IllegalArgumentException{ + try{ + + // Ensure that, if a class is provided, it contains a constructor with a single parameter of type ADQLOperand[]: + if (udfClass != null){ + Constructor constructor = udfClass.getConstructor(ADQLOperand[].class); + if (constructor == null) + throw new IllegalArgumentException("The given class (" + udfClass.getName() + ") does not provide any constructor with a single parameter of type ADQLOperand[]!"); + } + + // Set the new UDF class: + this.udfClass = udfClass; + + }catch(SecurityException e){ + throw new IllegalArgumentException("A security problem occurred while trying to get constructor from the class " + udfClass.getName() + ": " + e.getMessage()); + }catch(NoSuchMethodException e){ + throw new IllegalArgumentException("The given class (" + udfClass.getName() + ") does not provide any constructor with a single parameter of type ADQLOperand[]!"); + } + } + + /** + *

Let parsing the serialized form of a function definition.

+ * + *

The expected syntax is (items between brackets are optional):

+ *
{fctName}([{param1Name} {param1Type}, ...])[ -> {returnType}]
+ * + *

+ * Allowed parameter types and return types should be one the types listed by the UPLOAD section of the TAP recommendation document. + * These types are listed in the enumeration object {@link DBType}. + * However, other types should be accepted like the common database types...but it should be better to not rely on that + * since the conversion of those types to TAP types should not be exactly what is expected. + *

+ * + * @param strDefinition Serialized function definition to parse. + * + * @return The object representation of the given string definition. + * + * @throws ParseException If the given string has a wrong syntax or uses unknown types. + */ + public static FunctionDef parse(final String strDefinition) throws ParseException{ + if (strDefinition == null) + throw new NullPointerException("Missing string definition to build a FunctionDef!"); + + // Check the global syntax of the function definition: + Matcher m = fctPattern.matcher(strDefinition); + if (m.matches()){ + + // Get the function name: + String fctName = m.group(1); + + // Parse and get the return type: + DBType returnType = null; + if (m.group(3) != null){ + returnType = parseType(m.group(5), (m.group(7) == null) ? DBType.NO_LENGTH : Integer.parseInt(m.group(7))); + if (returnType == null) + throw new ParseException("Unknown return type: \"" + m.group(4).trim() + "\"!"); + } + + // Get the parameters, if any: + String paramsList = m.group(2); + FunctionParam[] params = null; + if (paramsList != null && paramsList.trim().length() > 0){ + + // Check the syntax of the parameters' list: + if (!paramsList.matches(fctParamsRegExp)) + throw new ParseException("Wrong parameters syntax! Expected syntax: \"( (, )*)\", where =\"[a-zA-Z]+[a-zA-Z0-9_]*\", should be one of the types described in the UPLOAD section of the TAP documentation. Examples of good syntax: \"()\", \"(param INTEGER)\", \"(param1 INTEGER, param2 DOUBLE)\""); + + // Split all the parameter definitions: + String[] paramsSplit = paramsList.split(","); + params = new FunctionParam[paramsSplit.length]; + DBType paramType; + + // For each parameter definition... + for(int i = 0; i < params.length; i++){ + m = paramPattern.matcher(paramsSplit[i]); + if (m.matches()){ + + // ...parse and get the parameter type: + paramType = parseType(m.group(2), (m.group(4) == null) ? DBType.NO_LENGTH : Integer.parseInt(m.group(4))); + + // ...build the parameter definition object: + if (paramType == null) + throw new ParseException("Unknown type for the parameter \"" + m.group(1) + "\": \"" + m.group(2) + ((m.group(3) == null) ? "" : m.group(3)) + "\"!"); + else + params[i] = new FunctionParam(m.group(1), paramType); + }else + // note: should never happen because we have already check the syntax of the whole parameters list before parsing each individual parameter. + throw new ParseException("Wrong syntax for the " + (i + 1) + "-th parameter: \"" + paramsSplit[i].trim() + "\"! Expected syntax: \"( (, )*)\", where =\"[a-zA-Z]+[a-zA-Z0-9_]*\", should be one of the types described in the UPLOAD section of the TAP documentation. Examples of good syntax: \"()\", \"(param INTEGER)\", \"(param1 INTEGER, param2 DOUBLE)\""); + } + } + + // Build the function definition object: + return new FunctionDef(fctName, returnType, params); + }else + throw new ParseException("Wrong function definition syntax! Expected syntax: \"(?) ?\", where =\"[a-zA-Z]+[a-zA-Z0-9_]*\", =\" -> \", =\"( (, )*)\", should be one of the types described in the UPLOAD section of the TAP documentation. Examples of good syntax: \"foo()\", \"foo() -> VARCHAR\", \"foo(param INTEGER)\", \"foo(param1 INTEGER, param2 DOUBLE) -> DOUBLE\""); + } + + /** + * Parse the given string representation of a datatype. + * + * @param datatype String representation of a datatype. + * Note: This string must not contain the length parameter or any other parameter. + * These latter should have been separated from the datatype before calling this function. + * @param length Length of this datatype. + * Note: This length will be used only for binary (BINARY and VARBINARY) + * and character (CHAR and VARCHAR) types. + * + * @return The object representation of the specified datatype. + */ + private static DBType parseType(String datatype, int length){ + if (datatype == null) + return null; + + try{ + // Try to find a corresponding DBType item: + DBDatatype dbDatatype = DBDatatype.valueOf(datatype.toUpperCase()); + + // If there's a match, build the type object representation: + length = (length <= 0) ? DBType.NO_LENGTH : length; + switch(dbDatatype){ + case CHAR: + case VARCHAR: + case BINARY: + case VARBINARY: + return new DBType(dbDatatype, length); + default: + return new DBType(dbDatatype); + } + }catch(IllegalArgumentException iae){ + // If there's no corresponding DBType item, try to find a match among the most used DB types: + datatype = datatype.toLowerCase(); + if (datatype.equals("bool") || datatype.equals("boolean") || datatype.equals("short")) + return new DBType(DBDatatype.SMALLINT); + else if (datatype.equals("int2")) + return new DBType(DBDatatype.SMALLINT); + else if (datatype.equals("int") || datatype.equals("int4")) + return new DBType(DBDatatype.INTEGER); + else if (datatype.equals("long") || datatype.equals("number") || datatype.equals("bigint") || datatype.equals("int8")) + return new DBType(DBDatatype.BIGINT); + else if (datatype.equals("float") || datatype.equals("float4")) + return new DBType(DBDatatype.REAL); + else if (datatype.equals("numeric") || datatype.equals("float8")) + return new DBType(DBDatatype.DOUBLE); + else if (datatype.equals("byte") || datatype.equals("raw")) + return new DBType(DBDatatype.BINARY, length); + else if (datatype.equals("unsignedByte")) + return new DBType(DBDatatype.VARBINARY, length); + else if (datatype.equals("character")) + return new DBType(DBDatatype.CHAR, length); + else if (datatype.equals("string") || datatype.equals("varchar2")) + return new DBType(DBDatatype.VARCHAR, length); + else if (datatype.equals("bytea")) + return new DBType(DBDatatype.BLOB); + else if (datatype.equals("text")) + return new DBType(DBDatatype.CLOB); + else if (datatype.equals("date") || datatype.equals("time")) + return new DBType(DBDatatype.TIMESTAMP); + else if (datatype.equals("position")) + return new DBType(DBDatatype.POINT); + else if (datatype.equals("polygon") || datatype.equals("box") || datatype.equals("circle")) + return new DBType(DBDatatype.REGION); + else + return null; + } + } + + @Override + public String toString(){ + return serializedForm; + } + + @Override + public int compareTo(final FunctionDef def){ + return compareForm.compareTo(def.compareForm); + } + + /** + *

Compare this function definition with the given ADQL function item.

+ * + *

+ * The comparison is done only on the function name and on rough type of the parameters. + * "Rough type" means here that just the kind of type is tested: numeric, string or geometry. + * Anyway, the return type is never tested by this function, since such information is usually + * not part of a function signature. + *

+ * + *

The notion of "greater" and "less" are defined here according to the three following test steps:

+ *
    + *
  1. Name test: if the name of both function are equals, next steps are evaluated, otherwise the standard string comparison (case insensitive) result is returned.
  2. + *
  3. Parameters test: parameters are compared individually. Each time parameters (at the same position in both functions) are equals the next parameter can be tested, + * and so on until two parameters are different or the end of the parameters' list is reached. + * Just the kind of type is used for parameter comparison. Each kind of type is tested in the following order: numeric, string and geometry. + * When a kind of type is not equal for both parameters, the function exits with the appropriate value + * (1 if the parameter of this function definition is of the kind of type, -1 otherwise).
  4. + *
  5. Number of parameters test: in the case where this function definition has N parameters and the given ADQL function has M parameters, + * and that the L (= min(N,M)) first parameters have the same type in both functions, the value returns by this function + * will be N-M. Thus, if this function definition has more parameters than the given function, a positive value will be + * returned. Otherwise a negative value will be returned, or 0 if the number of parameters is the same.
  6. + *
+ * + * @param fct ADQL function item to compare with this function definition. + * + * @return A positive value if this function definition is "greater" than the given {@link ADQLFunction}, + * 0 if they are perfectly matching, + * or a negative value if this function definition is "less" than the given {@link ADQLFunction}. + */ + public int compareTo(final ADQLFunction fct){ + if (fct == null) + throw new NullPointerException("Missing ADQL function with which comparing this function definition!"); + + // Names comparison: + int comp = name.compareToIgnoreCase(fct.getName()); + + // If equals, compare the parameters' type: + if (comp == 0){ + for(int i = 0; comp == 0 && i < nbParams && i < fct.getNbParameters(); i++){ + if (params[i].type.isNumeric() == fct.getParameter(i).isNumeric()){ + if (params[i].type.isString() == fct.getParameter(i).isString()){ + if (params[i].type.isGeometry() == fct.getParameter(i).isGeometry()) + comp = 0; + else + comp = params[i].type.isGeometry() ? 1 : -1; + }else + comp = params[i].type.isString() ? 1 : -1; + }else + comp = params[i].type.isNumeric() ? 1 : -1; + } + + // If the first min(N,M) parameters are of the same type, do the last comparison on the number of parameters: + if (comp == 0 && nbParams != fct.getNbParameters()) + comp = nbParams - fct.getNbParameters(); + } + + return comp; + } +} diff --git a/src/adql/db/STCS.java b/src/adql/db/STCS.java new file mode 100644 index 0000000..716ef58 --- /dev/null +++ b/src/adql/db/STCS.java @@ -0,0 +1,1683 @@ +package adql.db; + +/* + * This file is part of ADQLLibrary. + * + * ADQLLibrary is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ADQLLibrary is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ADQLLibrary. If not, see . + * + * Copyright 2014 - Astronomisches Rechen Institut (ARI) + */ + +import java.util.ArrayList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import adql.parser.ADQLQueryFactory; +import adql.parser.ParseException; +import adql.query.TextPosition; +import adql.query.operand.ADQLOperand; +import adql.query.operand.NegativeOperand; +import adql.query.operand.NumericConstant; +import adql.query.operand.StringConstant; +import adql.query.operand.function.ADQLFunction; +import adql.query.operand.function.geometry.BoxFunction; +import adql.query.operand.function.geometry.CircleFunction; +import adql.query.operand.function.geometry.GeometryFunction; +import adql.query.operand.function.geometry.PointFunction; +import adql.query.operand.function.geometry.PolygonFunction; +import adql.query.operand.function.geometry.RegionFunction; + +/** + *

This class helps dealing with the subset of STC-S expressions described by the section "6 Use of STC-S in TAP (informative)" + * of the TAP Recommendation 1.0 (27th March 2010). This subset is limited to the most common coordinate systems and regions.

+ * + *

Note: + * No instance of this class can be created. Its usage is only limited to its static functions and classes. + *

+ * + *

Coordinate system

+ *

+ * The function {@link #parseCoordSys(String)} is able to parse a string containing only the STC-S expression of a coordinate system + * (or an empty string or null which would be interpreted as the default coordinate system - UNKNOWNFRAME UNKNOWNREFPOS SPHERICAL2). + * When successful, this parsing returns an object representation of the coordinate system: {@link CoordSys}. + *

+ *

+ * To serialize into STC-S a coordinate system, you have to create a {@link CoordSys} instance with the desired values + * and to call the function {@link CoordSys#toSTCS()}. The static function {@link #toSTCS(CoordSys)} is just calling the + * {@link CoordSys#toSTCS()} on the given coordinate system. + *

+ * + *

Geometrical region

+ *

+ * As for the coordinate system, there is a static function to parse the STC-S representation of a geometrical region: {@link #parseRegion(String)}. + * Here again, when the parsing is successful an object representation is returned: {@link Region}. + *

+ *

+ * This class lets also serializing into STC-S a region. The procedure is the same as with a coordinate system: create a {@link Region} and then + * call {@link Region#toString()}. + *

+ *

+ * The class {@link Region} lets also dealing with the {@link ADQLFunction} implementing a region. It is then possible to create a {@link Region} + * object from a such {@link ADQLFunction} and to get the corresponding STC-S representation. The static function {@link #toSTCS(GeometryFunction)} + * is a helpful function which do these both actions in once. + *

+ *

Note: + * The conversion from {@link ADQLFunction} to {@link Region} or STC-S is possible only if the {@link ADQLFunction} contains constants as parameter. + * Thus, a such function using a column, a concatenation, a math operation or using another function can not be converted into STC-S using this class. + *

+ * + * @author Grégory Mantelet (ARI) + * @version 1.3 (12/2014) + * @since 1.3 + */ +public final class STCS { + + /** + * Empty private constructor ; in order to prevent any instance creation. + */ + private STCS(){} + + /* ***************** */ + /* COORDINATE SYSTEM */ + /* ***************** */ + + /** Regular expression for a STC-S representation of a coordinate system. It takes into account the fact that each part of + * a coordinate system is optional and so that a full coordinate system expression can be reduced to an empty string. */ + private final static String coordSysRegExp = Frame.regexp + "?\\s*" + RefPos.regexp + "?\\s*" + Flavor.regexp + "?"; + /** Regular expression of an expression exclusively limited to a coordinate system. */ + private final static String onlyCoordSysRegExp = "^\\s*" + coordSysRegExp + "\\s*$"; + /** Regular expression of a default coordinate system: either an empty string or a string containing only default values. */ + private final static String defaultCoordSysRegExp = "^\\s*" + Frame.DEFAULT + "?\\s*" + RefPos.DEFAULT + "?\\s*" + Flavor.DEFAULT + "?\\s*$"; + /** Regular expression of a pattern describing a set of allowed coordinate systems. See {@link #buildAllowedRegExp(String)} for more details. */ + /* With this regular expression, we get the following matching groups: + * 0: All the expression + * 1+(6*N): The N-th part of the coordinate system (N is an unsigned integer between 0 and 2 (included) ; it is reduced to '*' if the two following groups are NULL + * 2+(6*N): A single value for the N-th part + * 3+(6*N): A list of values for the N-th part + * 4+(6*N): First value of the list for the N-th part + * 5+(6*N): All the other values (starting with a |) of the list for the N-th part + * 6+(6*N): Last value of the list for the N-th part. + */ + private final static String allowedCoordSysRegExp = "^\\s*" + buildAllowedRegExp(Frame.regexp) + "\\s+" + buildAllowedRegExp(RefPos.regexp) + "\\s+" + buildAllowedRegExp(Flavor.regexp) + "\\s*$"; + + /** Pattern of an allowed coordinate system pattern. This object has been compiled with {@link #allowedCoordSysRegExp}. */ + private final static Pattern allowedCoordSysPattern = Pattern.compile(allowedCoordSysRegExp); + + /** Human description of the syntax of a full coordinate system expression. */ + private final static String COORD_SYS_SYNTAX = "\"[" + Frame.regexp + "] [" + RefPos.regexp + "] [" + Flavor.regexp + "]\" ; an empty string is also allowed and will be interpreted as the coordinate system locally used"; + + /** + * Build the regular expression of a string defining the allowed values for one part of the whole coordinate system. + * + * @param rootRegExp All allowed part values. + * + * @return The corresponding regular expression. + */ + private static String buildAllowedRegExp(final String rootRegExp){ + return "(" + rootRegExp + "|\\*|(\\(\\s*" + rootRegExp + "\\s*(\\|\\s*" + rootRegExp + "\\s*)*\\)))"; + } + + /** + *

List of all possible frames in an STC expression.

+ * + *

+ * When no value is specified, the default one is {@link #UNKNOWNFRAME}. + * The default value is also accessible through the attribute {@link #DEFAULT} + * and it is possible to test whether a frame is the default with the function {@link #isDefault()}. + *

+ * + *

Note: + * The possible values listed in this enumeration are limited to the subset of STC-S described by the section "6 Use of STC-S in TAP (informative)" + * of the TAP Recommendation 1.0 (27th March 2010). + *

+ * + * @author Grégory Mantelet (ARI) + * @version 1.3 (10/2014) + * @since 1.3 + */ + public static enum Frame{ + ECLIPTIC, FK4, FK5, GALACTIC, ICRS, UNKNOWNFRAME; + + /** Default value for a frame: {@link #UNKNOWNFRAME}. */ + public static final Frame DEFAULT = UNKNOWNFRAME; + + /** Regular expression to test whether a string is a valid frame or not. This regular expression does not take into account + * the case of an empty string (which means "default frame"). */ + public static final String regexp = buildRegexp(Frame.class); + + /** + * Tell whether this frame is the default one. + * + * @return true if this is the default frame, false + */ + public final boolean isDefault(){ + return this == DEFAULT; + } + } + + /** + *

List of all possible reference positions in an STC expression.

+ * + *

+ * When no value is specified, the default one is {@link #UNKNOWNREFPOS}. + * The default value is also accessible through the attribute {@link #DEFAULT} + * and it is possible to test whether a reference position is the default with the function {@link #isDefault()}. + *

+ * + *

Note: + * The possible values listed in this enumeration are limited to the subset of STC-S described by the section "6 Use of STC-S in TAP (informative)" + * of the TAP Recommendation 1.0 (27th March 2010). + *

+ * + * @author Grégory Mantelet (ARI) + * @version 1.3 (10/2014) + * @since 1.3 + */ + public static enum RefPos{ + BARYCENTER, GEOCENTER, HELIOCENTER, LSR, TOPOCENTER, RELOCATABLE, UNKNOWNREFPOS; + + /** Default value for a reference position: {@link #UNKNOWNREFPOS}. */ + public static final RefPos DEFAULT = UNKNOWNREFPOS; + + /** Regular expression to test whether a string is a valid reference position or not. This regular expression does not take into account + * the case of an empty string (which means "default reference position"). */ + public static final String regexp = buildRegexp(RefPos.class); + + /** + * Tell whether this reference position is the default one. + * + * @return true if this is the default reference position, false + */ + public final boolean isDefault(){ + return this == DEFAULT; + } + } + + /** + *

List of all possible flavors in an STC expression.

+ * + *

+ * When no value is specified, the default one is {@link #SPHERICAL2}. + * The default value is also accessible through the attribute {@link #DEFAULT} + * and it is possible to test whether a flavor is the default with the function {@link #isDefault()}. + *

+ * + *

Note: + * The possible values listed in this enumeration are limited to the subset of STC-S described by the section "6 Use of STC-S in TAP (informative)" + * of the TAP Recommendation 1.0 (27th March 2010). + *

+ * + * @author Grégory Mantelet (ARI) + * @version 1.3 (10/2014) + * @since 1.3 + */ + public static enum Flavor{ + CARTESIAN2, CARTESIAN3, SPHERICAL2; + + /** Default value for a flavor: {@link #SPHERICAL2}. */ + public static final Flavor DEFAULT = SPHERICAL2; + + /** Regular expression to test whether a string is a valid flavor or not. This regular expression does not take into account + * the case of an empty string (which means "default flavor"). */ + public static final String regexp = buildRegexp(Flavor.class); + + /** + * Tell whether this flavor is the default one. + * + * @return true if this is the default flavor, false + */ + public final boolean isDefault(){ + return this == DEFAULT; + } + } + + /** + * Build a regular expression covering all possible values of the given enumeration. + * + * @param enumType Class of an enumeration type. + * + * @return The build regular expression or "\s*" if the given enumeration contains no constants/values. + * + * @throws IllegalArgumentException If the given class is not an enumeration type. + */ + private static String buildRegexp(final Class enumType) throws IllegalArgumentException{ + // The given class must be an enumeration type: + if (!enumType.isEnum()) + throw new IllegalArgumentException("An enum class was expected, but a " + enumType.getName() + " has been given!"); + + // Get the enumeration constants/values: + Object[] constants = enumType.getEnumConstants(); + if (constants == null || constants.length == 0) + return "\\s*"; + + // Concatenate all constants with pipe to build a choice regular expression: + StringBuffer buf = new StringBuffer("("); + for(int i = 0; i < constants.length; i++){ + buf.append(constants[i]); + if ((i + 1) < constants.length) + buf.append('|'); + } + return buf.append(')').toString(); + } + + /** + *

Object representation of an STC coordinate system.

+ * + *

+ * A coordinate system is composed of three parts: a frame ({@link #frame}), + * a reference position ({@link #refpos}) and a flavor ({@link #flavor}). + *

+ * + *

+ * The default value - also corresponding to an empty string - should be: + * {@link Frame#UNKNOWNFRAME} {@link RefPos#UNKNOWNREFPOS} {@link Flavor#SPHERICAL2}. + * Once built, it is possible to know whether the coordinate system is the default one + * or not thanks to function {@link #isDefault()}. + *

+ * + *

+ * An instance of this class can be easily serialized into STC-S using {@link #toSTCS()}, {@link #toFullSTCS()} + * or {@link #toString()}. {@link #toFullSTCS()} will display default values explicitly + * on the contrary to {@link #toSTCS()} which will replace them by empty strings. + *

+ * + *

Important note: + * The flavors CARTESIAN2 and CARTESIAN3 can not be used with other frame and reference position than + * UNKNOWNFRAME and UNKNOWNREFPOS. In the contrary case an {@link IllegalArgumentException} is throw. + *

+ * + * @author Grégory Mantelet (ARI) + * @version 1.3 (10/2014) + * @since 1.3 + */ + public static class CoordSys { + /** First item of a coordinate system expression: the frame. */ + public final Frame frame; + + /** Second item of a coordinate system expression: the reference position. */ + public final RefPos refpos; + + /** Third and last item of a coordinate system expression: the flavor. */ + public final Flavor flavor; + + /** Indicate whether all parts of the coordinate system are set to their default value. */ + private final boolean isDefault; + + /** STC-S representation of this coordinate system. Default items are not written (that's to say, they are replaced by an empty string). */ + private final String stcs; + + /** STC-S representation of this coordinate system. Default items are explicitly written. */ + private final String fullStcs; + + /** + * Build a default coordinate system (UNKNOWNFRAME UNKNOWNREFPOS SPHERICAL2). + */ + public CoordSys(){ + this(null, null, null); + } + + /** + * Build a coordinate system with the given parts. + * + * @param fr Frame part. + * @param rp Reference position part. + * @param fl Flavor part. + * + * @throws IllegalArgumentException If a cartesian flavor is used with a frame and reference position other than UNKNOWNFRAME and UNKNOWNREFPOS. + */ + public CoordSys(final Frame fr, final RefPos rp, final Flavor fl) throws IllegalArgumentException{ + frame = (fr == null) ? Frame.DEFAULT : fr; + refpos = (rp == null) ? RefPos.DEFAULT : rp; + flavor = (fl == null) ? Flavor.DEFAULT : fl; + + if (flavor != Flavor.SPHERICAL2 && (frame != Frame.UNKNOWNFRAME || refpos != RefPos.UNKNOWNREFPOS)) + throw new IllegalArgumentException("a coordinate system expressed with a cartesian flavor MUST have an UNKNOWNFRAME and UNKNOWNREFPOS!"); + + isDefault = frame.isDefault() && refpos.isDefault() && flavor.isDefault(); + + stcs = ((!frame.isDefault() ? frame + " " : "") + (!refpos.isDefault() ? refpos + " " : "") + (!flavor.isDefault() ? flavor : "")).trim(); + fullStcs = frame + " " + refpos + " " + flavor; + } + + /** + * Build a coordinate system by parsing the given STC-S expression. + * + * @param coordsys STC-S expression representing a coordinate system. Empty string and NULL are allowed values ; they correspond to a default coordinate system. + * + * @throws ParseException If the syntax of the given STC-S expression is wrong or if it is not a coordinate system only. + */ + public CoordSys(final String coordsys) throws ParseException{ + CoordSys tmp = new STCSParser().parseCoordSys(coordsys); + frame = tmp.frame; + refpos = tmp.refpos; + flavor = tmp.flavor; + isDefault = tmp.isDefault; + stcs = tmp.stcs; + fullStcs = tmp.fullStcs; + } + + /** + * Tell whether this is the default coordinate system (UNKNOWNFRAME UNKNOWNREFPOS SPHERICAL2). + * + * @return true if it is the default coordinate system, false otherwise. + */ + public final boolean isDefault(){ + return isDefault; + } + + /** + * Get the STC-S expression of this coordinate system, + * in which default values are not written (they are replaced by empty strings). + * + * @return STC-S representation of this coordinate system. + */ + public String toSTCS(){ + return stcs; + } + + /** + * Get the STC-S expression of this coordinate system, + * in which default values are explicitly written. + * + * @return STC-S representation of this coordinate system. + */ + public String toFullSTCS(){ + return fullStcs; + } + + /** + * Convert this coordinate system into a STC-S expression. + * + * @see java.lang.Object#toString() + * @see #toSTCS() + */ + @Override + public String toString(){ + return stcs; + } + } + + /** + * Parse the given STC-S representation of a coordinate system. + * + * @param stcs STC-S expression of a coordinate system. Note: a NULL or empty string will be interpreted as a default coordinate system. + * + * @return The object representation of the specified coordinate system. + * + * @throws ParseException If the given expression has a wrong STC-S syntax. + */ + public static CoordSys parseCoordSys(final String stcs) throws ParseException{ + return (new STCSParser().parseCoordSys(stcs)); + } + + /** + *

Convert an object representation of a coordinate system into an STC-S expression.

+ * + *

Note: + * A NULL object will be interpreted as the default coordinate system and so an empty string will be returned. + * Otherwise, this function is equivalent to {@link CoordSys#toSTCS()} (in which default values for each + * coordinate system part is not displayed). + *

+ * + * @param coordSys The object representation of the coordinate system to convert into STC-S. + * + * @return The corresponding STC-S expression. + * + * @see CoordSys#toSTCS() + * @see CoordSys#toFullSTCS() + */ + public static String toSTCS(final CoordSys coordSys){ + if (coordSys == null) + return ""; + else + return coordSys.toSTCS(); + } + + /** + *

Build a big regular expression gathering all of the given coordinate system syntaxes.

+ * + *

+ * Each item of the given list must respect a strict syntax. Each part of the coordinate system + * may be a single value, a list of values or a '*' (meaning all values are allowed). + * A list of values must have the following syntax: ({value1}|{value2}|...). + * An empty string is NOT here accepted. + *

+ * + *

Example: + * (ICRS|FK4|FK5) * SPHERICAL2 is OK, + * but (ICRS|FK4|FK5) * is not valid because the flavor value is not defined. + *

+ * + *

+ * Since the default value of each part of a coordinate system should always be possible, + * this function ensure these default values are always possible in the returned regular expression. + * Thus, if some values except the default one are specified, the default value is automatically appended. + *

+ * + *

Note: + * If the given array is NULL, all coordinate systems are allowed. + * But if the given array is empty, none except an empty string or the default value will be allowed. + *

+ * + * @param allowedCoordSys List of all coordinate systems that are allowed. + * + * @return The corresponding regular expression. + * + * @throws ParseException If the syntax of one of the given allowed coordinate system is wrong. + */ + public static String buildCoordSysRegExp(final String[] allowedCoordSys) throws ParseException{ + // NULL array => all coordinate systems are allowed: + if (allowedCoordSys == null) + return onlyCoordSysRegExp; + // Empty array => no coordinate system (except the default one) is allowed: + else if (allowedCoordSys.length == 0) + return defaultCoordSysRegExp; + + // The final regular expression must be reduced to a coordinate system and nothing else before: + StringBuffer finalRegExp = new StringBuffer("^\\s*("); + + // For each allowed coordinate system: + Matcher m; + int nbCoordSys = 0; + for(int i = 0; i < allowedCoordSys.length; i++){ + + // NULL item => skipped! + if (allowedCoordSys[i] == null) + continue; + else{ + if (nbCoordSys > 0) + finalRegExp.append('|'); + nbCoordSys++; + } + + // Check its syntax and identify all of its parts: + m = allowedCoordSysPattern.matcher(allowedCoordSys[i].toUpperCase()); + if (m.matches()){ + finalRegExp.append('('); + for(int g = 0; g < 3; g++){ // See the comment after the Javadoc of #allowedCoordSysRegExp for a complete list of available groups returned by the pattern. + + // SINGLE VALUE: + if (m.group(2 + (6 * g)) != null) + finalRegExp.append('(').append(defaultChoice(g, m.group(2 + (6 * g)))).append(m.group(2 + (6 * g))).append(')'); + + // LIST OF VALUES: + else if (m.group(3 + (6 * g)) != null) + finalRegExp.append('(').append(defaultChoice(g, m.group(3 + (6 * g)))).append(m.group(3 + (6 * g)).replaceAll("\\s", "").substring(1)); + + // JOKER (*): + else{ + switch(g){ + case 0: + finalRegExp.append(Frame.regexp); + break; + case 1: + finalRegExp.append(RefPos.regexp); + break; + case 2: + finalRegExp.append(Flavor.regexp); + break; + } + finalRegExp.append('?'); + } + finalRegExp.append("\\s*"); + } + finalRegExp.append(')'); + }else + throw new ParseException("Wrong allowed coordinate system syntax for the " + (i + 1) + "-th item: \"" + allowedCoordSys[i] + "\"! Expected: \"frameRegExp refposRegExp flavorRegExp\" ; where each xxxRegExp = (xxx | '*' | '('xxx ('|' xxx)*')'), frame=\"" + Frame.regexp + "\", refpos=\"" + RefPos.regexp + "\" and flavor=\"" + Flavor.regexp + "\" ; an empty string is also allowed and will be interpreted as '*' (so all possible values)."); + } + + // The final regular expression must be reduced to a coordinate system and nothing else after: + finalRegExp.append(")\\s*$"); + + return (nbCoordSys > 0) ? finalRegExp.toString() : defaultCoordSysRegExp; + } + + /** + * Get the default value appended by a '|' character, ONLY IF the given value does not already contain the default value. + * + * @param g Index of the coordinate system part (0: Frame, 1: RefPos, 2: Flavor, another value will return an empty string). + * @param value Value in which the default value must prefix. + * + * @return A prefix for the given value (the default value and a '|' if the default value is not already in the given value, "" otherwise). + */ + private static String defaultChoice(final int g, final String value){ + switch(g){ + case 0: + return value.contains(Frame.DEFAULT.toString()) ? "" : Frame.DEFAULT + "|"; + case 1: + return value.contains(RefPos.DEFAULT.toString()) ? "" : RefPos.DEFAULT + "|"; + case 2: + return value.contains(Flavor.DEFAULT.toString()) ? "" : Flavor.DEFAULT + "|"; + default: + return ""; + } + } + + /* ****** */ + /* REGION */ + /* ****** */ + + /** + *

List all possible region types allowed in an STC-S expression.

+ * + *

Note: + * The possible values listed in this enumeration are limited to the subset of STC-S described by the section "6 Use of STC-S in TAP (informative)" + * of the TAP Recommendation 1.0 (27th March 2010). + *

+ * + * @author Grégory Mantelet (ARI) + * @version 1.3 (10/2014) + * @since 1.3 + */ + public static enum RegionType{ + POSITION, CIRCLE, BOX, POLYGON, UNION, INTERSECTION, NOT; + } + + /** + *

Object representation of an STC region.

+ * + *

+ * This class contains a field for each possible parameter of a region. Depending of the region type + * some are not used. In such case, these unused fields are set to NULL. + *

+ * + *

+ * An instance of this class can be easily serialized into STC-S using {@link #toSTCS()}, {@link #toFullSTCS()} + * or {@link #toString()}. {@link #toFullSTCS()} will display default value explicit + * on the contrary to {@link #toSTCS()} which will replace them by empty strings. + *

+ * + * @author Grégory Mantelet (ARI) + * @version 1.3 (10/2014) + * @since 1.3 + */ + public static class Region { + /** Type of the region. */ + public final RegionType type; + + /** Coordinate system used by this region. + * Note: only the NOT region does not declare a coordinate system ; so only for this region this field is NULL. */ + public final CoordSys coordSys; + + /** List of coordinates' pairs. The second dimension of this array represents a pair of coordinates ; it is then an array of two elements. + * Note: this field is used by POINT, BOX, CIRCLE and POLYGON. */ + public final double[][] coordinates; + + /** Width of the BOX region. */ + public final double width; + + /** Height of the BOX region. */ + public final double height; + + /** Radius of the CIRCLE region. */ + public final double radius; + + /** List of regions unified (UNION), intersected (INTERSECTION) or avoided (NOT). */ + public final Region[] regions; + + /** STC-S representation of this region, in which default values of the coordinate system (if any) are not written (they are replaced by empty strings). + * Note: This attribute is NULL until the first call of the function {@link #toSTCS()} where it is built. */ + private String stcs = null; + + /** STC-S representation of this region, in which default values of the coordinate system (if any) are explicitly written. + * Note: This attribute is NULL until the first call of the function {@link #toFullSTCS()} where it is built. */ + private String fullStcs = null; + + /** The ADQL function object representing this region. + * Note: this attribute is NULL until the first call of the function {@link #toGeometry()} or {@link #toGeometry(ADQLQueryFactory)}. */ + private GeometryFunction geometry = null; + + /** + *

Constructor for a POINT/POSITION region.

+ * + *

Important note: + * The array of coordinates is used like that. No copy is done. + *

+ * + * @param coordSys Coordinate system. note: It MAY BE null ; if so, the default coordinate system will be chosen + * @param coordinates A pair of coordinates ; coordinates[0] and coordinates[1]. + */ + public Region(final CoordSys coordSys, final double[] coordinates){ + this(coordSys, new double[][]{coordinates}); + } + + /** + *

Constructor for a POINT/POSITION or a POLYGON region.

+ * + *

Whether it is a polygon or a point depends on the number of given coordinates:

+ *
    + *
  • 1 item => POINT/POSITION
  • + *
  • more items => POLYGON
  • + *
+ * + *

Important note: + * The array of coordinates is used like that. No copy is done. + *

+ * + * @param coordSys Coordinate system. note: It MAY BE null ; if so, the default coordinate system will be chosen + * @param coordinates List of coordinates' pairs ; coordinates[n] = 1 pair = 2 items (coordinates[n][0] and coordinates[n][1]) ; if 1 pair, it is a POINT/POSITION, but if more, it is a POLYGON. + */ + public Region(final CoordSys coordSys, final double[][] coordinates){ + // Check roughly the coordinates: + if (coordinates == null || coordinates.length == 0) + throw new NullPointerException("Missing coordinates!"); + else if (coordinates[0].length != 2) + throw new IllegalArgumentException("Wrong number of coordinates! Expected at least 2 pairs of coordinates (so coordinates[0], coordinates[1] and coordinates[n].length = 2)."); + + // Decide of the region type in function of the number of coordinates' pairs: + type = (coordinates.length > 1) ? RegionType.POLYGON : RegionType.POSITION; + + // Set the coordinate system (if NULL, choose the default one): + this.coordSys = (coordSys == null ? new CoordSys() : coordSys); + + // Set the coordinates: + this.coordinates = coordinates; + + // Set the other fields as not used: + width = Double.NaN; + height = Double.NaN; + radius = Double.NaN; + regions = null; + } + + /** + *

Constructor for a CIRCLE region.

+ * + *

Important note: + * The array of coordinates is used like that. No copy is done. + *

+ * + * @param coordSys Coordinate system. note: It MAY BE null ; if so, the default coordinate system will be chosen + * @param coordinates A pair of coordinates ; coordinates[0] and coordinates[1]. + * @param radius The circle radius. + */ + public Region(final CoordSys coordSys, final double[] coordinates, final double radius){ + // Check roughly the coordinates: + if (coordinates == null || coordinates.length == 0) + throw new NullPointerException("Missing coordinates!"); + else if (coordinates.length != 2) + throw new IllegalArgumentException("Wrong number of coordinates! Expected exactly 2 values."); + + // Set the region type: + type = RegionType.CIRCLE; + + // Set the coordinate system (if NULL, choose the default one): + this.coordSys = (coordSys == null ? new CoordSys() : coordSys); + + // Set the coordinates: + this.coordinates = new double[][]{coordinates}; + + // Set the radius: + this.radius = radius; + + // Set the other fields as not used: + width = Double.NaN; + height = Double.NaN; + regions = null; + } + + /** + *

Constructor for a BOX region.

+ * + *

Important note: + * The array of coordinates is used like that. No copy is done. + *

+ * + * @param coordSys Coordinate system. note: It MAY BE null ; if so, the default coordinate system will be chosen + * @param coordinates A pair of coordinates ; coordinates[0] and coordinates[1]. + * @param width Width of the box. + * @param height Height of the box. + */ + public Region(final CoordSys coordSys, final double[] coordinates, final double width, final double height){ + // Check roughly the coordinates: + if (coordinates == null || coordinates.length == 0) + throw new NullPointerException("Missing coordinates!"); + else if (coordinates.length != 2) + throw new IllegalArgumentException("Wrong number of coordinates! Expected exactly 2 values."); + + // Set the region type: + type = RegionType.BOX; + + // Set the coordinate system (if NULL, choose the default one): + this.coordSys = (coordSys == null ? new CoordSys() : coordSys); + + // Set the coordinates: + this.coordinates = new double[][]{coordinates}; + + // Set the size of the box: + this.width = width; + this.height = height; + + // Set the other fields as not used: + radius = Double.NaN; + regions = null; + } + + /** + *

Constructor for a UNION or INTERSECTION region.

+ * + *

Important note: + * The array of regions is used like that. No copy is done. + *

+ * + * @param unionOrIntersection Type of the region to create. Note: It can be ONLY a UNION or INTERSECTION. Another value will throw an IllegalArgumentException). + * @param coordSys Coordinate system. note: It MAY BE null ; if so, the default coordinate system will be chosen + * @param regions Regions to unite or to intersect. Note: At least two regions must be provided. + */ + public Region(final RegionType unionOrIntersection, final CoordSys coordSys, final Region[] regions){ + // Check the type: + if (unionOrIntersection == null) + throw new NullPointerException("Missing type of region (UNION or INTERSECTION here)!"); + else if (unionOrIntersection != RegionType.UNION && unionOrIntersection != RegionType.INTERSECTION) + throw new IllegalArgumentException("Wrong region type: \"" + unionOrIntersection + "\"! This constructor lets create only an UNION or INTERSECTION region."); + + // Check the list of regions: + if (regions == null || regions.length == 0) + throw new NullPointerException("Missing regions to " + (unionOrIntersection == RegionType.UNION ? "unite" : "intersect") + "!"); + else if (regions.length < 2) + throw new IllegalArgumentException("Wrong number of regions! Expected at least 2 regions."); + + // Set the region type: + type = unionOrIntersection; + + // Set the coordinate system (if NULL, choose the default one): + this.coordSys = (coordSys == null ? new CoordSys() : coordSys); + + // Set the regions: + this.regions = regions; + + // Set the other fields as not used: + coordinates = null; + radius = Double.NaN; + width = Double.NaN; + height = Double.NaN; + } + + /** + * Constructor for a NOT region. + * + * @param region Any region to not select. + */ + public Region(final Region region){ + // Check the region parameter: + if (region == null) + throw new NullPointerException("Missing region to NOT select!"); + + // Set the region type: + type = RegionType.NOT; + + // Set the regions: + this.regions = new Region[]{region}; + + // Set the other fields as not used: + coordSys = null; + coordinates = null; + radius = Double.NaN; + width = Double.NaN; + height = Double.NaN; + } + + /** + *

Build a Region from the given ADQL representation.

+ * + *

Note: + * Only {@link PointFunction}, {@link CircleFunction}, {@link BoxFunction}, {@link PolygonFunction} and {@link RegionFunction} + * are accepted here. Other extensions of {@link GeometryFunction} will throw an {@link IllegalArgumentException}. + *

+ * + * @param geometry The ADQL representation of the region to create here. + * + * @throws IllegalArgumentException If the given geometry is neither of {@link PointFunction}, {@link BoxFunction}, {@link PolygonFunction} and {@link RegionFunction}. + * @throws ParseException If the declared coordinate system, the coordinates or the STC-S definition has a wrong syntax. + */ + public Region(final GeometryFunction geometry) throws IllegalArgumentException, ParseException{ + if (geometry == null) + throw new NullPointerException("Missing geometry to convert into STCS.Region!"); + + if (geometry instanceof PointFunction){ + type = RegionType.POSITION; + coordSys = STCS.parseCoordSys(extractString(geometry.getCoordinateSystem())); + coordinates = new double[][]{{extractNumeric(((PointFunction)geometry).getCoord1()),extractNumeric(((PointFunction)geometry).getCoord2())}}; + width = Double.NaN; + height = Double.NaN; + radius = Double.NaN; + regions = null; + }else if (geometry instanceof CircleFunction){ + type = RegionType.CIRCLE; + coordSys = STCS.parseCoordSys(extractString(geometry.getCoordinateSystem())); + coordinates = new double[][]{{extractNumeric(((CircleFunction)geometry).getCoord1()),extractNumeric(((CircleFunction)geometry).getCoord2())}}; + radius = extractNumeric(((CircleFunction)geometry).getRadius()); + width = Double.NaN; + height = Double.NaN; + regions = null; + }else if (geometry instanceof BoxFunction){ + type = RegionType.BOX; + coordSys = STCS.parseCoordSys(extractString(geometry.getCoordinateSystem())); + coordinates = new double[][]{{extractNumeric(((BoxFunction)geometry).getCoord1()),extractNumeric(((BoxFunction)geometry).getCoord2())}}; + width = extractNumeric(((BoxFunction)geometry).getWidth()); + height = extractNumeric(((BoxFunction)geometry).getHeight()); + radius = Double.NaN; + regions = null; + }else if (geometry instanceof PolygonFunction){ + PolygonFunction poly = (PolygonFunction)geometry; + type = RegionType.POLYGON; + coordSys = STCS.parseCoordSys(extractString(poly.getCoordinateSystem())); + coordinates = new double[(poly.getNbParameters() - 1) / 2][2]; + for(int i = 0; i < coordinates.length; i++) + coordinates[i] = new double[]{extractNumeric(poly.getParameter(1 + i * 2)),extractNumeric(poly.getParameter(2 + i * 2))}; + width = Double.NaN; + height = Double.NaN; + radius = Double.NaN; + regions = null; + }else if (geometry instanceof RegionFunction){ + Region r = STCS.parseRegion(extractString(((RegionFunction)geometry).getParameter(0))); + type = r.type; + coordSys = r.coordSys; + coordinates = r.coordinates; + width = r.width; + height = r.height; + radius = r.radius; + regions = r.regions; + }else + throw new IllegalArgumentException("Unknown region type! Only geometrical function PointFunction, CircleFunction, BoxFunction, PolygonFunction and RegionFunction are allowed."); + } + + /** + * Extract a string value from the given {@link ADQLOperand} + * which is expected to be a {@link StringConstant} instance. + * + * @param op A string operand. + * + * @return The string value embedded in the given operand. + * + * @throws ParseException If the given operand is not an instance of {@link StringConstant}. + */ + private static String extractString(final ADQLOperand op) throws ParseException{ + if (op == null) + throw new NullPointerException("Missing operand!"); + else if (op instanceof StringConstant) + return ((StringConstant)op).getValue(); + else + throw new ParseException("Can not convert into STC-S a non string argument (including ADQLColumn and Concatenation)!"); + } + + /** + * Extract a numeric value from the given {@link ADQLOperand} + * which is expected to be a {@link NumericConstant} instance + * or a {@link NegativeOperand} embedding a {@link NumericConstant}. + * + * @param op A numeric operand. + * + * @return The numeric value embedded in the given operand. + * + * @throws ParseException If the given operand is not an instance of {@link NumericConstant} or a {@link NegativeOperand}. + */ + private static double extractNumeric(final ADQLOperand op) throws ParseException{ + if (op == null) + throw new NullPointerException("Missing operand!"); + else if (op instanceof NumericConstant) + return Double.parseDouble(((NumericConstant)op).getValue()); + else if (op instanceof NegativeOperand) + return extractNumeric(((NegativeOperand)op).getOperand()) * -1; + else + throw new ParseException("Can not convert into STC-S a non numeric argument (including ADQLColumn and Operation)!"); + } + + /** + *

Get the STC-S representation of this region (in which default values + * of the coordinate system are not written ; they are replaced by empty strings).

+ * + *

Note: + * This function build the STC-S just once and store it in a class attribute. + * The value of this attribute is then returned at next calls of this function. + *

+ * + * @return Its STC-S representation. + */ + public String toSTCS(){ + if (stcs != null) + return stcs; + else{ + // Write the region type: + StringBuffer buf = new StringBuffer(type.toString()); + + // Write the coordinate system (except for NOT): + if (type != RegionType.NOT){ + String coordSysStr = coordSys.toSTCS(); + if (coordSysStr != null && coordSysStr.length() > 0) + buf.append(' ').append(coordSysStr); + buf.append(' '); + } + + // Write the other parameters (coordinates, regions, ...): + switch(type){ + case POSITION: + case POLYGON: + appendCoordinates(buf, coordinates); + break; + case CIRCLE: + appendCoordinates(buf, coordinates); + buf.append(' ').append(radius); + break; + case BOX: + appendCoordinates(buf, coordinates); + buf.append(' ').append(width).append(' ').append(height); + break; + case UNION: + case INTERSECTION: + case NOT: + buf.append('('); + appendRegions(buf, regions, false); + buf.append(')'); + break; + } + + // Return the built STC-S: + return (stcs = buf.toString()); + } + } + + /** + *

Get the STC-S representation of this region (in which default values + * of the coordinate system are explicitly written).

+ * + *

Note: + * This function build the STC-S just once and store it in a class attribute. + * The value of this attribute is then returned at next calls of this function. + *

+ * + * @return Its STC-S representation. + */ + public String toFullSTCS(){ + if (fullStcs != null) + return fullStcs; + else{ + // Write the region type: + StringBuffer buf = new StringBuffer(type.toString()); + + // Write the coordinate system (except for NOT): + if (type != RegionType.NOT){ + String coordSysStr = coordSys.toFullSTCS(); + if (coordSysStr != null && coordSysStr.length() > 0) + buf.append(' ').append(coordSysStr); + buf.append(' '); + } + + // Write the other parameters (coordinates, regions, ...): + switch(type){ + case POSITION: + case POLYGON: + appendCoordinates(buf, coordinates); + break; + case CIRCLE: + appendCoordinates(buf, coordinates); + buf.append(' ').append(radius); + break; + case BOX: + appendCoordinates(buf, coordinates); + buf.append(' ').append(width).append(' ').append(height); + break; + case UNION: + case INTERSECTION: + case NOT: + buf.append('('); + appendRegions(buf, regions, true); + buf.append(')'); + break; + } + + // Return the built STC-S: + return (fullStcs = buf.toString()); + } + } + + /** + * Append all the given coordinates to the given buffer. + * + * @param buf Buffer in which coordinates must be appended. + * @param coords Coordinates to append. + */ + private static void appendCoordinates(final StringBuffer buf, final double[][] coords){ + for(int i = 0; i < coords.length; i++){ + if (i > 0) + buf.append(' '); + buf.append(coords[i][0]).append(' ').append(coords[i][1]); + } + } + + /** + * Append all the given regions in the given buffer. + * + * @param buf Buffer in which regions must be appended. + * @param regions Regions to append. + * @param fullCoordSys Indicate whether the coordinate system of the regions must explicitly display the default values. + */ + private static void appendRegions(final StringBuffer buf, final Region[] regions, final boolean fullCoordSys){ + for(int i = 0; i < regions.length; i++){ + if (i > 0) + buf.append(' '); + if (fullCoordSys) + buf.append(regions[i].toFullSTCS()); + else + buf.append(regions[i].toSTCS()); + } + } + + @Override + public String toString(){ + return toSTCS(); + } + + /** + *

Convert this region into its corresponding ADQL representation.

+ * + *
    + *
  • POSITION: {@link PointFunction}
  • + *
  • CIRCLE: {@link CircleFunction}
  • + *
  • BOX: {@link BoxFunction}
  • + *
  • POLYGON: {@link PolygonFunction}
  • + *
  • UNION, INTERSECTION, NOT: {@link RegionFunction}
  • + *
+ * + *

Note: + * This function is using the default ADQL factory, built using {@link ADQLQueryFactory#ADQLQueryFactory()}. + *

+ * + * @return The corresponding ADQL representation. + * + * @see #toGeometry(ADQLQueryFactory) + */ + public GeometryFunction toGeometry(){ + return toGeometry(null); + } + + /** + *

Convert this region into its corresponding ADQL representation.

+ * + *
    + *
  • POSITION: {@link PointFunction}
  • + *
  • CIRCLE: {@link CircleFunction}
  • + *
  • BOX: {@link BoxFunction}
  • + *
  • POLYGON: {@link PolygonFunction}
  • + *
  • UNION, INTERSECTION, NOT: {@link RegionFunction}
  • + *
+ * + *

Note: + * This function build the ADQL representation just once and store it in a class attribute. + * The value of this attribute is then returned at next calls of this function. + *

+ * + * @param factory The factory of ADQL objects to use. + * + * @return The corresponding ADQL representation. + */ + public GeometryFunction toGeometry(ADQLQueryFactory factory){ + if (factory == null) + factory = new ADQLQueryFactory(); + + try{ + if (geometry != null) + return geometry; + else{ + StringConstant coordSysObj = factory.createStringConstant(coordSys == null ? "" : coordSys.toString()); + switch(type){ + case POSITION: + return (geometry = factory.createPoint(coordSysObj, toNumericObj(coordinates[0][0], factory), toNumericObj(coordinates[0][1], factory))); + case CIRCLE: + return (geometry = factory.createCircle(coordSysObj, toNumericObj(coordinates[0][0], factory), toNumericObj(coordinates[0][1], factory), toNumericObj(radius, factory))); + case BOX: + return (geometry = factory.createBox(coordSysObj, toNumericObj(coordinates[0][0], factory), toNumericObj(coordinates[0][1], factory), toNumericObj(width, factory), toNumericObj(height, factory))); + case POLYGON: + ArrayList coords = new ArrayList(coordinates.length * 2); + for(int i = 0; i < coordinates.length; i++){ + coords.add(toNumericObj(coordinates[i][0], factory)); + coords.add(toNumericObj(coordinates[i][1], factory)); + } + return (geometry = factory.createPolygon(coordSysObj, coords)); + default: + return (geometry = factory.createRegion(factory.createStringConstant(toString()))); + } + } + }catch(Exception pe){ + return null; + } + } + + /** + *

Convert a numeric value into an ADQL representation:

+ * + *
    + *
  • If negative: NegativeOperand(NumericConstant(val))
  • + *
  • Otherwise: NumericConstant(val)
  • + *
+ * + * @param val The value to embed in an ADQL object. + * @param factory The factory to use to created ADQL objects. + * + * @return The representing ADQL representation. + * + * @throws Exception If an error occurs while creating the ADQL object. + */ + private ADQLOperand toNumericObj(final double val, final ADQLQueryFactory factory) throws Exception{ + if (val >= 0) + return factory.createNumericConstant("" + val); + else + return factory.createNegativeOperand(factory.createNumericConstant("" + (val * -1))); + } + } + + /** + * Parse the given STC-S expression representing a geometrical region. + * + * @param stcsRegion STC-S expression of a region. Note: MUST be different from NULL. + * + * @return The object representation of the specified geometrical region. + * + * @throws ParseException If the given expression is NULL, empty string or if the STC-S syntax is wrong. + */ + public static Region parseRegion(final String stcsRegion) throws ParseException{ + if (stcsRegion == null || stcsRegion.trim().length() == 0) + throw new ParseException("Missing STC-S expression to parse!"); + return (new STCSParser().parseRegion(stcsRegion)); + } + + /** + * Convert into STC-S the given object representation of a geometrical region. + * + * @param region Region to convert into STC-S. + * + * @return The corresponding STC-S expression. + */ + public static String toSTCS(final Region region){ + if (region == null) + throw new NullPointerException("Missing region to serialize into STC-S!"); + return region.toSTCS(); + } + + /** + *

Convert into STC-S the given ADQL representation of a geometrical function.

+ * + *

Important note: + * Only {@link PointFunction}, {@link CircleFunction}, {@link BoxFunction}, {@link PolygonFunction} + * and {@link RegionFunction} are accepted here. Other extensions of {@link GeometryFunction} will + * throw an {@link IllegalArgumentException}. + *

+ * + * @param region ADQL representation of the region to convert into STC-S. + * + * @return The corresponding STC-S expression. + * + * @throws ParseException If the given object is NULL or not of the good type. + */ + public static String toSTCS(final GeometryFunction region) throws ParseException{ + if (region == null) + throw new NullPointerException("Missing region to serialize into STC-S!"); + return (new Region(region)).toSTCS(); + } + + /* *************************** */ + /* PARSER OF STC-S EXPRESSIONS */ + /* *************************** */ + + /** + * Let parse any STC-S expression. + * + * @author Grégory Mantelet (ARI) + * @version 1.3 (11/2014) + * @since 1.3 + */ + private static class STCSParser { + /** Regular expression of a numerical value. */ + private final static String numericRegExp = "(\\+|-)?(\\d+(\\.\\d*)?|\\.\\d+)([Ee](\\+|-)?\\d+)?"; + + /** Position of the next characters to read in the STC-S expression to parse. */ + private int pos; + /** Full STC-S expression to parse. */ + private String stcs; + /** Last read token (can be a numeric, a string, a region type, ...). */ + private String token; + /** Buffer used to read tokens. */ + private StringBuffer buffer; + + /** + * Exception sent when the end of the expression + * (EOE = End Of Expression) is reached. + * + * @author Grégory Mantelet (ARI) + * @version 1.3 (10/2014) + * @since 1.3 + */ + private static class EOEException extends ParseException { + private static final long serialVersionUID = 1L; + + /** Build a simple EOEException. */ + public EOEException(){ + super("Unexpected End Of Expression!"); + } + } + + /** + * Build the STC-S parser. + */ + public STCSParser(){} + + /** + * Parse the given STC-S expression, expected as a coordinate system. + * + * @param stcs The STC-S expression to parse. + * + * @return The corresponding object representation of the specified coordinate system. + * + * @throws ParseException If the syntax of the given STC-S expression is wrong or if it is not a coordinate system. + */ + public CoordSys parseCoordSys(final String stcs) throws ParseException{ + init(stcs); + CoordSys coordsys = null; + try{ + coordsys = coordSys(); + end(COORD_SYS_SYNTAX); + return coordsys; + }catch(EOEException ex){ + ex.printStackTrace(); + return new CoordSys(); + } + } + + /** + * Parse the given STC-S expression, expected as a geometrical region. + * + * @param stcs The STC-S expression to parse. + * + * @return The corresponding object representation of the specified geometrical region. + * + * @throws ParseException If the syntax of the given STC-S expression is wrong or if it is not a geometrical region. + */ + public Region parseRegion(final String stcs) throws ParseException{ + init(stcs); + Region region = region(); + end("\"POSITION \", \"CIRCLE \", \"BOX \", \"POLYGON [ ...]\", \"UNION ( [ ...] )\", \"INTERSECTION [] ( [ ...] )\" or \"NOT ( )\""); + return region; + } + + /** + * Prepare the parser in order to read the given STC-S expression. + * + * @param newStcs New STC-S expression to parse from now. + */ + private void init(final String newStcs){ + stcs = (newStcs == null) ? "" : newStcs; + token = null; + buffer = new StringBuffer(); + pos = 0; + } + + /** + * Finalize the parsing. + * No more characters (except eventually some space characters) should remain in the STC-S expression to parse. + * + * @param expectedSyntax Description of the good syntax expected. This description is used only to write the + * {@link ParseException} in case other non-space characters are found among the remaining characters. + * + * @throws ParseException If other non-space characters remains. + */ + private void end(final String expectedSyntax) throws ParseException{ + // Skip all spaces: + skipSpaces(); + + // If there is still some characters, they are not expected, and so throw an exception: + if (stcs.length() > 0 && pos < stcs.length()) + throw new ParseException("Incorrect syntax: \"" + stcs.substring(pos) + "\" was unexpected! Expected syntax: " + expectedSyntax + ".", new TextPosition(1, pos, 1, stcs.length())); + + // Reset the buffer, token and the STC-S expression to parse: + buffer = null; + stcs = null; + token = null; + } + + /** + * Tool function which skip all next space characters until the next meaningful characters. + */ + private void skipSpaces(){ + while(pos < stcs.length() && Character.isWhitespace(stcs.charAt(pos))) + pos++; + } + + /** + *

Get the next meaningful word. This word can be a numeric, any string constant or a region type.

+ * + *

+ * In case the end of the expression is reached before getting any meaningful character, an {@link EOEException} is thrown. + *

+ * + * @return The full read word/token. + * + * @throws EOEException If the end of the STC-S expression is reached before getting any meaningful character. + */ + private String nextToken() throws EOEException{ + // Skip all spaces: + skipSpaces(); + + // Fetch all characters until word separator (a space or a open/close parenthesis): + while(pos < stcs.length() && !Character.isWhitespace(stcs.charAt(pos)) && stcs.charAt(pos) != '(' && stcs.charAt(pos) != ')') + buffer.append(stcs.charAt(pos++)); + + // If no character has been fetched while at least one was expected, throw an exception: + if (buffer.length() == 0) + throw new EOEException(); + + // Save the read token and reset the buffer: + token = buffer.toString(); + buffer.delete(0, token.length()); + + return token; + } + + /** + * Read the next token as a numeric. + * If not a numeric, a {@link ParseException} is thrown. + * + * @return The read numerical value. + * + * @throws ParseException If the next token is not a numerical expression. + */ + private double numeric() throws ParseException{ + if (nextToken().matches(numericRegExp)) + return Double.parseDouble(token); + else + throw new ParseException("a numeric was expected!", new TextPosition(1, pos - token.length(), 1, pos)); // TODO Check the begin and end! + } + + /** + * Read the next 2 tokens as a coordinate pairs (so as 2 numerical values). + * If not 2 numeric, a {@link ParseException} is thrown. + * + * @return The read coordinate pairs. + * + * @throws ParseException If the next 2 tokens are not 2 numerical expressions. + */ + private double[] coordPair() throws ParseException{ + skipSpaces(); + int startPos = pos; + try{ + return new double[]{numeric(),numeric()}; + }catch(ParseException pe){ + if (pe instanceof EOEException) + throw pe; + else + throw new ParseException("a coordinates pair (2 numerics separated by one or more spaces) was expected!", new TextPosition(1, startPos, 1, pos)); // TODO Check the begin and end! + } + } + + /** + * Read and parse the next tokens as a coordinate system expression. + * If they do not match, a {@link ParseException} is thrown. + * + * @return The object representation of the read coordinate system. + * + * @throws ParseException If the next tokens are not representing a valid coordinate system. + */ + private CoordSys coordSys() throws ParseException{ + // Skip all spaces: + skipSpaces(); + + // Backup the current position: + /* (because every parts of a coordinate system are optional ; + * like this, it will be possible to go back in the expression + * to parse if optional parts are not written) */ + String oldToken = token; + int startPos = pos; + + Frame fr = null; + RefPos rp = null; + Flavor fl = null; + + try{ + // Read the token: + nextToken(); + // Try to parse it as a frame: + if ((fr = frame()) != null){ + // if success, go the next token: + startPos = pos; + oldToken = token; + nextToken(); + } + // Try to parse the last read token as a reference position: + if ((rp = refpos()) != null){ + // if success, go the next token: + startPos = pos; + oldToken = token; + nextToken(); + } + // Try to parse the last read token as a flavor: + if ((fl = flavor()) == null){ + // if NOT a success, go back "in time" (go back to the position before reading the token): + pos = startPos; + token = oldToken; + } + }catch(EOEException ex){ + /* End Of Expression may happen here since all parts of a coordinate system are optional. + * So, there is no need to treat the error. */ + } + + // Build the object representation of the read coordinate system: + /* Note: if nothing has been read for one or all parts of the coordinate system, + * the NULL value will be replaced automatically in the constructor + * by the default value of the corresponding part(s). */ + try{ + return new CoordSys(fr, rp, fl); + }catch(IllegalArgumentException iae){ + throw new ParseException(iae.getMessage(), new TextPosition(1, startPos, 1, pos)); + } + } + + /** + * Parse the last read token as FRAME. + * + * @return The corresponding enumeration item, or NULL if the last token is not a valid FRAME item. + */ + private Frame frame(){ + try{ + return Frame.valueOf(token.toUpperCase()); + }catch(IllegalArgumentException iae){ + return null; + } + } + + /** + * Parse the last read token as REFERENCE POSITION. + * + * @return The corresponding enumeration item, or NULL if the last token is not a valid REFERENCE POSITION item. + */ + private RefPos refpos(){ + try{ + return RefPos.valueOf(token.toUpperCase()); + }catch(IllegalArgumentException iae){ + return null; + } + } + + /** + * Parse the last read token as FLAVOR. + * + * @return The corresponding enumeration item, or NULL if the last token is not a valid FLAVOR item. + */ + private Flavor flavor(){ + try{ + return Flavor.valueOf(token.toUpperCase()); + }catch(IllegalArgumentException iae){ + return null; + } + } + + /** + * Read and parse the next tokens as a geometrical region. + * If they do not match, a {@link ParseException} is thrown. + * + * @return The object representation of the read geometrical region. + * + * @throws ParseException If the next tokens are not representing a valid geometrical region. + */ + private Region region() throws ParseException{ + // Skip all spaces: + skipSpaces(); + + // Read the next token (it should be the region type): + int startPos = pos; + token = nextToken().toUpperCase(); + + /* Identify the region type, next the expected parameters and finally build the corresponding object representation */ + // POSITION case: + if (token.equals("POSITION")){ + try{ + CoordSys coordSys = coordSys(); + double[] coords = coordPair(); + return new Region(coordSys, coords); + }catch(Exception e){ + throw buildException(e, "\"POSITION \", where coordPair=\" \" and coordSys=" + COORD_SYS_SYNTAX, startPos); + } + } + // CIRCLE case: + else if (token.equals("CIRCLE")){ + try{ + CoordSys coordSys = coordSys(); + double[] coords = coordPair(); + double radius = numeric(); + return new Region(coordSys, coords, radius); + }catch(Exception e){ + throw buildException(e, "\"CIRCLE \", where coordPair=\" \", radius=\"\" and coordSys=" + COORD_SYS_SYNTAX, startPos); + } + } + // BOX case: + else if (token.equals("BOX")){ + try{ + CoordSys coordSys = coordSys(); + double[] coords = coordPair(); + double width = numeric(), height = numeric(); + return new Region(coordSys, coords, width, height); + }catch(Exception e){ + throw buildException(e, "\"BOX \", where coordPair=\" \", width and height=\"\" and coordSys=" + COORD_SYS_SYNTAX, startPos); + } + } + // POLYGON case: + else if (token.equals("POLYGON")){ + try{ + CoordSys coordSys = coordSys(); + ArrayList coordinates = new ArrayList(6); + double[] coords; + for(int i = 0; i < 3; i++){ + coords = coordPair(); + coordinates.add(coords[0]); + coordinates.add(coords[1]); + } + boolean moreCoord = true; + int posBackup; + do{ + posBackup = pos; + try{ + coords = coordPair(); + coordinates.add(coords[0]); + coordinates.add(coords[1]); + }catch(ParseException pe){ + moreCoord = false; + pos = posBackup; + } + }while(moreCoord); + double[][] allCoords = new double[coordinates.size() / 2][2]; + for(int i = 0; i < coordinates.size() && i + 1 < coordinates.size(); i += 2) + allCoords[i / 2] = new double[]{coordinates.get(i),coordinates.get(i + 1)}; + return new Region(coordSys, allCoords); + }catch(Exception e){ + throw buildException(e, "\"POLYGON [ ...]\", where coordPair=\" \" and coordSys=" + COORD_SYS_SYNTAX, startPos); + } + } + // UNION & INTERSECTION cases: + else if (token.equals("UNION") || token.equals("INTERSECTION")){ + RegionType type = (token.equals("UNION") ? RegionType.UNION : RegionType.INTERSECTION); + try{ + CoordSys coordSys = coordSys(); + ArrayList regions = new ArrayList(2); + + skipSpaces(); + if (stcs.charAt(pos) != '(') + throw buildException(new ParseException("a opening parenthesis - ( - was expected!", new TextPosition(1, pos, 1, pos + 1)), "\"" + type + " ( [ ...] )\", where coordSys=" + COORD_SYS_SYNTAX, startPos); + else + pos++; + + // parse and add the FIRST region: + regions.add(region()); + + // parse and add the SECOND region: + regions.add(region()); + + skipSpaces(); + while(stcs.charAt(pos) != ')'){ + regions.add(region()); + skipSpaces(); + } + pos++; + + return new Region(type, coordSys, regions.toArray(new Region[regions.size()])); + }catch(Exception e){ + if (e instanceof ParseException && e.getMessage().startsWith("Incorrect syntax: \"")) + throw (ParseException)e; + else + throw buildException(e, "\"" + type + " ( [ ...] )\", where coordSys=" + COORD_SYS_SYNTAX, startPos); + } + } + // NOT case: + else if (token.equals("NOT")){ + try{ + skipSpaces(); + if (stcs.charAt(pos) != '(') + throw buildException(new ParseException("an opening parenthesis - ( - was expected!", new TextPosition(1, pos, 1, pos + 1)), "\"NOT ( )\"", startPos); + else + pos++; + Region region = region(); + skipSpaces(); + if (stcs.charAt(pos) != ')') + throw buildException(new ParseException("a closing parenthesis - ) - was expected!", new TextPosition(1, pos, 1, pos + 1)), "\"NOT ( )\"", startPos); + else + pos++; + return new Region(region); + }catch(Exception e){ + if (e instanceof ParseException && e.getMessage().startsWith("Incorrect syntax: ")) + throw (ParseException)e; + else + throw buildException(e, "\"NOT ( )\"", startPos); + } + } + // Otherwise, the region type is not known and so a ParseException is thrown: + else + throw new ParseException("Unknown STC region type: \"" + token + "\"!", new TextPosition(1, startPos, 1, pos)); + } + + /** + * Build a {@link ParseException} based on the given one and by adding the human description of what was expected, if needed. + * + * @param ex Root exception. + * @param expectedSyntax Human description of what was expected. + * @param startPos Position of the first character of the wrong part of expression. + * + * @return The build exception. + */ + private ParseException buildException(final Exception ex, final String expectedSyntax, int startPos){ + if (ex instanceof EOEException) + return new ParseException("Unexpected End Of Expression! Expected syntax: " + expectedSyntax + ".", new TextPosition(1, startPos, 1, pos)); + else if (ex instanceof ParseException) + return new ParseException("Incorrect syntax: " + ex.getMessage() + " Expected syntax: " + expectedSyntax + ".", (((ParseException)ex).getPosition() != null ? ((ParseException)ex).getPosition() : new TextPosition(1, startPos, 1, pos))); + else + return new ParseException(ex.getMessage(), new TextPosition(1, startPos, 1, pos)); + } + } +} diff --git a/src/adql/db/SearchColumnList.java b/src/adql/db/SearchColumnList.java index dbb9b97..b3117d6 100644 --- a/src/adql/db/SearchColumnList.java +++ b/src/adql/db/SearchColumnList.java @@ -17,7 +17,7 @@ package adql.db; * along with ADQLLibrary. If not, see . * * Copyright 2012-2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomishes Rechen Institute (ARI) + * Astronomisches Rechen Institut (ARI) */ import java.util.ArrayList; @@ -46,7 +46,7 @@ import cds.utils.TextualSearchList; *

* * @author Grégory Mantelet (CDS;ARI) - * @version 1.2 (11/2013) + * @version 1.3 (02/2015) */ public class SearchColumnList extends TextualSearchList { private static final long serialVersionUID = 1L; @@ -284,7 +284,7 @@ public class SearchColumnList extends TextualSearchList { } // test the schema name: - if (schema != null){ + if (schema != null && matchTable.getADQLSchemaName() != null){ if (IdentifierField.SCHEMA.isCaseSensitive(caseSensitivity)){ if (!matchTable.getADQLSchemaName().equals(schema)) continue; @@ -307,7 +307,6 @@ public class SearchColumnList extends TextualSearchList { // if here, all prefixes are matching and so the column is a good match: DBColumn goodMatch = matchTable.getColumn(match.getADQLName(), true); - System.out.println("Good match for \"" + catalog + "." + schema + "." + table + "." + column + "\" found: " + goodMatch); result.add(goodMatch); } } diff --git a/src/adql/db/SearchTableList.java b/src/adql/db/SearchTableList.java index 649b4fd..7de6ec5 100644 --- a/src/adql/db/SearchTableList.java +++ b/src/adql/db/SearchTableList.java @@ -16,13 +16,14 @@ package adql.db; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Astronomisches Rechen Institut (ARI) */ import java.util.ArrayList; import java.util.Collection; -import adql.query.IdentifierField; +import adql.query.IdentifierField; import adql.query.from.ADQLTable; import cds.utils.TextualSearchList; @@ -34,8 +35,8 @@ import cds.utils.TextualSearchList; * These last information will be used only if the ADQL table name is ambiguous, otherwise all matching elements are returned. *

* - * @author Grégory Mantelet (CDS) - * @version 09/2011 + * @author Grégory Mantelet (CDS;ARI) + * @version 1.3 (02/2015) */ public class SearchTableList extends TextualSearchList { private static final long serialVersionUID = 1L; @@ -58,7 +59,7 @@ public class SearchTableList extends TextualSearchList { * * @param collection Collection of {@link DBTable} to copy. */ - public SearchTableList(final Collection collection){ + public SearchTableList(final Collection collection){ super(collection, new DBTableKeyExtractor()); } @@ -155,22 +156,24 @@ public class SearchTableList extends TextualSearchList { ArrayList result = new ArrayList(); for(DBTable match : tmpResult){ - if (IdentifierField.SCHEMA.isCaseSensitive(caseSensitivity)){ - if (!match.getADQLSchemaName().equals(schema)) - continue; - }else{ - if (!match.getADQLSchemaName().equalsIgnoreCase(schema)) - continue; - } - - if (catalog != null){ - if (IdentifierField.CATALOG.isCaseSensitive(caseSensitivity)){ - if (!match.getADQLCatalogName().equals(catalog)) + if (match.getADQLSchemaName() != null){ + if (IdentifierField.SCHEMA.isCaseSensitive(caseSensitivity)){ + if (!match.getADQLSchemaName().equals(schema)) continue; }else{ - if (!match.getADQLCatalogName().equalsIgnoreCase(catalog)) + if (!match.getADQLSchemaName().equalsIgnoreCase(schema)) continue; } + + if (catalog != null && match.getADQLCatalogName() != null){ + if (IdentifierField.CATALOG.isCaseSensitive(caseSensitivity)){ + if (!match.getADQLCatalogName().equals(catalog)) + continue; + }else{ + if (!match.getADQLCatalogName().equalsIgnoreCase(catalog)) + continue; + } + } } result.add(match); @@ -199,6 +202,7 @@ public class SearchTableList extends TextualSearchList { * @version 09/2011 */ private static class DBTableKeyExtractor implements KeyExtractor { + @Override public String getKey(DBTable obj){ return obj.getADQLName(); } diff --git a/src/adql/db/exception/UnresolvedFunctionException.java b/src/adql/db/exception/UnresolvedFunctionException.java new file mode 100644 index 0000000..befb556 --- /dev/null +++ b/src/adql/db/exception/UnresolvedFunctionException.java @@ -0,0 +1,124 @@ +package adql.db.exception; + +/* + * This file is part of ADQLLibrary. + * + * ADQLLibrary is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ADQLLibrary is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ADQLLibrary. If not, see . + * + * Copyright 2014-2015 - Astronomisches Rechen Institut (ARI) + */ + +import adql.parser.ParseException; +import adql.query.operand.function.ADQLFunction; + +/** + * Exception thrown when a function can not be resolved by the library. + * + * @author Grégory Mantelet (ARI) + * @version 1.3 (05/2015) + * @since 1.3 + */ +public class UnresolvedFunctionException extends ParseException { + private static final long serialVersionUID = 1L; + + /** Function which can not be resolved. */ + protected final ADQLFunction functionInError; + + /** + * Build the exception with just a message. + * + * @param message Description of the error. + */ + public UnresolvedFunctionException(final String message){ + super(message); + functionInError = null; + } + + /** + * Build the exception with the unresolved function in parameter. + * The position of this function in the ADQL query can be retrieved and used afterwards. + * + * @param fct The unresolved function. + */ + public UnresolvedFunctionException(final ADQLFunction fct){ + super("Unresolved function: \"" + fct.toADQL() + "\"! No UDF has been defined or found with the signature: " + getFctSignature(fct) + "."); // TODO Add the position of the function in the ADQL query! + functionInError = fct; + } + + /** + * Build the exception with a message but also with the unresolved function in parameter. + * The position of this function in the ADQL query can be retrieved and used afterwards. + * + * @param message Description of the error. + * @param fct The unresolved function. + */ + public UnresolvedFunctionException(final String message, final ADQLFunction fct){ + super(message); // TODO Add the position of the function in the ADQL query! + functionInError = fct; + } + + /** + * Get the unresolved function at the origin of this exception. + * + * @return The unresolved function. Note: MAY be NULL + */ + public final ADQLFunction getFunction(){ + return functionInError; + } + + /** + *

Get the signature of the function given in parameter.

+ * + *

+ * In this signature, just the name and the type of all the parameters are written. + * The return type is never part of a function signature. + *

+ * + *

Note 1: + * A parameter type can be either "NUMERIC", "STRING" or "GEOMETRY". In order to be the most generic has possible, + * no more precision about a type is returned here. If the parameter is none of these type kinds, "???" is returned. + *

+ * + *

Note 2: + * If the given object is NULL, an empty string is returned. + *

+ * + * @param fct Function whose the signature must be returned. + * + * @return The corresponding signature. + */ + public static String getFctSignature(final ADQLFunction fct){ + if (fct == null) + return ""; + + StringBuffer buf = new StringBuffer(fct.getName().toLowerCase()); + buf.append('('); + for(int i = 0; i < fct.getNbParameters(); i++){ + if (fct.getParameter(i).isNumeric()) + buf.append("NUMERIC"); + else if (fct.getParameter(i).isString()) + buf.append("STRING"); + else if (fct.getParameter(i).isGeometry()) + buf.append("GEOMETRY"); + else + buf.append("???"); + + if ((i + 1) < fct.getNbParameters()) + buf.append(", "); + } + buf.append(')'); + return buf.toString(); + } + +} diff --git a/src/adql/db/exception/UnresolvedIdentifiersException.java b/src/adql/db/exception/UnresolvedIdentifiersException.java index b141bef..446ae79 100644 --- a/src/adql/db/exception/UnresolvedIdentifiersException.java +++ b/src/adql/db/exception/UnresolvedIdentifiersException.java @@ -16,7 +16,8 @@ package adql.db.exception; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.util.ArrayList; @@ -36,8 +37,8 @@ import adql.parser.ParseException; * on this {@link UnresolvedIdentifiersException} (method {@link #iterator()}). *

* - * @author Grégory Mantelet (CDS) - * @version 06/2012 + * @author Grégory Mantelet (CDS;ARI) + * @version 1.4 (06/2015) * * @see DBChecker */ @@ -65,12 +66,16 @@ public class UnresolvedIdentifiersException extends ParseException implements It exceptions.add(pe); if (pe instanceof UnresolvedColumnException){ String colName = ((UnresolvedColumnException)pe).getColumnName(); - if (colName != null && !colName.trim().isEmpty()) + if (colName != null && colName.trim().length() > 0) addIdentifierName(colName + " " + pe.getPosition()); }else if (pe instanceof UnresolvedTableException){ String tableName = ((UnresolvedTableException)pe).getTableName(); - if (tableName != null && !tableName.trim().isEmpty()) + if (tableName != null && tableName.trim().length() > 0) addIdentifierName(tableName + " " + pe.getPosition()); + }else if (pe instanceof UnresolvedFunctionException){ + String fctName = (((UnresolvedFunctionException)pe).getFunction() == null) ? null : ((UnresolvedFunctionException)pe).getFunction().getName() + "(...)"; + if (fctName != null && fctName.trim().length() > 0) + addIdentifierName(fctName + " " + pe.getPosition()); }else if (pe instanceof UnresolvedIdentifiersException) addIdentifierName(((UnresolvedIdentifiersException)pe).unresolvedIdentifiers); } @@ -82,7 +87,7 @@ public class UnresolvedIdentifiersException extends ParseException implements It * @param name Name (or description) of the identifier to add. */ private final void addIdentifierName(final String name){ - if (name != null && !name.trim().isEmpty()){ + if (name != null && name.trim().length() > 0){ if (unresolvedIdentifiers == null) unresolvedIdentifiers = ""; else @@ -109,6 +114,7 @@ public class UnresolvedIdentifiersException extends ParseException implements It return exceptions.iterator(); } + @Override public final Iterator iterator(){ return getErrors(); } @@ -120,7 +126,11 @@ public class UnresolvedIdentifiersException extends ParseException implements It */ @Override public String getMessage(){ - return exceptions.size() + " unresolved identifiers" + ((unresolvedIdentifiers != null) ? (": " + unresolvedIdentifiers) : "") + " !"; + StringBuffer buf = new StringBuffer(); + buf.append(exceptions.size()).append(" unresolved identifiers").append(((unresolvedIdentifiers != null) ? (": " + unresolvedIdentifiers) : "")).append('!'); + for(ParseException pe : exceptions) + buf.append("\n - ").append(pe.getMessage()); + return buf.toString(); } } diff --git a/src/adql/db/exception/UnresolvedJoin.java b/src/adql/db/exception/UnresolvedJoinException.java similarity index 85% rename from src/adql/db/exception/UnresolvedJoin.java rename to src/adql/db/exception/UnresolvedJoinException.java index ac47cfd..162bcd0 100644 --- a/src/adql/db/exception/UnresolvedJoin.java +++ b/src/adql/db/exception/UnresolvedJoinException.java @@ -16,7 +16,7 @@ package adql.db.exception; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2013-2014 - Astronomishes Rechen Institute (ARI) + * Copyright 2013-2015 - Astronomisches Rechen Institut (ARI) */ import adql.parser.ParseException; @@ -27,9 +27,10 @@ import adql.query.TextPosition; * and particularly because of the join condition (i.e. column names not found, ...). * * @author Grégory Mantelet (ARI) - gmantele@ari.uni-heidelberg.de - * @version 1.2 (11/2013) + * @version 1.3 (05/2015) + * @since 1.2 */ -public class UnresolvedJoin extends ParseException { +public class UnresolvedJoinException extends ParseException { private static final long serialVersionUID = 1L; /** @@ -38,7 +39,7 @@ public class UnresolvedJoin extends ParseException { * * @param message Message to display explaining why the join can't be resolved. */ - public UnresolvedJoin(String message){ + public UnresolvedJoinException(String message){ super(message); } @@ -48,7 +49,7 @@ public class UnresolvedJoin extends ParseException { * @param message Message to display explaining why the join can't be resolved. * @param errorPosition Position of the wrong part of the join. */ - public UnresolvedJoin(String message, TextPosition errorPosition){ + public UnresolvedJoinException(String message, TextPosition errorPosition){ super(message, errorPosition); } diff --git a/src/adql/parser/.gitignore b/src/adql/parser/.gitignore new file mode 100644 index 0000000..5e75933 --- /dev/null +++ b/src/adql/parser/.gitignore @@ -0,0 +1,3 @@ +/ADQLParser.java +/ADQLParserConstants.java +/ADQLParserTokenManager.java diff --git a/src/adql/parser/ADQLParser.java b/src/adql/parser/ADQLParser.java index f5f1e54..08f3482 100644 --- a/src/adql/parser/ADQLParser.java +++ b/src/adql/parser/ADQLParser.java @@ -4,7 +4,6 @@ package adql.parser; import java.io.FileReader; import java.io.IOException; import java.util.ArrayList; -import java.util.Collection; import java.util.Stack; import java.util.Vector; @@ -68,7 +67,7 @@ import adql.translator.TranslationException; * @see ADQLQueryFactory * * @author Grégory Mantelet (CDS;ARI) - gmantele@ari.uni-heidelberg.de -* @version 1.2 (03/2014) +* @version 1.4 (06/2015) */ public class ADQLParser implements ADQLParserConstants { @@ -87,9 +86,6 @@ public class ADQLParser implements ADQLParserConstants { /** The first token of a table/column name. This token is extracted by {@link #Identifier()}. */ private Token currentIdentifierToken = null; - /** List of all allowed coordinate systems. */ - private ArrayList allowedCoordSys = new ArrayList(); - /** * Builds an ADQL parser without a query to parse. */ @@ -327,24 +323,6 @@ public class ADQLParser implements ADQLParserConstants { return Query(); } - public final void addCoordinateSystem(final String coordSys){ - allowedCoordSys.add(coordSys); - } - - public final void setCoordinateSystems(final Collection coordSys){ - allowedCoordSys.clear(); - if (coordSys != null) - allowedCoordSys.addAll(coordSys); - } - - public final boolean isAllowedCoordSys(final String coordSys){ - for(String cs : allowedCoordSys){ - if (cs.equalsIgnoreCase(coordSys)) - return true; - } - return false; - } - public final void setDebug(boolean debug){ if (debug) enable_tracing(); @@ -500,1057 +478,1367 @@ public class ADQLParser implements ADQLParserConstants { * @throws ParseException If the query syntax is incorrect. */ final public ADQLQuery Query() throws ParseException{ - trace_call("Query"); + ADQLQuery q = null; + q = QueryExpression(); + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case 0: + jj_consume_token(0); + break; + case EOQ: + jj_consume_token(EOQ); + break; + default: + jj_la1[0] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + // check the query: + if (queryChecker != null) + queryChecker.check(q); + + { + if (true) + return q; + } + throw new Error("Missing return statement in function"); + } + + final public ADQLQuery QueryExpression() throws ParseException{ + TextPosition endPos = null; try{ - ADQLQuery q = null; - q = QueryExpression(); + // create the query: + query = queryFactory.createQuery(); + stackQuery.push(query); + }catch(Exception ex){ + { + if (true) + throw generateParseException(ex); + } + } + Select(); + From(); + endPos = query.getFrom().getPosition(); + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case WHERE: + Where(); + endPos = query.getWhere().getPosition(); + break; + default: + jj_la1[1] = jj_gen; + ; + } + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case GROUP_BY: + GroupBy(); + endPos = query.getGroupBy().getPosition(); + break; + default: + jj_la1[2] = jj_gen; + ; + } + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case HAVING: + Having(); + endPos = query.getHaving().getPosition(); + break; + default: + jj_la1[3] = jj_gen; + ; + } + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case ORDER_BY: + OrderBy(); + endPos = query.getOrderBy().getPosition(); + break; + default: + jj_la1[4] = jj_gen; + ; + } + // set the position of the query: + query.setPosition(new TextPosition(query.getSelect().getPosition(), endPos)); + + // get the previous query (!= null if the current query is a sub-query): + ADQLQuery previousQuery = stackQuery.pop(); + if (stackQuery.isEmpty()) + query = null; + else + query = stackQuery.peek(); + + { + if (true) + return previousQuery; + } + throw new Error("Missing return statement in function"); + } + + final public ADQLQuery SubQueryExpression() throws ParseException{ + ADQLQuery q = null; + Token start, end; + start = jj_consume_token(LEFT_PAR); + q = QueryExpression(); + end = jj_consume_token(RIGHT_PAR); + q.setPosition(new TextPosition(start, end)); + { + if (true) + return q; + } + throw new Error("Missing return statement in function"); + } + + final public void Select() throws ParseException{ + ClauseSelect select = query.getSelect(); + SelectItem item = null; + Token start, t = null; + start = jj_consume_token(SELECT); + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case QUANTIFIER: + t = jj_consume_token(QUANTIFIER); + select.setDistinctColumns(t.image.equalsIgnoreCase("DISTINCT")); + break; + default: + jj_la1[5] = jj_gen; + ; + } + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case TOP: + jj_consume_token(TOP); + t = jj_consume_token(UNSIGNED_INTEGER); + try{ + select.setLimit(Integer.parseInt(t.image)); + }catch(NumberFormatException nfe){ + { + if (true) + throw new ParseException("[l." + t.beginLine + ";c." + t.beginColumn + "] The TOP limit (\u005c"" + t.image + "\u005c") isn't a regular unsigned integer !"); + } + } + break; + default: + jj_la1[6] = jj_gen; + ; + } + item = SelectItem(); + select.add(item); + label_1: while(true){ switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case 0: - jj_consume_token(0); - break; - case EOQ: - jj_consume_token(EOQ); + case COMMA: + ; break; default: - jj_la1[0] = jj_gen; - jj_consume_token(-1); - throw new ParseException(); + jj_la1[7] = jj_gen; + break label_1; } - // check the query: - if (queryChecker != null) - queryChecker.check(q); + jj_consume_token(COMMA); + item = SelectItem(); + select.add(item); + } + TextPosition lastItemPos = query.getSelect().get(query.getSelect().size() - 1).getPosition(); + select.setPosition(new TextPosition(start.beginLine, start.beginColumn, lastItemPos.endLine, lastItemPos.endColumn)); + } + final public SelectItem SelectItem() throws ParseException{ + IdentifierItems identifiers = new IdentifierItems(true); + IdentifierItem id = null, label = null; + ADQLOperand op = null; + SelectItem item; + Token starToken; + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case ASTERISK: + starToken = jj_consume_token(ASTERISK); + item = new SelectAllColumns(query); + item.setPosition(new TextPosition(starToken)); + { + if (true) + return item; + } + break; + default: + jj_la1[11] = jj_gen; + if (jj_2_1(7)){ + id = Identifier(); + jj_consume_token(DOT); + identifiers.append(id); + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case DELIMITED_IDENTIFIER: + case REGULAR_IDENTIFIER: + id = Identifier(); + jj_consume_token(DOT); + identifiers.append(id); + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case DELIMITED_IDENTIFIER: + case REGULAR_IDENTIFIER: + id = Identifier(); + jj_consume_token(DOT); + identifiers.append(id); + break; + default: + jj_la1[8] = jj_gen; + ; + } + break; + default: + jj_la1[9] = jj_gen; + ; + } + starToken = jj_consume_token(ASTERISK); + try{ + item = new SelectAllColumns(queryFactory.createTable(identifiers, null)); + TextPosition firstPos = identifiers.get(0).position; + item.setPosition(new TextPosition(firstPos.beginLine, firstPos.beginColumn, starToken.endLine, (starToken.endColumn < 0) ? -1 : (starToken.endColumn + 1))); + { + if (true) + return item; + } + }catch(Exception ex){ + { + if (true) + throw generateParseException(ex); + } + } + }else{ + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case LEFT_PAR: + case PLUS: + case MINUS: + case AVG: + case MAX: + case MIN: + case SUM: + case COUNT: + case BOX: + case CENTROID: + case CIRCLE: + case POINT: + case POLYGON: + case REGION: + case CONTAINS: + case INTERSECTS: + case AREA: + case COORD1: + case COORD2: + case COORDSYS: + case DISTANCE: + case ABS: + case CEILING: + case DEGREES: + case EXP: + case FLOOR: + case LOG: + case LOG10: + case MOD: + case PI: + case POWER: + case RADIANS: + case RAND: + case ROUND: + case SQRT: + case TRUNCATE: + case ACOS: + case ASIN: + case ATAN: + case ATAN2: + case COS: + case COT: + case SIN: + case TAN: + case STRING_LITERAL: + case DELIMITED_IDENTIFIER: + case REGULAR_IDENTIFIER: + case SCIENTIFIC_NUMBER: + case UNSIGNED_FLOAT: + case UNSIGNED_INTEGER: + op = ValueExpression(); + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case AS: + jj_consume_token(AS); + label = Identifier(); + break; + default: + jj_la1[10] = jj_gen; + ; + } + break; + default: + jj_la1[12] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } + } + try{ + item = queryFactory.createSelectItem(op, (label == null) ? null : label.identifier); + if (label != null){ + item.setCaseSensitive(label.caseSensitivity); + item.setPosition(new TextPosition(op.getPosition(), label.position)); + }else + item.setPosition(new TextPosition(op.getPosition())); { if (true) - return q; + return item; + } + }catch(Exception ex){ + { + if (true) + throw generateParseException(ex); } - throw new Error("Missing return statement in function"); - }finally{ - trace_return("Query"); } + throw new Error("Missing return statement in function"); } - final public ADQLQuery QueryExpression() throws ParseException{ - trace_call("QueryExpression"); + final public void From() throws ParseException{ + FromContent content = null, content2 = null; try{ - TextPosition endPos = null; - try{ - // create the query: - query = queryFactory.createQuery(); - stackQuery.push(query); - }catch(Exception ex){ - { - if (true) - throw generateParseException(ex); + jj_consume_token(FROM); + content = TableRef(); + label_2: while(true){ + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case COMMA: + ; + break; + default: + jj_la1[13] = jj_gen; + break label_2; } + jj_consume_token(COMMA); + content2 = TableRef(); + TextPosition startPos = content.getPosition(), endPos = content2.getPosition(); + content = queryFactory.createJoin(JoinType.CROSS, content, content2); + content.setPosition(new TextPosition(startPos, endPos)); } - Select(); - From(); - endPos = query.getFrom().getPosition(); - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case WHERE: - Where(); - endPos = query.getWhere().getPosition(); - break; - default: - jj_la1[1] = jj_gen; - ; + query.setFrom(content); + }catch(Exception ex){ + { + if (true) + throw generateParseException(ex); } + } + } + + final public void Where() throws ParseException{ + ClauseConstraints where = query.getWhere(); + ADQLConstraint condition; + Token start; + start = jj_consume_token(WHERE); + ConditionsList(where); + TextPosition endPosition = where.getPosition(); + where.setPosition(new TextPosition(start.beginLine, start.beginColumn, endPosition.endLine, endPosition.endColumn)); + } + + final public void GroupBy() throws ParseException{ + ClauseADQL groupBy = query.getGroupBy(); + ColumnReference colRef = null; + Token start; + start = jj_consume_token(GROUP_BY); + colRef = ColumnRef(); + groupBy.add(colRef); + label_3: while(true){ switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case GROUP_BY: - GroupBy(); - endPos = query.getGroupBy().getPosition(); - break; - default: - jj_la1[2] = jj_gen; + case COMMA: ; - } - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case HAVING: - Having(); - endPos = query.getHaving().getPosition(); break; default: - jj_la1[3] = jj_gen; - ; + jj_la1[14] = jj_gen; + break label_3; } + jj_consume_token(COMMA); + colRef = ColumnRef(); + groupBy.add(colRef); + } + groupBy.setPosition(new TextPosition(start.beginLine, start.beginColumn, colRef.getPosition().endLine, colRef.getPosition().endColumn)); + } + + final public void Having() throws ParseException{ + ClauseConstraints having = query.getHaving(); + Token start; + start = jj_consume_token(HAVING); + ConditionsList(having); + TextPosition endPosition = having.getPosition(); + having.setPosition(new TextPosition(start.beginLine, start.beginColumn, endPosition.endLine, endPosition.endColumn)); + } + + final public void OrderBy() throws ParseException{ + ClauseADQL orderBy = query.getOrderBy(); + ADQLOrder order = null; + Token start; + start = jj_consume_token(ORDER_BY); + order = OrderItem(); + orderBy.add(order); + label_4: while(true){ switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case ORDER_BY: - OrderBy(); - endPos = query.getOrderBy().getPosition(); + case COMMA: + ; break; default: - jj_la1[4] = jj_gen; - ; + jj_la1[15] = jj_gen; + break label_4; } - // set the position of the query: - query.setPosition(new TextPosition(query.getSelect().getPosition(), endPos)); + jj_consume_token(COMMA); + order = OrderItem(); + orderBy.add(order); + } + orderBy.setPosition(new TextPosition(start.beginLine, start.beginColumn, order.getPosition().endLine, order.getPosition().endColumn)); + } - // get the previous query (!= null if the current query is a sub-query): - ADQLQuery previousQuery = stackQuery.pop(); - if (stackQuery.isEmpty()) - query = null; - else - query = stackQuery.peek(); + /* *************************** */ + /* COLUMN AND TABLE REFERENCES */ + /* *************************** */ + final public IdentifierItem Identifier() throws ParseException{ + Token t; + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case REGULAR_IDENTIFIER: + t = jj_consume_token(REGULAR_IDENTIFIER); + { + if (true) + return new IdentifierItem(t, false); + } + break; + case DELIMITED_IDENTIFIER: + t = jj_consume_token(DELIMITED_IDENTIFIER); + { + if (true) + return new IdentifierItem(t, true); + } + break; + default: + jj_la1[16] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + throw new Error("Missing return statement in function"); + } - { - if (true) - return previousQuery; - } - throw new Error("Missing return statement in function"); - }finally{ - trace_return("QueryExpression"); + /** + * Extracts the name of a table with its possible catalog and schema prefixes. + * + * @return A {@link IdentifierItems} which contains at most three items: catalogName, schemaName and tableName. + */ + final public IdentifierItems TableName() throws ParseException{ + IdentifierItems identifiers = new IdentifierItems(true); + IdentifierItem id = null; + id = Identifier(); + identifiers.append(id); + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case DOT: + jj_consume_token(DOT); + id = Identifier(); + identifiers.append(id); + break; + default: + jj_la1[17] = jj_gen; + ; + } + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case DOT: + jj_consume_token(DOT); + id = Identifier(); + identifiers.append(id); + break; + default: + jj_la1[18] = jj_gen; + ; + } + { + if (true) + return identifiers; } + throw new Error("Missing return statement in function"); } - final public ADQLQuery SubQueryExpression() throws ParseException{ - trace_call("SubQueryExpression"); + /** + * Extracts the name of a column with its possible catalog, schema and table prefixes. + * + * @return A {@link IdentifierItems} which contains at most four items: catalogName, schemaName, tableName and columnName. + */ + final public IdentifierItems ColumnName() throws ParseException{ + IdentifierItem id; + IdentifierItems table = null, identifiers = new IdentifierItems(false); + id = Identifier(); + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case DOT: + jj_consume_token(DOT); + table = TableName(); + break; + default: + jj_la1[19] = jj_gen; + ; + } + identifiers.append(id); + if (table != null){ + for(int i = 0; i < table.size(); i++) + identifiers.append(table.get(i)); + } + { + if (true) + return identifiers; + } + throw new Error("Missing return statement in function"); + } + + final public ADQLColumn Column() throws ParseException{ + IdentifierItems identifiers; + identifiers = ColumnName(); try{ - ADQLQuery q = null; - Token start, end; - start = jj_consume_token(LEFT_PAR); - q = QueryExpression(); - end = jj_consume_token(RIGHT_PAR); - q.setPosition(new TextPosition(start, end)); { if (true) - return q; + return queryFactory.createColumn(identifiers); + } + }catch(Exception ex){ + { + if (true) + throw generateParseException(ex); } - throw new Error("Missing return statement in function"); - }finally{ - trace_return("SubQueryExpression"); } + throw new Error("Missing return statement in function"); } - final public void Select() throws ParseException{ - trace_call("Select"); + final public ColumnReference ColumnRef() throws ParseException{ + IdentifierItems identifiers = null; + Token ind = null; + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case DELIMITED_IDENTIFIER: + case REGULAR_IDENTIFIER: + identifiers = ColumnName(); + break; + case UNSIGNED_INTEGER: + ind = jj_consume_token(UNSIGNED_INTEGER); + break; + default: + jj_la1[20] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } try{ - ClauseSelect select = query.getSelect(); - SelectItem item = null; - Token start, t = null; - start = jj_consume_token(SELECT); - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case QUANTIFIER: - t = jj_consume_token(QUANTIFIER); - select.setDistinctColumns(t.image.equalsIgnoreCase("DISTINCT")); - break; - default: - jj_la1[5] = jj_gen; - ; + ColumnReference colRef = null; + if (identifiers != null) + colRef = queryFactory.createColRef(identifiers); + else + colRef = queryFactory.createColRef(Integer.parseInt(ind.image), new TextPosition(ind)); + { + if (true) + return colRef; } - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case TOP: - jj_consume_token(TOP); - t = jj_consume_token(UNSIGNED_INTEGER); - try{ - select.setLimit(Integer.parseInt(t.image)); - }catch(NumberFormatException nfe){ - { - if (true) - throw new ParseException("[l." + t.beginLine + ";c." + t.beginColumn + "] The TOP limit (\u005c"" + t.image + "\u005c") isn't a regular unsigned integer !"); - } - } - break; - default: - jj_la1[6] = jj_gen; - ; + }catch(Exception ex){ + { + if (true) + throw generateParseException(ex); } - item = SelectItem(); - select.add(item); - label_1: while(true){ + } + throw new Error("Missing return statement in function"); + } + + final public ADQLOrder OrderItem() throws ParseException{ + IdentifierItems identifiers = null; + Token ind = null, desc = null; + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case DELIMITED_IDENTIFIER: + case REGULAR_IDENTIFIER: + identifiers = ColumnName(); + break; + case UNSIGNED_INTEGER: + ind = jj_consume_token(UNSIGNED_INTEGER); + break; + default: + jj_la1[21] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case ASC: + case DESC: switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case COMMA: - ; + case ASC: + jj_consume_token(ASC); + break; + case DESC: + desc = jj_consume_token(DESC); break; default: - jj_la1[7] = jj_gen; - break label_1; + jj_la1[22] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); } - jj_consume_token(COMMA); - item = SelectItem(); - select.add(item); + break; + default: + jj_la1[23] = jj_gen; + ; + } + try{ + ADQLOrder order = null; + if (identifiers != null){ + order = queryFactory.createOrder(identifiers, desc != null); + order.setPosition(identifiers.getPosition()); + }else{ + order = queryFactory.createOrder(Integer.parseInt(ind.image), desc != null); + order.setPosition(new TextPosition(ind)); + } + { + if (true) + return order; + } + }catch(Exception ex){ + { + if (true) + throw generateParseException(ex); } - TextPosition lastItemPos = query.getSelect().get(query.getSelect().size() - 1).getPosition(); - select.setPosition(new TextPosition(start.beginLine, start.beginColumn, lastItemPos.endLine, lastItemPos.endColumn)); - }finally{ - trace_return("Select"); } + throw new Error("Missing return statement in function"); } - final public SelectItem SelectItem() throws ParseException{ - trace_call("SelectItem"); + final public FromContent SimpleTableRef() throws ParseException{ + IdentifierItem alias = null; + IdentifierItems identifiers = null; + ADQLQuery subQuery = null; + FromContent content = null; + Token start, end; try{ - IdentifierItems identifiers = new IdentifierItems(true); - IdentifierItem id = null, label = null; - ADQLOperand op = null; - SelectItem item; - Token starToken; switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case ASTERISK: - starToken = jj_consume_token(ASTERISK); - item = new SelectAllColumns(query); - item.setPosition(new TextPosition(starToken)); + case DELIMITED_IDENTIFIER: + case REGULAR_IDENTIFIER: + identifiers = TableName(); + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case AS: + case DELIMITED_IDENTIFIER: + case REGULAR_IDENTIFIER: + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case AS: + jj_consume_token(AS); + break; + default: + jj_la1[24] = jj_gen; + ; + } + alias = Identifier(); + break; + default: + jj_la1[25] = jj_gen; + ; + } + content = queryFactory.createTable(identifiers, alias); + if (alias == null) + content.setPosition(new TextPosition(identifiers.get(0).position, identifiers.get(identifiers.size() - 1).position)); + else + content.setPosition(new TextPosition(identifiers.get(0).position, alias.position)); { if (true) - return item; + return content; } break; default: - jj_la1[11] = jj_gen; - if (jj_2_1(7)){ - id = Identifier(); - jj_consume_token(DOT); - identifiers.append(id); + jj_la1[27] = jj_gen; + if (jj_2_2(2)){ + subQuery = SubQueryExpression(); switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case DELIMITED_IDENTIFIER: - case REGULAR_IDENTIFIER: - id = Identifier(); - jj_consume_token(DOT); - identifiers.append(id); - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case DELIMITED_IDENTIFIER: - case REGULAR_IDENTIFIER: - id = Identifier(); - jj_consume_token(DOT); - identifiers.append(id); - break; - default: - jj_la1[8] = jj_gen; - ; - } + case AS: + jj_consume_token(AS); break; default: - jj_la1[9] = jj_gen; + jj_la1[26] = jj_gen; ; } - starToken = jj_consume_token(ASTERISK); - try{ - item = new SelectAllColumns(queryFactory.createTable(identifiers, null)); - TextPosition firstPos = identifiers.get(0).position; - item.setPosition(new TextPosition(firstPos.beginLine, firstPos.beginColumn, starToken.endLine, (starToken.endColumn < 0) ? -1 : (starToken.endColumn + 1))); - { - if (true) - return item; - } - }catch(Exception ex){ - { - if (true) - throw generateParseException(ex); - } + alias = Identifier(); + content = queryFactory.createTable(subQuery, alias); + if (alias == null) + content.setPosition(new TextPosition(subQuery.getPosition())); + else + content.setPosition(new TextPosition(subQuery.getPosition(), alias.position)); + { + if (true) + return content; } }else{ switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ case LEFT_PAR: - case PLUS: - case MINUS: - case AVG: - case MAX: - case MIN: - case SUM: - case COUNT: - case BOX: - case CENTROID: - case CIRCLE: - case POINT: - case POLYGON: - case REGION: - case CONTAINS: - case INTERSECTS: - case AREA: - case COORD1: - case COORD2: - case COORDSYS: - case DISTANCE: - case ABS: - case CEILING: - case DEGREES: - case EXP: - case FLOOR: - case LOG: - case LOG10: - case MOD: - case PI: - case POWER: - case RADIANS: - case RAND: - case ROUND: - case SQRT: - case TRUNCATE: - case ACOS: - case ASIN: - case ATAN: - case ATAN2: - case COS: - case COT: - case SIN: - case TAN: - case STRING_LITERAL: - case DELIMITED_IDENTIFIER: - case REGULAR_IDENTIFIER: - case SCIENTIFIC_NUMBER: - case UNSIGNED_FLOAT: - case UNSIGNED_INTEGER: - op = ValueExpression(); - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case AS: - jj_consume_token(AS); - label = Identifier(); - break; - default: - jj_la1[10] = jj_gen; - ; + start = jj_consume_token(LEFT_PAR); + content = JoinedTable(); + end = jj_consume_token(RIGHT_PAR); + content.setPosition(new TextPosition(start, end)); + { + if (true) + return content; } break; default: - jj_la1[12] = jj_gen; + jj_la1[28] = jj_gen; jj_consume_token(-1); throw new ParseException(); } } } - try{ - item = queryFactory.createSelectItem(op, (label == null) ? null : label.identifier); - if (label != null){ - item.setCaseSensitive(label.caseSensitivity); - item.setPosition(new TextPosition(op.getPosition(), label.position)); - }else - item.setPosition(new TextPosition(op.getPosition())); - { - if (true) - return item; - } - }catch(Exception ex){ - { - if (true) - throw generateParseException(ex); - } + }catch(Exception ex){ + { + if (true) + throw generateParseException(ex); } - throw new Error("Missing return statement in function"); - }finally{ - trace_return("SelectItem"); } + throw new Error("Missing return statement in function"); } - final public void From() throws ParseException{ - trace_call("From"); - try{ - FromContent content = null, content2 = null; - try{ - jj_consume_token(FROM); - content = TableRef(); - label_2: while(true){ - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case COMMA: - ; - break; - default: - jj_la1[13] = jj_gen; - break label_2; - } - jj_consume_token(COMMA); - content2 = TableRef(); - TextPosition startPos = content.getPosition(), endPos = content2.getPosition(); - content = queryFactory.createJoin(JoinType.CROSS, content, content2); - content.setPosition(new TextPosition(startPos, endPos)); - } - query.setFrom(content); - }catch(Exception ex){ - { - if (true) - throw generateParseException(ex); - } + final public FromContent TableRef() throws ParseException{ + FromContent content; + content = SimpleTableRef(); + label_5: while(true){ + if (jj_2_3(2)){ + ; + }else{ + break label_5; } - }finally{ - trace_return("From"); + content = JoinSpecification(content); } - } - - final public void Where() throws ParseException{ - trace_call("Where"); - try{ - ClauseConstraints where = query.getWhere(); - ADQLConstraint condition; - Token start; - start = jj_consume_token(WHERE); - ConditionsList(where); - TextPosition endPosition = where.getPosition(); - where.setPosition(new TextPosition(start.beginLine, start.beginColumn, endPosition.endLine, endPosition.endColumn)); - }finally{ - trace_return("Where"); + { + if (true) + return content; } + throw new Error("Missing return statement in function"); } - final public void GroupBy() throws ParseException{ - trace_call("GroupBy"); - try{ - ClauseADQL groupBy = query.getGroupBy(); - ColumnReference colRef = null; - Token start; - start = jj_consume_token(GROUP_BY); - colRef = ColumnRef(); - groupBy.add(colRef); - label_3: while(true){ - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case COMMA: - ; - break; - default: - jj_la1[14] = jj_gen; - break label_3; - } - jj_consume_token(COMMA); - colRef = ColumnRef(); - groupBy.add(colRef); + final public FromContent JoinedTable() throws ParseException{ + FromContent content; + content = SimpleTableRef(); + label_6: while(true){ + content = JoinSpecification(content); + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case NATURAL: + case INNER: + case RIGHT: + case LEFT: + case FULL: + case JOIN: + ; + break; + default: + jj_la1[29] = jj_gen; + break label_6; } - groupBy.setPosition(new TextPosition(start.beginLine, start.beginColumn, colRef.getPosition().endLine, colRef.getPosition().endColumn)); - }finally{ - trace_return("GroupBy"); - } - } - - final public void Having() throws ParseException{ - trace_call("Having"); - try{ - ClauseConstraints having = query.getHaving(); - Token start; - start = jj_consume_token(HAVING); - ConditionsList(having); - TextPosition endPosition = having.getPosition(); - having.setPosition(new TextPosition(start.beginLine, start.beginColumn, endPosition.endLine, endPosition.endColumn)); - }finally{ - trace_return("Having"); } - } - - final public void OrderBy() throws ParseException{ - trace_call("OrderBy"); - try{ - ClauseADQL orderBy = query.getOrderBy(); - ADQLOrder order = null; - Token start; - start = jj_consume_token(ORDER_BY); - order = OrderItem(); - orderBy.add(order); - label_4: while(true){ - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case COMMA: - ; - break; - default: - jj_la1[15] = jj_gen; - break label_4; - } - jj_consume_token(COMMA); - order = OrderItem(); - orderBy.add(order); - } - orderBy.setPosition(new TextPosition(start.beginLine, start.beginColumn, order.getPosition().endLine, order.getPosition().endColumn)); - }finally{ - trace_return("OrderBy"); + { + if (true) + return content; } + throw new Error("Missing return statement in function"); } - /* *************************** */ - /* COLUMN AND TABLE REFERENCES */ - /* *************************** */ - final public IdentifierItem Identifier() throws ParseException{ - trace_call("Identifier"); + final public ADQLJoin JoinSpecification(FromContent leftTable) throws ParseException{ + boolean natural = false; + JoinType type = JoinType.INNER; + ClauseConstraints condition = new ClauseConstraints("ON"); + ArrayList lstColumns = new ArrayList(); + IdentifierItem id; + FromContent rightTable; + ADQLJoin join; + Token lastPar; try{ - Token t; switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case REGULAR_IDENTIFIER: - t = jj_consume_token(REGULAR_IDENTIFIER); + case NATURAL: + jj_consume_token(NATURAL); + natural = true; + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case INNER: + case RIGHT: + case LEFT: + case FULL: + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case INNER: + jj_consume_token(INNER); + break; + case RIGHT: + case LEFT: + case FULL: + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case LEFT: + jj_consume_token(LEFT); + type = JoinType.OUTER_LEFT; + break; + case RIGHT: + jj_consume_token(RIGHT); + type = JoinType.OUTER_RIGHT; + break; + case FULL: + jj_consume_token(FULL); + type = JoinType.OUTER_FULL; + break; + default: + jj_la1[30] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case OUTER: + jj_consume_token(OUTER); + break; + default: + jj_la1[31] = jj_gen; + ; + } + break; + default: + jj_la1[32] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + break; + default: + jj_la1[33] = jj_gen; + ; + } + jj_consume_token(JOIN); + rightTable = TableRef(); + join = queryFactory.createJoin(type, leftTable, rightTable); + join.setPosition(new TextPosition(leftTable.getPosition(), rightTable.getPosition())); { if (true) - return new IdentifierItem(t, false); + return join; } break; - case DELIMITED_IDENTIFIER: - t = jj_consume_token(DELIMITED_IDENTIFIER); - { - if (true) - return new IdentifierItem(t, true); + case INNER: + case RIGHT: + case LEFT: + case FULL: + case JOIN: + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case INNER: + case RIGHT: + case LEFT: + case FULL: + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case INNER: + jj_consume_token(INNER); + break; + case RIGHT: + case LEFT: + case FULL: + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case LEFT: + jj_consume_token(LEFT); + type = JoinType.OUTER_LEFT; + break; + case RIGHT: + jj_consume_token(RIGHT); + type = JoinType.OUTER_RIGHT; + break; + case FULL: + jj_consume_token(FULL); + type = JoinType.OUTER_FULL; + break; + default: + jj_la1[34] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case OUTER: + jj_consume_token(OUTER); + break; + default: + jj_la1[35] = jj_gen; + ; + } + break; + default: + jj_la1[36] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + break; + default: + jj_la1[37] = jj_gen; + ; + } + jj_consume_token(JOIN); + rightTable = TableRef(); + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case ON: + jj_consume_token(ON); + ConditionsList(condition); + join = queryFactory.createJoin(type, leftTable, rightTable, condition); + join.setPosition(new TextPosition(leftTable.getPosition(), condition.getPosition())); + { + if (true) + return join; + } + break; + case USING: + jj_consume_token(USING); + jj_consume_token(LEFT_PAR); + id = Identifier(); + lstColumns.add(queryFactory.createColumn(id)); + label_7: while(true){ + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case COMMA: + ; + break; + default: + jj_la1[38] = jj_gen; + break label_7; + } + jj_consume_token(COMMA); + id = Identifier(); + lstColumns.add(queryFactory.createColumn(id)); + } + lastPar = jj_consume_token(RIGHT_PAR); + join = queryFactory.createJoin(type, leftTable, rightTable, lstColumns); + join.setPosition(new TextPosition(leftTable.getPosition().beginLine, leftTable.getPosition().beginColumn, lastPar.endLine, (lastPar.endColumn < 0) ? -1 : (lastPar.endColumn + 1))); + { + if (true) + return join; + } + break; + default: + jj_la1[39] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); } break; default: - jj_la1[16] = jj_gen; + jj_la1[40] = jj_gen; jj_consume_token(-1); throw new ParseException(); } - throw new Error("Missing return statement in function"); - }finally{ - trace_return("Identifier"); + }catch(Exception ex){ + { + if (true) + throw generateParseException(ex); + } } + throw new Error("Missing return statement in function"); } - /** - * Extracts the name of a table with its possible catalog and schema prefixes. - * - * @return A {@link IdentifierItems} which contains at most three items: catalogName, schemaName and tableName. - */ - final public IdentifierItems TableName() throws ParseException{ - trace_call("TableName"); - try{ - IdentifierItems identifiers = new IdentifierItems(true); - IdentifierItem id = null; - id = Identifier(); - identifiers.append(id); + /* ****** */ + /* STRING */ + /* ****** */ + final public StringConstant String() throws ParseException{ + Token t; + String str = ""; + StringConstant cst; + label_8: while(true){ + t = jj_consume_token(STRING_LITERAL); + str += t.image; switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case DOT: - jj_consume_token(DOT); - id = Identifier(); - identifiers.append(id); - break; - default: - jj_la1[17] = jj_gen; + case STRING_LITERAL: ; - } - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case DOT: - jj_consume_token(DOT); - id = Identifier(); - identifiers.append(id); break; default: - jj_la1[18] = jj_gen; - ; + jj_la1[41] = jj_gen; + break label_8; } + } + try{ + str = (str != null) ? str.substring(1, str.length() - 1) : str; + cst = queryFactory.createStringConstant(str); + cst.setPosition(new TextPosition(t)); { if (true) - return identifiers; + return cst; + } + }catch(Exception ex){ + { + if (true) + throw generateParseException(ex); } - throw new Error("Missing return statement in function"); - }finally{ - trace_return("TableName"); } + throw new Error("Missing return statement in function"); } - /** - * Extracts the name of a column with its possible catalog, schema and table prefixes. - * - * @return A {@link IdentifierItems} which contains at most four items: catalogName, schemaName, tableName and columnName. - */ - final public IdentifierItems ColumnName() throws ParseException{ - trace_call("ColumnName"); + /* ************* */ + /* NUMERIC TYPES */ + /* ************* */ + final public NumericConstant UnsignedNumeric() throws ParseException{ + Token t; + NumericConstant cst; + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case SCIENTIFIC_NUMBER: + t = jj_consume_token(SCIENTIFIC_NUMBER); + break; + case UNSIGNED_FLOAT: + t = jj_consume_token(UNSIGNED_FLOAT); + break; + case UNSIGNED_INTEGER: + t = jj_consume_token(UNSIGNED_INTEGER); + break; + default: + jj_la1[42] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } try{ - IdentifierItem id; - IdentifierItems table = null, identifiers = new IdentifierItems(false); - id = Identifier(); - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case DOT: - jj_consume_token(DOT); - table = TableName(); - break; - default: - jj_la1[19] = jj_gen; - ; - } - identifiers.append(id); - if (table != null){ - for(int i = 0; i < table.size(); i++) - identifiers.append(table.get(i)); + cst = queryFactory.createNumericConstant(t.image); + cst.setPosition(new TextPosition(t)); + { + if (true) + return cst; } + }catch(Exception ex){ { if (true) - return identifiers; + throw generateParseException(ex); } - throw new Error("Missing return statement in function"); - }finally{ - trace_return("ColumnName"); } + throw new Error("Missing return statement in function"); } - final public ADQLColumn Column() throws ParseException{ - trace_call("Column"); + final public NumericConstant UnsignedFloat() throws ParseException{ + Token t; + NumericConstant cst; + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case UNSIGNED_INTEGER: + t = jj_consume_token(UNSIGNED_INTEGER); + break; + case UNSIGNED_FLOAT: + t = jj_consume_token(UNSIGNED_FLOAT); + break; + default: + jj_la1[43] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } try{ - IdentifierItems identifiers; - identifiers = ColumnName(); - try{ - { - if (true) - return queryFactory.createColumn(identifiers); - } - }catch(Exception ex){ - { - if (true) - throw generateParseException(ex); + cst = queryFactory.createNumericConstant(t.image); + cst.setPosition(new TextPosition(t)); + { + if (true) + return cst; + } + }catch(Exception ex){ + { + if (true) + throw generateParseException(ex); + } + } + throw new Error("Missing return statement in function"); + } + + final public NumericConstant SignedInteger() throws ParseException{ + Token sign = null, number; + NumericConstant cst; + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case PLUS: + case MINUS: + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case PLUS: + sign = jj_consume_token(PLUS); + break; + case MINUS: + sign = jj_consume_token(MINUS); + break; + default: + jj_la1[44] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); } + break; + default: + jj_la1[45] = jj_gen; + ; + } + number = jj_consume_token(UNSIGNED_INTEGER); + try{ + if (sign == null){ + cst = queryFactory.createNumericConstant(number.image); + cst.setPosition(new TextPosition(number)); + }else{ + cst = queryFactory.createNumericConstant(sign.image + number.image); + cst.setPosition(new TextPosition(sign, number)); + } + { + if (true) + return cst; + } + }catch(Exception ex){ + { + if (true) + throw generateParseException(ex); } - throw new Error("Missing return statement in function"); - }finally{ - trace_return("Column"); } + throw new Error("Missing return statement in function"); } - final public ColumnReference ColumnRef() throws ParseException{ - trace_call("ColumnRef"); + /* *********** */ + /* EXPRESSIONS */ + /* *********** */ + final public ADQLOperand NumericValueExpressionPrimary() throws ParseException{ + String expr; + ADQLColumn column; + ADQLOperand op; + Token left, right; try{ - IdentifierItems identifiers = null; - Token ind = null; switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case SCIENTIFIC_NUMBER: + case UNSIGNED_FLOAT: + case UNSIGNED_INTEGER: + // unsigned_value_specification + op = UnsignedNumeric(); + { + if (true) + return op; + } + break; case DELIMITED_IDENTIFIER: case REGULAR_IDENTIFIER: - identifiers = ColumnName(); + column = Column(); + column.setExpectedType('N'); + { + if (true) + return column; + } break; - case UNSIGNED_INTEGER: - ind = jj_consume_token(UNSIGNED_INTEGER); + case AVG: + case MAX: + case MIN: + case SUM: + case COUNT: + op = SqlFunction(); + { + if (true) + return op; + } + break; + case LEFT_PAR: + left = jj_consume_token(LEFT_PAR); + op = NumericExpression(); + right = jj_consume_token(RIGHT_PAR); + WrappedOperand wop = queryFactory.createWrappedOperand(op); + wop.setPosition(new TextPosition(left, right)); + { + if (true) + return wop; + } break; default: - jj_la1[20] = jj_gen; + jj_la1[46] = jj_gen; jj_consume_token(-1); throw new ParseException(); } - try{ - ColumnReference colRef = null; - if (identifiers != null) - colRef = queryFactory.createColRef(identifiers); - else - colRef = queryFactory.createColRef(Integer.parseInt(ind.image), new TextPosition(ind)); - { - if (true) - return colRef; - } - }catch(Exception ex){ - { - if (true) - throw generateParseException(ex); - } + }catch(Exception ex){ + { + if (true) + throw generateParseException(ex); } - throw new Error("Missing return statement in function"); - }finally{ - trace_return("ColumnRef"); } + throw new Error("Missing return statement in function"); } - final public ADQLOrder OrderItem() throws ParseException{ - trace_call("OrderItem"); + final public ADQLOperand StringValueExpressionPrimary() throws ParseException{ + StringConstant expr; + ADQLColumn column; + ADQLOperand op; try{ - IdentifierItems identifiers = null; - Token ind = null, desc = null; switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case STRING_LITERAL: + // string + expr = String(); + { + if (true) + return expr; + } + break; case DELIMITED_IDENTIFIER: case REGULAR_IDENTIFIER: - identifiers = ColumnName(); + column = Column(); + column.setExpectedType('S'); + { + if (true) + return column; + } break; - case UNSIGNED_INTEGER: - ind = jj_consume_token(UNSIGNED_INTEGER); + case LEFT_PAR: + jj_consume_token(LEFT_PAR); + op = StringExpression(); + jj_consume_token(RIGHT_PAR); + { + if (true) + return queryFactory.createWrappedOperand(op); + } break; default: - jj_la1[21] = jj_gen; + jj_la1[47] = jj_gen; jj_consume_token(-1); throw new ParseException(); } - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case ASC: - case DESC: - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case ASC: - jj_consume_token(ASC); - break; - case DESC: - desc = jj_consume_token(DESC); - break; - default: - jj_la1[22] = jj_gen; - jj_consume_token(-1); - throw new ParseException(); - } - break; - default: - jj_la1[23] = jj_gen; - ; - } - try{ - ADQLOrder order = null; - if (identifiers != null){ - order = queryFactory.createOrder(identifiers, desc != null); - order.setPosition(identifiers.getPosition()); - }else{ - order = queryFactory.createOrder(Integer.parseInt(ind.image), desc != null); - order.setPosition(new TextPosition(ind)); - } - { - if (true) - return order; - } - }catch(Exception ex){ - { - if (true) - throw generateParseException(ex); - } + }catch(Exception ex){ + { + if (true) + throw generateParseException(ex); } - throw new Error("Missing return statement in function"); - }finally{ - trace_return("OrderItem"); } + throw new Error("Missing return statement in function"); } - final public FromContent SimpleTableRef() throws ParseException{ - trace_call("SimpleTableRef"); + final public ADQLOperand ValueExpression() throws ParseException{ + ADQLOperand valueExpr = null; try{ - IdentifierItem alias = null; - IdentifierItems identifiers = null; - ADQLQuery subQuery = null; - FromContent content = null; - Token start, end; - try{ + if (jj_2_4(2147483647)){ + valueExpr = NumericExpression(); + }else if (jj_2_5(2147483647)){ + valueExpr = StringExpression(); + }else if (jj_2_6(2147483647)){ + jj_consume_token(LEFT_PAR); + valueExpr = ValueExpression(); + jj_consume_token(RIGHT_PAR); + valueExpr = queryFactory.createWrappedOperand(valueExpr); + }else if (jj_2_7(2147483647)){ + valueExpr = UserDefinedFunction(); + }else{ switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case DELIMITED_IDENTIFIER: - case REGULAR_IDENTIFIER: - identifiers = TableName(); - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case AS: - case DELIMITED_IDENTIFIER: - case REGULAR_IDENTIFIER: - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case AS: - jj_consume_token(AS); - break; - default: - jj_la1[24] = jj_gen; - ; - } - alias = Identifier(); - break; - default: - jj_la1[25] = jj_gen; - ; - } - content = queryFactory.createTable(identifiers, alias); - if (alias == null) - content.setPosition(new TextPosition(identifiers.get(0).position, identifiers.get(identifiers.size() - 1).position)); - else - content.setPosition(new TextPosition(identifiers.get(0).position, alias.position)); - { - if (true) - return content; - } + case BOX: + case CENTROID: + case CIRCLE: + case POINT: + case POLYGON: + case REGION: + valueExpr = GeometryValueFunction(); break; default: - jj_la1[27] = jj_gen; - if (jj_2_2(2)){ - subQuery = SubQueryExpression(); - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case AS: - jj_consume_token(AS); - break; - default: - jj_la1[26] = jj_gen; - ; - } - alias = Identifier(); - content = queryFactory.createTable(subQuery, alias); - if (alias == null) - content.setPosition(new TextPosition(subQuery.getPosition())); - else - content.setPosition(new TextPosition(subQuery.getPosition(), alias.position)); - { - if (true) - return content; - } + jj_la1[48] = jj_gen; + if (jj_2_8(2147483647)){ + valueExpr = Column(); + }else if (jj_2_9(2147483647)){ + valueExpr = StringFactor(); }else{ switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ case LEFT_PAR: - start = jj_consume_token(LEFT_PAR); - content = JoinedTable(); - end = jj_consume_token(RIGHT_PAR); - content.setPosition(new TextPosition(start, end)); - { - if (true) - return content; - } + case PLUS: + case MINUS: + case AVG: + case MAX: + case MIN: + case SUM: + case COUNT: + case CONTAINS: + case INTERSECTS: + case AREA: + case COORD1: + case COORD2: + case DISTANCE: + case ABS: + case CEILING: + case DEGREES: + case EXP: + case FLOOR: + case LOG: + case LOG10: + case MOD: + case PI: + case POWER: + case RADIANS: + case RAND: + case ROUND: + case SQRT: + case TRUNCATE: + case ACOS: + case ASIN: + case ATAN: + case ATAN2: + case COS: + case COT: + case SIN: + case TAN: + case DELIMITED_IDENTIFIER: + case REGULAR_IDENTIFIER: + case SCIENTIFIC_NUMBER: + case UNSIGNED_FLOAT: + case UNSIGNED_INTEGER: + valueExpr = Factor(); break; default: - jj_la1[28] = jj_gen; + jj_la1[49] = jj_gen; jj_consume_token(-1); throw new ParseException(); } } } - }catch(Exception ex){ - { - if (true) - throw generateParseException(ex); - } - } - throw new Error("Missing return statement in function"); - }finally{ - trace_return("SimpleTableRef"); - } - } - - final public FromContent TableRef() throws ParseException{ - trace_call("TableRef"); - try{ - FromContent content; - content = SimpleTableRef(); - label_5: while(true){ - if (jj_2_3(2)){ - ; - }else{ - break label_5; - } - content = JoinSpecification(content); } { if (true) - return content; - } - throw new Error("Missing return statement in function"); - }finally{ - trace_return("TableRef"); - } - } - - final public FromContent JoinedTable() throws ParseException{ - trace_call("JoinedTable"); - try{ - FromContent content; - content = SimpleTableRef(); - label_6: while(true){ - content = JoinSpecification(content); - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case NATURAL: - case INNER: - case RIGHT: - case LEFT: - case FULL: - case JOIN: - ; - break; - default: - jj_la1[29] = jj_gen; - break label_6; - } + return valueExpr; } + }catch(Exception ex){ { if (true) - return content; + throw generateParseException(ex); } - throw new Error("Missing return statement in function"); - }finally{ - trace_return("JoinedTable"); } + throw new Error("Missing return statement in function"); } - final public ADQLJoin JoinSpecification(FromContent leftTable) throws ParseException{ - trace_call("JoinSpecification"); - try{ - boolean natural = false; - JoinType type = JoinType.INNER; - ClauseConstraints condition = new ClauseConstraints("ON"); - ArrayList lstColumns = new ArrayList(); - IdentifierItem id; - FromContent rightTable; - ADQLJoin join; - Token lastPar; - try{ + final public ADQLOperand NumericExpression() throws ParseException{ + Token sign = null; + ADQLOperand leftOp, rightOp = null; + leftOp = NumericTerm(); + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case PLUS: + case MINUS: switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case NATURAL: - jj_consume_token(NATURAL); - natural = true; - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case INNER: - case RIGHT: - case LEFT: - case FULL: - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case INNER: - jj_consume_token(INNER); - break; - case RIGHT: - case LEFT: - case FULL: - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case LEFT: - jj_consume_token(LEFT); - type = JoinType.OUTER_LEFT; - break; - case RIGHT: - jj_consume_token(RIGHT); - type = JoinType.OUTER_RIGHT; - break; - case FULL: - jj_consume_token(FULL); - type = JoinType.OUTER_FULL; - break; - default: - jj_la1[30] = jj_gen; - jj_consume_token(-1); - throw new ParseException(); - } - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case OUTER: - jj_consume_token(OUTER); - break; - default: - jj_la1[31] = jj_gen; - ; - } - break; - default: - jj_la1[32] = jj_gen; - jj_consume_token(-1); - throw new ParseException(); - } - break; - default: - jj_la1[33] = jj_gen; - ; - } - jj_consume_token(JOIN); - rightTable = TableRef(); - join = queryFactory.createJoin(type, leftTable, rightTable); - join.setPosition(new TextPosition(leftTable.getPosition(), rightTable.getPosition())); - { - if (true) - return join; - } + case PLUS: + sign = jj_consume_token(PLUS); break; - case INNER: - case RIGHT: - case LEFT: - case FULL: - case JOIN: - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case INNER: - case RIGHT: - case LEFT: - case FULL: - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case INNER: - jj_consume_token(INNER); - break; - case RIGHT: - case LEFT: - case FULL: - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case LEFT: - jj_consume_token(LEFT); - type = JoinType.OUTER_LEFT; - break; - case RIGHT: - jj_consume_token(RIGHT); - type = JoinType.OUTER_RIGHT; - break; - case FULL: - jj_consume_token(FULL); - type = JoinType.OUTER_FULL; - break; - default: - jj_la1[34] = jj_gen; - jj_consume_token(-1); - throw new ParseException(); - } - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case OUTER: - jj_consume_token(OUTER); - break; - default: - jj_la1[35] = jj_gen; - ; - } - break; - default: - jj_la1[36] = jj_gen; - jj_consume_token(-1); - throw new ParseException(); - } - break; - default: - jj_la1[37] = jj_gen; - ; - } - jj_consume_token(JOIN); - rightTable = TableRef(); - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case ON: - jj_consume_token(ON); - ConditionsList(condition); - join = queryFactory.createJoin(type, leftTable, rightTable, condition); - join.setPosition(new TextPosition(leftTable.getPosition(), condition.getPosition())); - { - if (true) - return join; - } - break; - case USING: - jj_consume_token(USING); - jj_consume_token(LEFT_PAR); - id = Identifier(); - lstColumns.add(queryFactory.createColumn(id)); - label_7: while(true){ - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case COMMA: - ; - break; - default: - jj_la1[38] = jj_gen; - break label_7; - } - jj_consume_token(COMMA); - id = Identifier(); - lstColumns.add(queryFactory.createColumn(id)); - } - lastPar = jj_consume_token(RIGHT_PAR); - join = queryFactory.createJoin(type, leftTable, rightTable, lstColumns); - join.setPosition(new TextPosition(leftTable.getPosition().beginLine, leftTable.getPosition().beginColumn, lastPar.endLine, (lastPar.endColumn < 0) ? -1 : (lastPar.endColumn + 1))); - { - if (true) - return join; - } - break; - default: - jj_la1[39] = jj_gen; - jj_consume_token(-1); - throw new ParseException(); - } + case MINUS: + sign = jj_consume_token(MINUS); break; default: - jj_la1[40] = jj_gen; + jj_la1[50] = jj_gen; jj_consume_token(-1); throw new ParseException(); } + rightOp = NumericExpression(); + break; + default: + jj_la1[51] = jj_gen; + ; + } + if (sign == null){ + if (true) + return leftOp; + }else{ + try{ + Operation operation = queryFactory.createOperation(leftOp, OperationType.getOperator(sign.image), rightOp); + operation.setPosition(new TextPosition(leftOp.getPosition(), rightOp.getPosition())); + { + if (true) + return operation; + } }catch(Exception ex){ { if (true) throw generateParseException(ex); } } - throw new Error("Missing return statement in function"); - }finally{ - trace_return("JoinSpecification"); } + throw new Error("Missing return statement in function"); } - /* ****** */ - /* STRING */ - /* ****** */ - final public StringConstant String() throws ParseException{ - trace_call("String"); - try{ - Token t; - String str = ""; - StringConstant cst; - label_8: while(true){ - t = jj_consume_token(STRING_LITERAL); - str += t.image; + final public ADQLOperand NumericTerm() throws ParseException{ + Token sign = null; + ADQLOperand leftOp, rightOp = null; + leftOp = Factor(); + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case ASTERISK: + case DIVIDE: switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case STRING_LITERAL: - ; + case ASTERISK: + sign = jj_consume_token(ASTERISK); + break; + case DIVIDE: + sign = jj_consume_token(DIVIDE); break; default: - jj_la1[41] = jj_gen; - break label_8; + jj_la1[52] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); } - } + rightOp = NumericTerm(); + break; + default: + jj_la1[53] = jj_gen; + ; + } + if (sign == null){ + if (true) + return leftOp; + }else{ try{ - str = (str != null) ? str.substring(1, str.length() - 1) : str; - cst = queryFactory.createStringConstant(str); - cst.setPosition(new TextPosition(t)); + Operation operation = queryFactory.createOperation(leftOp, OperationType.getOperator(sign.image), rightOp); + operation.setPosition(new TextPosition(leftOp.getPosition(), rightOp.getPosition())); { if (true) - return cst; + return operation; } }catch(Exception ex){ { @@ -1558,236 +1846,367 @@ public class ADQLParser implements ADQLParserConstants { throw generateParseException(ex); } } - throw new Error("Missing return statement in function"); - }finally{ - trace_return("String"); } + throw new Error("Missing return statement in function"); } - /* ************* */ - /* NUMERIC TYPES */ - /* ************* */ - final public NumericConstant UnsignedNumeric() throws ParseException{ - trace_call("UnsignedNumeric"); - try{ - Token t; - NumericConstant cst; + final public ADQLOperand Factor() throws ParseException{ + boolean negative = false; + Token minusSign = null; + ADQLOperand op; + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case PLUS: + case MINUS: + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case PLUS: + jj_consume_token(PLUS); + break; + case MINUS: + jj_consume_token(MINUS); + negative = true; + break; + default: + jj_la1[54] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + break; + default: + jj_la1[55] = jj_gen; + ; + } + if (jj_2_10(2)){ + op = NumericFunction(); + }else{ switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case LEFT_PAR: + case AVG: + case MAX: + case MIN: + case SUM: + case COUNT: + case DELIMITED_IDENTIFIER: + case REGULAR_IDENTIFIER: case SCIENTIFIC_NUMBER: - t = jj_consume_token(SCIENTIFIC_NUMBER); - break; case UNSIGNED_FLOAT: - t = jj_consume_token(UNSIGNED_FLOAT); - break; case UNSIGNED_INTEGER: - t = jj_consume_token(UNSIGNED_INTEGER); + op = NumericValueExpressionPrimary(); break; default: - jj_la1[42] = jj_gen; + jj_la1[56] = jj_gen; jj_consume_token(-1); throw new ParseException(); } + } + if (negative){ try{ - cst = queryFactory.createNumericConstant(t.image); - cst.setPosition(new TextPosition(t)); - { - if (true) - return cst; - } + op = queryFactory.createNegativeOperand(op); + NegativeOperand negativeOp = (NegativeOperand)op; + negativeOp.setPosition(new TextPosition(minusSign.beginLine, minusSign.beginColumn, negativeOp.getPosition().endLine, negativeOp.getPosition().endColumn)); }catch(Exception ex){ { if (true) throw generateParseException(ex); } } - throw new Error("Missing return statement in function"); - }finally{ - trace_return("UnsignedNumeric"); } + + { + if (true) + return op; + } + throw new Error("Missing return statement in function"); } - final public NumericConstant UnsignedFloat() throws ParseException{ - trace_call("UnsignedFloat"); - try{ - Token t; - NumericConstant cst; + final public ADQLOperand StringExpression() throws ParseException{ + ADQLOperand leftOp; + ADQLOperand rightOp = null; + leftOp = StringFactor(); + label_9: while(true){ switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case UNSIGNED_INTEGER: - t = jj_consume_token(UNSIGNED_INTEGER); - break; - case UNSIGNED_FLOAT: - t = jj_consume_token(UNSIGNED_FLOAT); + case CONCAT: + ; break; default: - jj_la1[43] = jj_gen; - jj_consume_token(-1); - throw new ParseException(); + jj_la1[57] = jj_gen; + break label_9; } - try{ - cst = queryFactory.createNumericConstant(t.image); - cst.setPosition(new TextPosition(t)); - { - if (true) - return cst; - } - }catch(Exception ex){ - { - if (true) - throw generateParseException(ex); + jj_consume_token(CONCAT); + rightOp = StringFactor(); + if (!(leftOp instanceof Concatenation)){ + try{ + ADQLOperand temp = leftOp; + leftOp = queryFactory.createConcatenation(); + ((Concatenation)leftOp).add(temp); + }catch(Exception ex){ + { + if (true) + throw generateParseException(ex); + } } } - throw new Error("Missing return statement in function"); - }finally{ - trace_return("UnsignedFloat"); + ((Concatenation)leftOp).add(rightOp); + } + if (leftOp instanceof Concatenation){ + Concatenation concat = (Concatenation)leftOp; + concat.setPosition(new TextPosition(concat.get(0).getPosition(), concat.get(concat.size() - 1).getPosition())); + } + { + if (true) + return leftOp; } + throw new Error("Missing return statement in function"); } - final public NumericConstant SignedInteger() throws ParseException{ - trace_call("SignedInteger"); - try{ - Token sign = null, number; - NumericConstant cst; - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case PLUS: - case MINUS: + final public ADQLOperand StringFactor() throws ParseException{ + ADQLOperand op; + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case COORDSYS: + op = ExtractCoordSys(); + break; + default: + jj_la1[58] = jj_gen; + if (jj_2_11(2)){ + op = UserDefinedFunction(); + ((UserDefinedFunction)op).setExpectedType('S'); + }else{ switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case PLUS: - sign = jj_consume_token(PLUS); - break; - case MINUS: - sign = jj_consume_token(MINUS); + case LEFT_PAR: + case STRING_LITERAL: + case DELIMITED_IDENTIFIER: + case REGULAR_IDENTIFIER: + op = StringValueExpressionPrimary(); break; default: - jj_la1[44] = jj_gen; + jj_la1[59] = jj_gen; jj_consume_token(-1); throw new ParseException(); } - break; - default: - jj_la1[45] = jj_gen; - ; - } - number = jj_consume_token(UNSIGNED_INTEGER); - try{ - if (sign == null){ - cst = queryFactory.createNumericConstant(number.image); - cst.setPosition(new TextPosition(number)); - }else{ - cst = queryFactory.createNumericConstant(sign.image + number.image); - cst.setPosition(new TextPosition(sign, number)); - } - { - if (true) - return cst; - } - }catch(Exception ex){ - { - if (true) - throw generateParseException(ex); } + } + { + if (true) + return op; + } + throw new Error("Missing return statement in function"); + } + + final public GeometryValue GeometryExpression() throws ParseException{ + ADQLColumn col = null; + GeometryFunction gf = null; + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case DELIMITED_IDENTIFIER: + case REGULAR_IDENTIFIER: + col = Column(); + break; + case BOX: + case CENTROID: + case CIRCLE: + case POINT: + case POLYGON: + case REGION: + gf = GeometryValueFunction(); + break; + default: + jj_la1[60] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + if (col != null){ + col.setExpectedType('G'); + { + if (true) + return new GeometryValue(col); } - throw new Error("Missing return statement in function"); - }finally{ - trace_return("SignedInteger"); + }else{ + if (true) + return new GeometryValue(gf); } + throw new Error("Missing return statement in function"); } - /* *********** */ - /* EXPRESSIONS */ - /* *********** */ - final public ADQLOperand ValueExpressionPrimary() throws ParseException{ - trace_call("ValueExpressionPrimary"); + /* ********************************** */ + /* BOOLEAN EXPRESSIONS (WHERE clause) */ + /* ********************************** */ + final public ClauseConstraints ConditionsList(ClauseConstraints clause) throws ParseException{ + ADQLConstraint constraint = null; + Token op = null; + boolean notOp = false; try{ - ADQLColumn column; - ADQLOperand op; - Token left, right; - try{ + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case NOT: + op = jj_consume_token(NOT); + notOp = true; + break; + default: + jj_la1[61] = jj_gen; + ; + } + constraint = Constraint(); + if (notOp){ + TextPosition oldPos = constraint.getPosition(); + constraint = queryFactory.createNot(constraint); + ((NotConstraint)constraint).setPosition(new TextPosition(op.beginLine, op.beginColumn, oldPos.endLine, oldPos.endColumn)); + } + notOp = false; + + if (clause instanceof ADQLConstraint) + clause.add(constraint); + else + clause.add(constraint); + label_10: while(true){ switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case SCIENTIFIC_NUMBER: - case UNSIGNED_FLOAT: - case UNSIGNED_INTEGER: - // unsigned_value_specification - op = UnsignedNumeric(); - { - if (true) - return op; - } - break; - case STRING_LITERAL: - op = String(); - { - if (true) - return op; - } - break; - case DELIMITED_IDENTIFIER: - case REGULAR_IDENTIFIER: - column = Column(); - { - if (true) - return column; - } + case AND: + case OR: + ; break; - case AVG: - case MAX: - case MIN: - case SUM: - case COUNT: - op = SqlFunction(); - { - if (true) - return op; - } + default: + jj_la1[62] = jj_gen; + break label_10; + } + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case AND: + op = jj_consume_token(AND); break; - case LEFT_PAR: - left = jj_consume_token(LEFT_PAR); - op = ValueExpression(); - right = jj_consume_token(RIGHT_PAR); - WrappedOperand wop = queryFactory.createWrappedOperand(op); - wop.setPosition(new TextPosition(left, right)); - { - if (true) - return wop; - } + case OR: + op = jj_consume_token(OR); break; default: - jj_la1[46] = jj_gen; + jj_la1[63] = jj_gen; jj_consume_token(-1); throw new ParseException(); } - }catch(Exception ex){ - { - if (true) - throw generateParseException(ex); + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case NOT: + jj_consume_token(NOT); + notOp = true; + break; + default: + jj_la1[64] = jj_gen; + ; } - } - throw new Error("Missing return statement in function"); - }finally{ - trace_return("ValueExpressionPrimary"); - } - } - - final public ADQLOperand ValueExpression() throws ParseException{ - trace_call("ValueExpression"); + constraint = Constraint(); + if (notOp){ + TextPosition oldPos = constraint.getPosition(); + constraint = queryFactory.createNot(constraint); + ((NotConstraint)constraint).setPosition(new TextPosition(op.beginLine, op.beginColumn, oldPos.endLine, oldPos.endColumn)); + } + notOp = false; + + if (clause instanceof ADQLConstraint) + clause.add(op.image, constraint); + else + clause.add(op.image, constraint); + } + }catch(Exception ex){ + { + if (true) + throw generateParseException(ex); + } + } + if (!clause.isEmpty()){ + TextPosition start = clause.get(0).getPosition(); + TextPosition end = clause.get(clause.size() - 1).getPosition(); + clause.setPosition(new TextPosition(start, end)); + } + { + if (true) + return clause; + } + throw new Error("Missing return statement in function"); + } + + final public ADQLConstraint Constraint() throws ParseException{ + ADQLConstraint constraint = null; + Token start, end; + if (jj_2_12(2147483647)){ + constraint = Predicate(); + }else{ + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case LEFT_PAR: + start = jj_consume_token(LEFT_PAR); + try{ + constraint = queryFactory.createGroupOfConstraints(); + }catch(Exception ex){ + { + if (true) + throw generateParseException(ex); + } + } + ConditionsList((ConstraintsGroup)constraint); + end = jj_consume_token(RIGHT_PAR); + ((ConstraintsGroup)constraint).setPosition(new TextPosition(start, end)); + break; + default: + jj_la1[65] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } + { + if (true) + return constraint; + } + throw new Error("Missing return statement in function"); + } + + final public ADQLConstraint Predicate() throws ParseException{ + ADQLQuery q = null; + ADQLColumn column = null; + ADQLOperand strExpr1 = null, strExpr2 = null; + ADQLOperand op; + Token start, notToken = null, end; + ADQLConstraint constraint = null; try{ - ADQLOperand valueExpr = null; switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case BOX: - case CENTROID: - case CIRCLE: - case POINT: - case POLYGON: - case REGION: - valueExpr = GeometryValueFunction(); + case EXISTS: + start = jj_consume_token(EXISTS); + q = SubQueryExpression(); + Exists e = queryFactory.createExists(q); + e.setPosition(new TextPosition(start.beginLine, start.beginColumn, q.getPosition().endLine, q.getPosition().endColumn)); + { + if (true) + return e; + } break; default: - jj_la1[47] = jj_gen; - if (jj_2_4(2147483647)){ - valueExpr = NumericExpression(); - }else if (jj_2_5(2147483647)){ - valueExpr = StringExpression(); - }else if (jj_2_6(2147483647)){ - valueExpr = StringExpression(); + jj_la1[70] = jj_gen; + if (jj_2_14(2147483647)){ + column = Column(); + jj_consume_token(IS); + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case NOT: + notToken = jj_consume_token(NOT); + break; + default: + jj_la1[66] = jj_gen; + ; + } + end = jj_consume_token(NULL); + IsNull in = queryFactory.createIsNull((notToken != null), column); + in.setPosition(new TextPosition(column.getPosition().beginLine, column.getPosition().beginColumn, end.endLine, (end.endColumn < 0) ? -1 : (end.endColumn + 1))); + { + if (true) + return in; + } + }else if (jj_2_15(2147483647)){ + strExpr1 = StringExpression(); + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case NOT: + notToken = jj_consume_token(NOT); + break; + default: + jj_la1[67] = jj_gen; + ; + } + jj_consume_token(LIKE); + strExpr2 = StringExpression(); + Comparison comp = queryFactory.createComparison(strExpr1, (notToken == null) ? ComparisonOperator.LIKE : ComparisonOperator.NOTLIKE, strExpr2); + comp.setPosition(new TextPosition(strExpr1.getPosition(), strExpr2.getPosition())); + { + if (true) + return comp; + } }else{ switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ case LEFT_PAR: @@ -1798,11 +2217,18 @@ public class ADQLParser implements ADQLParserConstants { case MIN: case SUM: case COUNT: + case BOX: + case CENTROID: + case CIRCLE: + case POINT: + case POLYGON: + case REGION: case CONTAINS: case INTERSECTS: case AREA: case COORD1: case COORD2: + case COORDSYS: case DISTANCE: case ABS: case CEILING: @@ -1833,1742 +2259,1099 @@ public class ADQLParser implements ADQLParserConstants { case SCIENTIFIC_NUMBER: case UNSIGNED_FLOAT: case UNSIGNED_INTEGER: - valueExpr = NumericExpression(); + op = ValueExpression(); + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case EQUAL: + case NOT_EQUAL: + case LESS_THAN: + case LESS_EQUAL_THAN: + case GREATER_THAN: + case GREATER_EQUAL_THAN: + constraint = ComparisonEnd(op); + break; + default: + jj_la1[68] = jj_gen; + if (jj_2_13(2)){ + constraint = BetweenEnd(op); + }else{ + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case NOT: + case IN: + constraint = InEnd(op); + break; + default: + jj_la1[69] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } + } break; default: - jj_la1[48] = jj_gen; + jj_la1[71] = jj_gen; jj_consume_token(-1); throw new ParseException(); } } } + }catch(Exception ex){ { if (true) - return valueExpr; + throw generateParseException(ex); } - throw new Error("Missing return statement in function"); - }finally{ - trace_return("ValueExpression"); } + { + if (true) + return constraint; + } + throw new Error("Missing return statement in function"); } - final public ADQLOperand NumericExpression() throws ParseException{ - trace_call("NumericExpression"); + final public Comparison ComparisonEnd(ADQLOperand leftOp) throws ParseException{ + Token comp; + ADQLOperand rightOp; + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case EQUAL: + comp = jj_consume_token(EQUAL); + break; + case NOT_EQUAL: + comp = jj_consume_token(NOT_EQUAL); + break; + case LESS_THAN: + comp = jj_consume_token(LESS_THAN); + break; + case LESS_EQUAL_THAN: + comp = jj_consume_token(LESS_EQUAL_THAN); + break; + case GREATER_THAN: + comp = jj_consume_token(GREATER_THAN); + break; + case GREATER_EQUAL_THAN: + comp = jj_consume_token(GREATER_EQUAL_THAN); + break; + default: + jj_la1[72] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + rightOp = ValueExpression(); + try{ + Comparison comparison = queryFactory.createComparison(leftOp, ComparisonOperator.getOperator(comp.image), rightOp); + comparison.setPosition(new TextPosition(leftOp.getPosition(), rightOp.getPosition())); + { + if (true) + return comparison; + } + }catch(Exception ex){ + { + if (true) + throw generateParseException(ex); + } + } + throw new Error("Missing return statement in function"); + } + + final public Between BetweenEnd(ADQLOperand leftOp) throws ParseException{ + Token start, notToken = null; + ADQLOperand min, max; + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case NOT: + notToken = jj_consume_token(NOT); + break; + default: + jj_la1[73] = jj_gen; + ; + } + start = jj_consume_token(BETWEEN); + min = ValueExpression(); + jj_consume_token(AND); + max = ValueExpression(); try{ - Token sign = null; - ADQLOperand leftOp, rightOp = null; - leftOp = NumericTerm(); + Between bet = queryFactory.createBetween((notToken != null), leftOp, min, max); + if (notToken != null) + start = notToken; + bet.setPosition(new TextPosition(start.beginLine, start.beginColumn, max.getPosition().endLine, max.getPosition().endColumn)); + { + if (true) + return bet; + } + }catch(Exception ex){ + { + if (true) + throw generateParseException(ex); + } + } + throw new Error("Missing return statement in function"); + } + + final public In InEnd(ADQLOperand leftOp) throws ParseException{ + Token not = null, start; + ADQLQuery q = null; + ADQLOperand item; + Vector items = new Vector(); + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case NOT: + not = jj_consume_token(NOT); + break; + default: + jj_la1[74] = jj_gen; + ; + } + start = jj_consume_token(IN); + if (jj_2_16(2)){ + q = SubQueryExpression(); + }else{ switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case PLUS: - case MINUS: - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case PLUS: - sign = jj_consume_token(PLUS); - break; - case MINUS: - sign = jj_consume_token(MINUS); - break; - default: - jj_la1[49] = jj_gen; - jj_consume_token(-1); - throw new ParseException(); + case LEFT_PAR: + jj_consume_token(LEFT_PAR); + item = ValueExpression(); + items.add(item); + label_11: while(true){ + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case COMMA: + ; + break; + default: + jj_la1[75] = jj_gen; + break label_11; + } + jj_consume_token(COMMA); + item = ValueExpression(); + items.add(item); } - rightOp = NumericExpression(); + jj_consume_token(RIGHT_PAR); break; default: - jj_la1[50] = jj_gen; - ; + jj_la1[76] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); } - if (sign == null){ - if (true) - return leftOp; + } + try{ + In in; + start = (not != null) ? not : start; + if (q != null){ + in = queryFactory.createIn(leftOp, q, not != null); + in.setPosition(new TextPosition(start.beginLine, start.beginColumn, q.getPosition().endLine, q.getPosition().endColumn)); }else{ - try{ - Operation operation = queryFactory.createOperation(leftOp, OperationType.getOperator(sign.image), rightOp); - operation.setPosition(new TextPosition(leftOp.getPosition(), rightOp.getPosition())); - { - if (true) - return operation; - } - }catch(Exception ex){ - { - if (true) - throw generateParseException(ex); - } - } + ADQLOperand[] list = new ADQLOperand[items.size()]; + int i = 0; + for(ADQLOperand op : items) + list[i++] = op; + in = queryFactory.createIn(leftOp, list, not != null); + in.setPosition(new TextPosition(start.beginLine, start.beginColumn, list[list.length - 1].getPosition().endLine, list[list.length - 1].getPosition().endColumn)); + } + { + if (true) + return in; + } + }catch(Exception ex){ + { + if (true) + throw generateParseException(ex); } - throw new Error("Missing return statement in function"); - }finally{ - trace_return("NumericExpression"); } + throw new Error("Missing return statement in function"); } - final public ADQLOperand NumericTerm() throws ParseException{ - trace_call("NumericTerm"); + /* ************* */ + /* SQL FUNCTIONS */ + /* ************* */ + final public SQLFunction SqlFunction() throws ParseException{ + Token fct, all = null, distinct = null, end; + ADQLOperand op = null; + SQLFunction funct = null; try{ - Token sign = null; - ADQLOperand leftOp, rightOp = null; - leftOp = Factor(); switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case ASTERISK: - case DIVIDE: + case COUNT: + fct = jj_consume_token(COUNT); + jj_consume_token(LEFT_PAR); + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case QUANTIFIER: + distinct = jj_consume_token(QUANTIFIER); + break; + default: + jj_la1[77] = jj_gen; + ; + } switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ case ASTERISK: - sign = jj_consume_token(ASTERISK); + all = jj_consume_token(ASTERISK); break; - case DIVIDE: - sign = jj_consume_token(DIVIDE); + case LEFT_PAR: + case PLUS: + case MINUS: + case AVG: + case MAX: + case MIN: + case SUM: + case COUNT: + case BOX: + case CENTROID: + case CIRCLE: + case POINT: + case POLYGON: + case REGION: + case CONTAINS: + case INTERSECTS: + case AREA: + case COORD1: + case COORD2: + case COORDSYS: + case DISTANCE: + case ABS: + case CEILING: + case DEGREES: + case EXP: + case FLOOR: + case LOG: + case LOG10: + case MOD: + case PI: + case POWER: + case RADIANS: + case RAND: + case ROUND: + case SQRT: + case TRUNCATE: + case ACOS: + case ASIN: + case ATAN: + case ATAN2: + case COS: + case COT: + case SIN: + case TAN: + case STRING_LITERAL: + case DELIMITED_IDENTIFIER: + case REGULAR_IDENTIFIER: + case SCIENTIFIC_NUMBER: + case UNSIGNED_FLOAT: + case UNSIGNED_INTEGER: + op = ValueExpression(); break; default: - jj_la1[51] = jj_gen; + jj_la1[78] = jj_gen; jj_consume_token(-1); throw new ParseException(); } - rightOp = NumericTerm(); + end = jj_consume_token(RIGHT_PAR); + funct = queryFactory.createSQLFunction((all != null) ? SQLFunctionType.COUNT_ALL : SQLFunctionType.COUNT, op, distinct != null && distinct.image.equalsIgnoreCase("distinct")); + funct.setPosition(new TextPosition(fct, end)); break; - default: - jj_la1[52] = jj_gen; - ; - } - if (sign == null){ - if (true) - return leftOp; - }else{ - try{ - Operation operation = queryFactory.createOperation(leftOp, OperationType.getOperator(sign.image), rightOp); - operation.setPosition(new TextPosition(leftOp.getPosition(), rightOp.getPosition())); - { - if (true) - return operation; - } - }catch(Exception ex){ - { - if (true) - throw generateParseException(ex); - } - } - } - throw new Error("Missing return statement in function"); - }finally{ - trace_return("NumericTerm"); - } - } - - final public ADQLOperand Factor() throws ParseException{ - trace_call("Factor"); - try{ - boolean negative = false; - Token minusSign = null; - ADQLOperand op; - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case PLUS: - case MINUS: + case AVG: + case MAX: + case MIN: + case SUM: switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case PLUS: - jj_consume_token(PLUS); + case AVG: + fct = jj_consume_token(AVG); break; - case MINUS: - minusSign = jj_consume_token(MINUS); - negative = true; + case MAX: + fct = jj_consume_token(MAX); + break; + case MIN: + fct = jj_consume_token(MIN); + break; + case SUM: + fct = jj_consume_token(SUM); break; default: - jj_la1[53] = jj_gen; + jj_la1[79] = jj_gen; jj_consume_token(-1); throw new ParseException(); } + jj_consume_token(LEFT_PAR); + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case QUANTIFIER: + distinct = jj_consume_token(QUANTIFIER); + break; + default: + jj_la1[80] = jj_gen; + ; + } + op = ValueExpression(); + end = jj_consume_token(RIGHT_PAR); + funct = queryFactory.createSQLFunction(SQLFunctionType.valueOf(fct.image.toUpperCase()), op, distinct != null && distinct.image.equalsIgnoreCase("distinct")); + funct.setPosition(new TextPosition(fct, end)); break; default: - jj_la1[54] = jj_gen; - ; - } - if (jj_2_7(2)){ - op = NumericFunction(); - }else{ - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case LEFT_PAR: - case AVG: - case MAX: - case MIN: - case SUM: - case COUNT: - case STRING_LITERAL: - case DELIMITED_IDENTIFIER: - case REGULAR_IDENTIFIER: - case SCIENTIFIC_NUMBER: - case UNSIGNED_FLOAT: - case UNSIGNED_INTEGER: - op = ValueExpressionPrimary(); - break; - default: - jj_la1[55] = jj_gen; - jj_consume_token(-1); - throw new ParseException(); - } - } - if (negative){ - try{ - op = queryFactory.createNegativeOperand(op); - NegativeOperand negativeOp = (NegativeOperand)op; - negativeOp.setPosition(new TextPosition(minusSign.beginLine, minusSign.beginColumn, negativeOp.getPosition().endLine, negativeOp.getPosition().endColumn)); - }catch(Exception ex){ - { - if (true) - throw generateParseException(ex); - } - } + jj_la1[81] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); } - + }catch(Exception ex){ { if (true) - return op; + throw generateParseException(ex); } - throw new Error("Missing return statement in function"); - }finally{ - trace_return("Factor"); } + { + if (true) + return funct; + } + throw new Error("Missing return statement in function"); } - final public ADQLOperand StringExpression() throws ParseException{ - trace_call("StringExpression"); + /* ************** */ + /* ADQL FUNCTIONS */ + /* ************** */ + final public ADQLOperand[] Coordinates() throws ParseException{ + ADQLOperand[] ops = new ADQLOperand[2]; + ops[0] = NumericExpression(); + jj_consume_token(COMMA); + ops[1] = NumericExpression(); + { + if (true) + return ops; + } + throw new Error("Missing return statement in function"); + } + + final public GeometryFunction GeometryFunction() throws ParseException{ + Token fct = null, end; + GeometryValue gvf1, gvf2; + GeometryValue gvp1, gvp2; + GeometryFunction gf = null; + PointFunction p1 = null, p2 = null; + ADQLColumn col1 = null, col2 = null; try{ - ADQLOperand leftOp; - ADQLOperand rightOp = null; - leftOp = StringFactor(); - label_9: while(true){ - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case CONCAT: - ; - break; - default: - jj_la1[56] = jj_gen; - break label_9; - } - jj_consume_token(CONCAT); - rightOp = StringFactor(); - if (!(leftOp instanceof Concatenation)){ - try{ - ADQLOperand temp = leftOp; - leftOp = queryFactory.createConcatenation(); - ((Concatenation)leftOp).add(temp); - }catch(Exception ex){ - { - if (true) - throw generateParseException(ex); - } - } - } - ((Concatenation)leftOp).add(rightOp); - } - if (leftOp instanceof Concatenation){ - Concatenation concat = (Concatenation)leftOp; - concat.setPosition(new TextPosition(concat.get(0).getPosition(), concat.get(concat.size() - 1).getPosition())); - } - { - if (true) - return leftOp; - } - throw new Error("Missing return statement in function"); - }finally{ - trace_return("StringExpression"); - } - } - - final public ADQLOperand StringFactor() throws ParseException{ - trace_call("StringFactor"); - try{ - ADQLOperand op; switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case COORDSYS: - op = ExtractCoordSys(); - break; - default: - jj_la1[57] = jj_gen; - if (jj_2_8(2)){ - op = UserDefinedFunction(); - }else{ - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case LEFT_PAR: - case AVG: - case MAX: - case MIN: - case SUM: - case COUNT: - case STRING_LITERAL: - case DELIMITED_IDENTIFIER: - case REGULAR_IDENTIFIER: - case SCIENTIFIC_NUMBER: - case UNSIGNED_FLOAT: - case UNSIGNED_INTEGER: - op = ValueExpressionPrimary(); - break; - default: - jj_la1[58] = jj_gen; - jj_consume_token(-1); - throw new ParseException(); - } + case CONTAINS: + case INTERSECTS: + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case CONTAINS: + fct = jj_consume_token(CONTAINS); + break; + case INTERSECTS: + fct = jj_consume_token(INTERSECTS); + break; + default: + jj_la1[82] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); } - } - { - if (true) - return op; - } - throw new Error("Missing return statement in function"); - }finally{ - trace_return("StringFactor"); - } - } - - final public GeometryValue GeometryExpression() throws ParseException{ - trace_call("GeometryExpression"); - try{ - ADQLColumn col = null; - GeometryFunction gf = null; - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case DELIMITED_IDENTIFIER: - case REGULAR_IDENTIFIER: - col = Column(); + jj_consume_token(LEFT_PAR); + gvf1 = GeometryExpression(); + jj_consume_token(COMMA); + gvf2 = GeometryExpression(); + end = jj_consume_token(RIGHT_PAR); + if (fct.image.equalsIgnoreCase("contains")) + gf = queryFactory.createContains(gvf1, gvf2); + else + gf = queryFactory.createIntersects(gvf1, gvf2); break; - case BOX: - case CENTROID: - case CIRCLE: - case POINT: - case POLYGON: - case REGION: - gf = GeometryValueFunction(); + case AREA: + fct = jj_consume_token(AREA); + jj_consume_token(LEFT_PAR); + gvf1 = GeometryExpression(); + end = jj_consume_token(RIGHT_PAR); + gf = queryFactory.createArea(gvf1); break; - default: - jj_la1[59] = jj_gen; - jj_consume_token(-1); - throw new ParseException(); - } - if (col != null){ - if (true) - return new GeometryValue(col); - }else{ - if (true) - return new GeometryValue(gf); - } - throw new Error("Missing return statement in function"); - }finally{ - trace_return("GeometryExpression"); - } - } - - /* ********************************** */ - /* BOOLEAN EXPRESSIONS (WHERE clause) */ - /* ********************************** */ - final public ClauseConstraints ConditionsList(ClauseConstraints clause) throws ParseException{ - trace_call("ConditionsList"); - try{ - ADQLConstraint constraint = null; - Token op = null; - boolean notOp = false; - try{ - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case NOT: - op = jj_consume_token(NOT); - notOp = true; - break; - default: - jj_la1[60] = jj_gen; - ; - } - constraint = Constraint(); - if (notOp){ - TextPosition oldPos = constraint.getPosition(); - constraint = queryFactory.createNot(constraint); - ((NotConstraint)constraint).setPosition(new TextPosition(op.beginLine, op.beginColumn, oldPos.endLine, oldPos.endColumn)); - } - notOp = false; - - if (clause instanceof ADQLConstraint) - clause.add(constraint); - else - clause.add(constraint); - label_10: while(true){ + case COORD1: + fct = jj_consume_token(COORD1); + jj_consume_token(LEFT_PAR); switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case AND: - case OR: - ; + case POINT: + p1 = Point(); + gf = queryFactory.createCoord1(p1); + break; + case DELIMITED_IDENTIFIER: + case REGULAR_IDENTIFIER: + col1 = Column(); + col1.setExpectedType('G'); + gf = queryFactory.createCoord1(col1); break; default: - jj_la1[61] = jj_gen; - break label_10; + jj_la1[83] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); } + end = jj_consume_token(RIGHT_PAR); + break; + case COORD2: + fct = jj_consume_token(COORD2); + jj_consume_token(LEFT_PAR); switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case AND: - op = jj_consume_token(AND); + case POINT: + p1 = Point(); + gf = queryFactory.createCoord2(p1); break; - case OR: - op = jj_consume_token(OR); + case DELIMITED_IDENTIFIER: + case REGULAR_IDENTIFIER: + col1 = Column(); + col1.setExpectedType('G'); + gf = queryFactory.createCoord2(col1); break; default: - jj_la1[62] = jj_gen; + jj_la1[84] = jj_gen; jj_consume_token(-1); throw new ParseException(); } + end = jj_consume_token(RIGHT_PAR); + break; + case DISTANCE: + fct = jj_consume_token(DISTANCE); + jj_consume_token(LEFT_PAR); switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case NOT: - jj_consume_token(NOT); - notOp = true; + case POINT: + p1 = Point(); + break; + case DELIMITED_IDENTIFIER: + case REGULAR_IDENTIFIER: + col1 = Column(); break; default: - jj_la1[63] = jj_gen; - ; + jj_la1[85] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); } - constraint = Constraint(); - if (notOp){ - TextPosition oldPos = constraint.getPosition(); - constraint = queryFactory.createNot(constraint); - ((NotConstraint)constraint).setPosition(new TextPosition(op.beginLine, op.beginColumn, oldPos.endLine, oldPos.endColumn)); + if (p1 != null) + gvp1 = new GeometryValue(p1); + else{ + col1.setExpectedType('G'); + gvp1 = new GeometryValue(col1); } - notOp = false; - - if (clause instanceof ADQLConstraint) - clause.add(op.image, constraint); - else - clause.add(op.image, constraint); - } - }catch(Exception ex){ - { - if (true) - throw generateParseException(ex); - } - } - if (!clause.isEmpty()){ - TextPosition start = clause.get(0).getPosition(); - TextPosition end = clause.get(clause.size() - 1).getPosition(); - clause.setPosition(new TextPosition(start, end)); + jj_consume_token(COMMA); + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case POINT: + p2 = Point(); + break; + case DELIMITED_IDENTIFIER: + case REGULAR_IDENTIFIER: + col2 = Column(); + break; + default: + jj_la1[86] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + if (p2 != null) + gvp2 = new GeometryValue(p2); + else{ + col2.setExpectedType('G'); + gvp2 = new GeometryValue(col2); + } + end = jj_consume_token(RIGHT_PAR); + gf = queryFactory.createDistance(gvp1, gvp2); + break; + default: + jj_la1[87] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); } + }catch(Exception ex){ { if (true) - return clause; + throw generateParseException(ex); } - throw new Error("Missing return statement in function"); - }finally{ - trace_return("ConditionsList"); } + gf.setPosition(new TextPosition(fct, end)); + { + if (true) + return gf; + } + throw new Error("Missing return statement in function"); } - final public ADQLConstraint Constraint() throws ParseException{ - trace_call("Constraint"); - try{ - ADQLConstraint constraint = null; - Token start, end; - if (jj_2_9(2147483647)){ - constraint = Predicate(); - }else{ - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case LEFT_PAR: - start = jj_consume_token(LEFT_PAR); - try{ - constraint = queryFactory.createGroupOfConstraints(); - }catch(Exception ex){ - { - if (true) - throw generateParseException(ex); - } - } - ConditionsList((ConstraintsGroup)constraint); - end = jj_consume_token(RIGHT_PAR); - ((ConstraintsGroup)constraint).setPosition(new TextPosition(start, end)); - break; - default: - jj_la1[64] = jj_gen; - jj_consume_token(-1); - throw new ParseException(); - } - } - { - if (true) - return constraint; - } - throw new Error("Missing return statement in function"); - }finally{ - trace_return("Constraint"); + final public ADQLOperand CoordinateSystem() throws ParseException{ + ADQLOperand coordSys = null; + coordSys = StringExpression(); + { + if (true) + return coordSys; } + throw new Error("Missing return statement in function"); } - final public ADQLConstraint Predicate() throws ParseException{ - trace_call("Predicate"); + final public GeometryFunction GeometryValueFunction() throws ParseException{ + Token fct = null, end = null; + ADQLOperand coordSys; + ADQLOperand width, height; + ADQLOperand[] coords, tmp; + Vector vCoords; + ADQLOperand op = null; + GeometryValue gvf = null; + GeometryFunction gf = null; try{ - ADQLQuery q = null; - ADQLColumn column = null; - ADQLOperand strExpr1 = null, strExpr2 = null; - ADQLOperand op; - Token start, notToken = null, end; - ADQLConstraint constraint = null; - try{ - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case EXISTS: - start = jj_consume_token(EXISTS); - q = SubQueryExpression(); - Exists e = queryFactory.createExists(q); - e.setPosition(new TextPosition(start.beginLine, start.beginColumn, q.getPosition().endLine, q.getPosition().endColumn)); - { - if (true) - return e; + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case BOX: + fct = jj_consume_token(BOX); + jj_consume_token(LEFT_PAR); + coordSys = CoordinateSystem(); + jj_consume_token(COMMA); + coords = Coordinates(); + jj_consume_token(COMMA); + width = NumericExpression(); + jj_consume_token(COMMA); + height = NumericExpression(); + end = jj_consume_token(RIGHT_PAR); + gf = queryFactory.createBox(coordSys, coords[0], coords[1], width, height); + break; + case CENTROID: + fct = jj_consume_token(CENTROID); + jj_consume_token(LEFT_PAR); + gvf = GeometryExpression(); + end = jj_consume_token(RIGHT_PAR); + gf = queryFactory.createCentroid(gvf); + break; + case CIRCLE: + fct = jj_consume_token(CIRCLE); + jj_consume_token(LEFT_PAR); + coordSys = CoordinateSystem(); + jj_consume_token(COMMA); + coords = Coordinates(); + jj_consume_token(COMMA); + width = NumericExpression(); + end = jj_consume_token(RIGHT_PAR); + gf = queryFactory.createCircle(coordSys, coords[0], coords[1], width); + break; + case POINT: + gf = Point(); + break; + case POLYGON: + fct = jj_consume_token(POLYGON); + jj_consume_token(LEFT_PAR); + coordSys = CoordinateSystem(); + vCoords = new Vector(); + jj_consume_token(COMMA); + tmp = Coordinates(); + vCoords.add(tmp[0]); + vCoords.add(tmp[1]); + jj_consume_token(COMMA); + tmp = Coordinates(); + vCoords.add(tmp[0]); + vCoords.add(tmp[1]); + jj_consume_token(COMMA); + tmp = Coordinates(); + vCoords.add(tmp[0]); + vCoords.add(tmp[1]); + label_12: while(true){ + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case COMMA: + ; + break; + default: + jj_la1[88] = jj_gen; + break label_12; } - break; - default: - jj_la1[69] = jj_gen; - if (jj_2_11(2147483647)){ - column = Column(); - jj_consume_token(IS); - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case NOT: - notToken = jj_consume_token(NOT); - break; - default: - jj_la1[65] = jj_gen; - ; - } - end = jj_consume_token(NULL); - IsNull in = queryFactory.createIsNull((notToken != null), column); - in.setPosition(new TextPosition(column.getPosition().beginLine, column.getPosition().beginColumn, end.endLine, (end.endColumn < 0) ? -1 : (end.endColumn + 1))); - { - if (true) - return in; - } - }else if (jj_2_12(2147483647)){ - strExpr1 = StringExpression(); - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case NOT: - notToken = jj_consume_token(NOT); - break; - default: - jj_la1[66] = jj_gen; - ; - } - jj_consume_token(LIKE); - strExpr2 = StringExpression(); - Comparison comp = queryFactory.createComparison(strExpr1, (notToken == null) ? ComparisonOperator.LIKE : ComparisonOperator.NOTLIKE, strExpr2); - comp.setPosition(new TextPosition(strExpr1.getPosition(), strExpr2.getPosition())); - { - if (true) - return comp; - } - }else{ - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case LEFT_PAR: - case PLUS: - case MINUS: - case AVG: - case MAX: - case MIN: - case SUM: - case COUNT: - case BOX: - case CENTROID: - case CIRCLE: - case POINT: - case POLYGON: - case REGION: - case CONTAINS: - case INTERSECTS: - case AREA: - case COORD1: - case COORD2: - case COORDSYS: - case DISTANCE: - case ABS: - case CEILING: - case DEGREES: - case EXP: - case FLOOR: - case LOG: - case LOG10: - case MOD: - case PI: - case POWER: - case RADIANS: - case RAND: - case ROUND: - case SQRT: - case TRUNCATE: - case ACOS: - case ASIN: - case ATAN: - case ATAN2: - case COS: - case COT: - case SIN: - case TAN: - case STRING_LITERAL: - case DELIMITED_IDENTIFIER: - case REGULAR_IDENTIFIER: - case SCIENTIFIC_NUMBER: - case UNSIGNED_FLOAT: - case UNSIGNED_INTEGER: - op = ValueExpression(); - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case EQUAL: - case NOT_EQUAL: - case LESS_THAN: - case LESS_EQUAL_THAN: - case GREATER_THAN: - case GREATER_EQUAL_THAN: - constraint = ComparisonEnd(op); - break; - default: - jj_la1[67] = jj_gen; - if (jj_2_10(2)){ - constraint = BetweenEnd(op); - }else{ - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case NOT: - case IN: - constraint = InEnd(op); - break; - default: - jj_la1[68] = jj_gen; - jj_consume_token(-1); - throw new ParseException(); - } - } - } - break; - default: - jj_la1[70] = jj_gen; - jj_consume_token(-1); - throw new ParseException(); - } - } - } - }catch(Exception ex){ - { - if (true) - throw generateParseException(ex); - } - } - { - if (true) - return constraint; - } - throw new Error("Missing return statement in function"); - }finally{ - trace_return("Predicate"); - } - } - - final public Comparison ComparisonEnd(ADQLOperand leftOp) throws ParseException{ - trace_call("ComparisonEnd"); - try{ - Token comp; - ADQLOperand rightOp; - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case EQUAL: - comp = jj_consume_token(EQUAL); - break; - case NOT_EQUAL: - comp = jj_consume_token(NOT_EQUAL); - break; - case LESS_THAN: - comp = jj_consume_token(LESS_THAN); - break; - case LESS_EQUAL_THAN: - comp = jj_consume_token(LESS_EQUAL_THAN); - break; - case GREATER_THAN: - comp = jj_consume_token(GREATER_THAN); - break; - case GREATER_EQUAL_THAN: - comp = jj_consume_token(GREATER_EQUAL_THAN); - break; - default: - jj_la1[71] = jj_gen; - jj_consume_token(-1); - throw new ParseException(); - } - rightOp = ValueExpression(); - try{ - Comparison comparison = queryFactory.createComparison(leftOp, ComparisonOperator.getOperator(comp.image), rightOp); - comparison.setPosition(new TextPosition(leftOp.getPosition(), rightOp.getPosition())); - { - if (true) - return comparison; - } - }catch(Exception ex){ - { - if (true) - throw generateParseException(ex); - } - } - throw new Error("Missing return statement in function"); - }finally{ - trace_return("ComparisonEnd"); - } - } - - final public Between BetweenEnd(ADQLOperand leftOp) throws ParseException{ - trace_call("BetweenEnd"); - try{ - Token start, notToken = null; - ADQLOperand min, max; - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case NOT: - notToken = jj_consume_token(NOT); - break; - default: - jj_la1[72] = jj_gen; - ; - } - start = jj_consume_token(BETWEEN); - min = ValueExpression(); - jj_consume_token(AND); - max = ValueExpression(); - try{ - Between bet = queryFactory.createBetween((notToken != null), leftOp, min, max); - if (notToken != null) - start = notToken; - bet.setPosition(new TextPosition(start.beginLine, start.beginColumn, max.getPosition().endLine, max.getPosition().endColumn)); - { - if (true) - return bet; - } - }catch(Exception ex){ - { - if (true) - throw generateParseException(ex); - } - } - throw new Error("Missing return statement in function"); - }finally{ - trace_return("BetweenEnd"); - } - } - - final public In InEnd(ADQLOperand leftOp) throws ParseException{ - trace_call("InEnd"); - try{ - Token not = null, start; - ADQLQuery q = null; - ADQLOperand item; - Vector items = new Vector(); - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case NOT: - not = jj_consume_token(NOT); - break; - default: - jj_la1[73] = jj_gen; - ; - } - start = jj_consume_token(IN); - if (jj_2_13(2)){ - q = SubQueryExpression(); - }else{ - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case LEFT_PAR: - jj_consume_token(LEFT_PAR); - item = ValueExpression(); - items.add(item); - label_11: while(true){ - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case COMMA: - ; - break; - default: - jj_la1[74] = jj_gen; - break label_11; - } - jj_consume_token(COMMA); - item = ValueExpression(); - items.add(item); - } - jj_consume_token(RIGHT_PAR); - break; - default: - jj_la1[75] = jj_gen; - jj_consume_token(-1); - throw new ParseException(); - } - } - try{ - In in; - start = (not != null) ? not : start; - if (q != null){ - in = queryFactory.createIn(leftOp, q, not != null); - in.setPosition(new TextPosition(start.beginLine, start.beginColumn, q.getPosition().endLine, q.getPosition().endColumn)); - }else{ - ADQLOperand[] list = new ADQLOperand[items.size()]; - int i = 0; - for(ADQLOperand op : items) - list[i++] = op; - in = queryFactory.createIn(leftOp, list, not != null); - in.setPosition(new TextPosition(start.beginLine, start.beginColumn, list[list.length - 1].getPosition().endLine, list[list.length - 1].getPosition().endColumn)); - } - { - if (true) - return in; - } - }catch(Exception ex){ - { - if (true) - throw generateParseException(ex); - } - } - throw new Error("Missing return statement in function"); - }finally{ - trace_return("InEnd"); - } - } - - /* ************* */ - /* SQL FUNCTIONS */ - /* ************* */ - final public SQLFunction SqlFunction() throws ParseException{ - trace_call("SqlFunction"); - try{ - Token fct, all = null, distinct = null, end; - ADQLOperand op = null; - SQLFunction funct = null; - try{ - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case COUNT: - fct = jj_consume_token(COUNT); - jj_consume_token(LEFT_PAR); - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case QUANTIFIER: - distinct = jj_consume_token(QUANTIFIER); - break; - default: - jj_la1[76] = jj_gen; - ; - } - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case ASTERISK: - all = jj_consume_token(ASTERISK); - break; - case LEFT_PAR: - case PLUS: - case MINUS: - case AVG: - case MAX: - case MIN: - case SUM: - case COUNT: - case BOX: - case CENTROID: - case CIRCLE: - case POINT: - case POLYGON: - case REGION: - case CONTAINS: - case INTERSECTS: - case AREA: - case COORD1: - case COORD2: - case COORDSYS: - case DISTANCE: - case ABS: - case CEILING: - case DEGREES: - case EXP: - case FLOOR: - case LOG: - case LOG10: - case MOD: - case PI: - case POWER: - case RADIANS: - case RAND: - case ROUND: - case SQRT: - case TRUNCATE: - case ACOS: - case ASIN: - case ATAN: - case ATAN2: - case COS: - case COT: - case SIN: - case TAN: - case STRING_LITERAL: - case DELIMITED_IDENTIFIER: - case REGULAR_IDENTIFIER: - case SCIENTIFIC_NUMBER: - case UNSIGNED_FLOAT: - case UNSIGNED_INTEGER: - op = ValueExpression(); - break; - default: - jj_la1[77] = jj_gen; - jj_consume_token(-1); - throw new ParseException(); - } - end = jj_consume_token(RIGHT_PAR); - funct = queryFactory.createSQLFunction((all != null) ? SQLFunctionType.COUNT_ALL : SQLFunctionType.COUNT, op, distinct != null && distinct.image.equalsIgnoreCase("distinct")); - funct.setPosition(new TextPosition(fct, end)); - break; - case AVG: - case MAX: - case MIN: - case SUM: - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case AVG: - fct = jj_consume_token(AVG); - break; - case MAX: - fct = jj_consume_token(MAX); - break; - case MIN: - fct = jj_consume_token(MIN); - break; - case SUM: - fct = jj_consume_token(SUM); - break; - default: - jj_la1[78] = jj_gen; - jj_consume_token(-1); - throw new ParseException(); - } - jj_consume_token(LEFT_PAR); - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case QUANTIFIER: - distinct = jj_consume_token(QUANTIFIER); - break; - default: - jj_la1[79] = jj_gen; - ; - } - op = ValueExpression(); - end = jj_consume_token(RIGHT_PAR); - funct = queryFactory.createSQLFunction(SQLFunctionType.valueOf(fct.image.toUpperCase()), op, distinct != null && distinct.image.equalsIgnoreCase("distinct")); - funct.setPosition(new TextPosition(fct, end)); - break; - default: - jj_la1[80] = jj_gen; - jj_consume_token(-1); - throw new ParseException(); - } - }catch(Exception ex){ - { - if (true) - throw generateParseException(ex); - } - } - { - if (true) - return funct; - } - throw new Error("Missing return statement in function"); - }finally{ - trace_return("SqlFunction"); - } - } - - /* ************** */ - /* ADQL FUNCTIONS */ - /* ************** */ - final public ADQLOperand[] Coordinates() throws ParseException{ - trace_call("Coordinates"); - try{ - ADQLOperand[] ops = new ADQLOperand[2]; - ops[0] = NumericExpression(); - jj_consume_token(COMMA); - ops[1] = NumericExpression(); - { - if (true) - return ops; - } - throw new Error("Missing return statement in function"); - }finally{ - trace_return("Coordinates"); - } - } - - final public GeometryFunction GeometryFunction() throws ParseException{ - trace_call("GeometryFunction"); - try{ - Token fct = null, end; - GeometryValue gvf1, gvf2; - GeometryValue gvp1, gvp2; - GeometryFunction gf = null; - PointFunction p1 = null, p2 = null; - ADQLColumn col1 = null, col2 = null; - try{ - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case CONTAINS: - case INTERSECTS: - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case CONTAINS: - fct = jj_consume_token(CONTAINS); - break; - case INTERSECTS: - fct = jj_consume_token(INTERSECTS); - break; - default: - jj_la1[81] = jj_gen; - jj_consume_token(-1); - throw new ParseException(); - } - jj_consume_token(LEFT_PAR); - gvf1 = GeometryExpression(); - jj_consume_token(COMMA); - gvf2 = GeometryExpression(); - end = jj_consume_token(RIGHT_PAR); - if (fct.image.equalsIgnoreCase("contains")) - gf = queryFactory.createContains(gvf1, gvf2); - else - gf = queryFactory.createIntersects(gvf1, gvf2); - break; - case AREA: - fct = jj_consume_token(AREA); - jj_consume_token(LEFT_PAR); - gvf1 = GeometryExpression(); - end = jj_consume_token(RIGHT_PAR); - gf = queryFactory.createArea(gvf1); - break; - case COORD1: - fct = jj_consume_token(COORD1); - jj_consume_token(LEFT_PAR); - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case POINT: - p1 = Point(); - gf = queryFactory.createCoord1(p1); - break; - case DELIMITED_IDENTIFIER: - case REGULAR_IDENTIFIER: - col1 = Column(); - gf = queryFactory.createCoord1(col1); - break; - default: - jj_la1[82] = jj_gen; - jj_consume_token(-1); - throw new ParseException(); - } - end = jj_consume_token(RIGHT_PAR); - break; - case COORD2: - fct = jj_consume_token(COORD2); - jj_consume_token(LEFT_PAR); - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case POINT: - p1 = Point(); - gf = queryFactory.createCoord2(p1); - break; - case DELIMITED_IDENTIFIER: - case REGULAR_IDENTIFIER: - col1 = Column(); - gf = queryFactory.createCoord2(col1); - break; - default: - jj_la1[83] = jj_gen; - jj_consume_token(-1); - throw new ParseException(); - } - end = jj_consume_token(RIGHT_PAR); - break; - case DISTANCE: - fct = jj_consume_token(DISTANCE); - jj_consume_token(LEFT_PAR); - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case POINT: - p1 = Point(); - break; - case DELIMITED_IDENTIFIER: - case REGULAR_IDENTIFIER: - col1 = Column(); - break; - default: - jj_la1[84] = jj_gen; - jj_consume_token(-1); - throw new ParseException(); - } - if (p1 != null) - gvp1 = new GeometryValue(p1); - else - gvp1 = new GeometryValue(col1); - jj_consume_token(COMMA); - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case POINT: - p2 = Point(); - break; - case DELIMITED_IDENTIFIER: - case REGULAR_IDENTIFIER: - col2 = Column(); - break; - default: - jj_la1[85] = jj_gen; - jj_consume_token(-1); - throw new ParseException(); - } - if (p2 != null) - gvp2 = new GeometryValue(p2); - else - gvp2 = new GeometryValue(col2); - end = jj_consume_token(RIGHT_PAR); - gf = queryFactory.createDistance(gvp1, gvp2); - break; - default: - jj_la1[86] = jj_gen; - jj_consume_token(-1); - throw new ParseException(); - } - }catch(Exception ex){ - { - if (true) - throw generateParseException(ex); - } - } - gf.setPosition(new TextPosition(fct, end)); - { - if (true) - return gf; - } - throw new Error("Missing return statement in function"); - }finally{ - trace_return("GeometryFunction"); - } - } - - final public ADQLOperand CoordinateSystem() throws ParseException{ - trace_call("CoordinateSystem"); - try{ - Token oldToken = token; - ADQLOperand coordSys = null; - coordSys = StringExpression(); - if (allowedCoordSys.size() > 0){ - TextPosition position = new TextPosition(oldToken.next, token); - if (coordSys == null){ - if (true) - throw new ParseException("A coordinate system must always be provided !", position); - } - if (coordSys instanceof StringConstant && !isAllowedCoordSys(((StringConstant)coordSys).getValue())){ - if (true) - throw new ParseException("\u005c"" + coordSys.toADQL() + "\u005c" is not an allowed coordinate systems !", position); - } - } - - { - if (true) - return coordSys; - } - throw new Error("Missing return statement in function"); - }finally{ - trace_return("CoordinateSystem"); - } - } - - final public GeometryFunction GeometryValueFunction() throws ParseException{ - trace_call("GeometryValueFunction"); - try{ - Token fct = null, end = null; - ADQLOperand coordSys; - ADQLOperand width, height; - ADQLOperand[] coords, tmp; - Vector vCoords; - ADQLOperand op = null; - GeometryValue gvf = null; - GeometryFunction gf = null; - try{ - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case BOX: - fct = jj_consume_token(BOX); - jj_consume_token(LEFT_PAR); - coordSys = CoordinateSystem(); - jj_consume_token(COMMA); - coords = Coordinates(); - jj_consume_token(COMMA); - width = NumericExpression(); - jj_consume_token(COMMA); - height = NumericExpression(); - end = jj_consume_token(RIGHT_PAR); - gf = queryFactory.createBox(coordSys, coords[0], coords[1], width, height); - break; - case CENTROID: - fct = jj_consume_token(CENTROID); - jj_consume_token(LEFT_PAR); - gvf = GeometryExpression(); - end = jj_consume_token(RIGHT_PAR); - gf = queryFactory.createCentroid(gvf); - break; - case CIRCLE: - fct = jj_consume_token(CIRCLE); - jj_consume_token(LEFT_PAR); - coordSys = CoordinateSystem(); - jj_consume_token(COMMA); - coords = Coordinates(); - jj_consume_token(COMMA); - width = NumericExpression(); - end = jj_consume_token(RIGHT_PAR); - gf = queryFactory.createCircle(coordSys, coords[0], coords[1], width); - break; - case POINT: - gf = Point(); - break; - case POLYGON: - fct = jj_consume_token(POLYGON); - jj_consume_token(LEFT_PAR); - coordSys = CoordinateSystem(); - vCoords = new Vector(); - jj_consume_token(COMMA); - tmp = Coordinates(); - vCoords.add(tmp[0]); - vCoords.add(tmp[1]); - jj_consume_token(COMMA); - tmp = Coordinates(); - vCoords.add(tmp[0]); - vCoords.add(tmp[1]); jj_consume_token(COMMA); tmp = Coordinates(); vCoords.add(tmp[0]); vCoords.add(tmp[1]); - label_12: while(true){ - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case COMMA: - ; - break; - default: - jj_la1[87] = jj_gen; - break label_12; - } - jj_consume_token(COMMA); - tmp = Coordinates(); - vCoords.add(tmp[0]); - vCoords.add(tmp[1]); - } - end = jj_consume_token(RIGHT_PAR); - gf = queryFactory.createPolygon(coordSys, vCoords); - break; - case REGION: - fct = jj_consume_token(REGION); - jj_consume_token(LEFT_PAR); - op = StringExpression(); - end = jj_consume_token(RIGHT_PAR); - gf = queryFactory.createRegion(op); - break; - default: - jj_la1[88] = jj_gen; - jj_consume_token(-1); - throw new ParseException(); - } - }catch(Exception ex){ - { - if (true) - throw generateParseException(ex); - } - } - if (fct != null && end != null) // = !(gf instanceof Point) - gf.setPosition(new TextPosition(fct, end)); - { - if (true) - return gf; - } - throw new Error("Missing return statement in function"); - }finally{ - trace_return("GeometryValueFunction"); - } - } - - final public PointFunction Point() throws ParseException{ - trace_call("Point"); - try{ - Token start, end; - ADQLOperand coordSys; - ADQLOperand[] coords; - start = jj_consume_token(POINT); - jj_consume_token(LEFT_PAR); - coordSys = CoordinateSystem(); - jj_consume_token(COMMA); - coords = Coordinates(); - end = jj_consume_token(RIGHT_PAR); - try{ - PointFunction pf = queryFactory.createPoint(coordSys, coords[0], coords[1]); - pf.setPosition(new TextPosition(start, end)); - { - if (true) - return pf; - } - }catch(Exception ex){ - { - if (true) - throw generateParseException(ex); - } - } - throw new Error("Missing return statement in function"); - }finally{ - trace_return("Point"); - } - } - - final public GeometryFunction ExtractCoordSys() throws ParseException{ - trace_call("ExtractCoordSys"); - try{ - Token start, end; - GeometryValue gvf; - start = jj_consume_token(COORDSYS); - jj_consume_token(LEFT_PAR); - gvf = GeometryExpression(); - end = jj_consume_token(RIGHT_PAR); - try{ - GeometryFunction gf = queryFactory.createExtractCoordSys(gvf); - gf.setPosition(new TextPosition(start, end)); - { - if (true) - return gf; - } - }catch(Exception ex){ - { - if (true) - throw generateParseException(ex); - } - } - throw new Error("Missing return statement in function"); - }finally{ - trace_return("ExtractCoordSys"); - } - } - - /* ***************** */ - /* NUMERIC FUNCTIONS */ - /* ***************** */ - final public ADQLFunction NumericFunction() throws ParseException{ - trace_call("NumericFunction"); - try{ - ADQLFunction fct; - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case ABS: - case CEILING: - case DEGREES: - case EXP: - case FLOOR: - case LOG: - case LOG10: - case MOD: - case PI: - case POWER: - case RADIANS: - case RAND: - case ROUND: - case SQRT: - case TRUNCATE: - fct = MathFunction(); - break; - case ACOS: - case ASIN: - case ATAN: - case ATAN2: - case COS: - case COT: - case SIN: - case TAN: - fct = TrigFunction(); - break; - case CONTAINS: - case INTERSECTS: - case AREA: - case COORD1: - case COORD2: - case DISTANCE: - fct = GeometryFunction(); + } + end = jj_consume_token(RIGHT_PAR); + gf = queryFactory.createPolygon(coordSys, vCoords); break; - case REGULAR_IDENTIFIER: - fct = UserDefinedFunction(); + case REGION: + fct = jj_consume_token(REGION); + jj_consume_token(LEFT_PAR); + op = StringExpression(); + end = jj_consume_token(RIGHT_PAR); + gf = queryFactory.createRegion(op); break; default: jj_la1[89] = jj_gen; jj_consume_token(-1); throw new ParseException(); } + }catch(Exception ex){ { if (true) - return fct; + throw generateParseException(ex); } - throw new Error("Missing return statement in function"); - }finally{ - trace_return("NumericFunction"); } + if (fct != null && end != null) // = !(gf instanceof Point) + gf.setPosition(new TextPosition(fct, end)); + { + if (true) + return gf; + } + throw new Error("Missing return statement in function"); } - final public MathFunction MathFunction() throws ParseException{ - trace_call("MathFunction"); + final public PointFunction Point() throws ParseException{ + Token start, end; + ADQLOperand coordSys; + ADQLOperand[] coords; + start = jj_consume_token(POINT); + jj_consume_token(LEFT_PAR); + coordSys = CoordinateSystem(); + jj_consume_token(COMMA); + coords = Coordinates(); + end = jj_consume_token(RIGHT_PAR); try{ - Token fct = null, end; - ADQLOperand param1 = null, param2 = null; - NumericConstant integerValue = null; - try{ - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case ABS: - fct = jj_consume_token(ABS); - jj_consume_token(LEFT_PAR); - param1 = NumericExpression(); - end = jj_consume_token(RIGHT_PAR); - break; - case CEILING: - fct = jj_consume_token(CEILING); - jj_consume_token(LEFT_PAR); - param1 = NumericExpression(); - end = jj_consume_token(RIGHT_PAR); - break; - case DEGREES: - fct = jj_consume_token(DEGREES); - jj_consume_token(LEFT_PAR); - param1 = NumericExpression(); - end = jj_consume_token(RIGHT_PAR); - break; - case EXP: - fct = jj_consume_token(EXP); - jj_consume_token(LEFT_PAR); - param1 = NumericExpression(); - end = jj_consume_token(RIGHT_PAR); - break; - case FLOOR: - fct = jj_consume_token(FLOOR); - jj_consume_token(LEFT_PAR); - param1 = NumericExpression(); - end = jj_consume_token(RIGHT_PAR); - break; - case LOG: - fct = jj_consume_token(LOG); - jj_consume_token(LEFT_PAR); - param1 = NumericExpression(); - end = jj_consume_token(RIGHT_PAR); - break; - case LOG10: - fct = jj_consume_token(LOG10); - jj_consume_token(LEFT_PAR); - param1 = NumericExpression(); - end = jj_consume_token(RIGHT_PAR); - break; - case MOD: - fct = jj_consume_token(MOD); - jj_consume_token(LEFT_PAR); - param1 = NumericExpression(); - jj_consume_token(COMMA); - param2 = NumericExpression(); - end = jj_consume_token(RIGHT_PAR); - break; - case PI: - fct = jj_consume_token(PI); - jj_consume_token(LEFT_PAR); - end = jj_consume_token(RIGHT_PAR); - break; - case POWER: - fct = jj_consume_token(POWER); - jj_consume_token(LEFT_PAR); - param1 = NumericExpression(); - jj_consume_token(COMMA); - param2 = NumericExpression(); - end = jj_consume_token(RIGHT_PAR); - break; - case RADIANS: - fct = jj_consume_token(RADIANS); - jj_consume_token(LEFT_PAR); - param1 = NumericExpression(); - end = jj_consume_token(RIGHT_PAR); - break; - case RAND: - fct = jj_consume_token(RAND); - jj_consume_token(LEFT_PAR); - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case LEFT_PAR: - case PLUS: - case MINUS: - case AVG: - case MAX: - case MIN: - case SUM: - case COUNT: - case CONTAINS: - case INTERSECTS: - case AREA: - case COORD1: - case COORD2: - case DISTANCE: - case ABS: - case CEILING: - case DEGREES: - case EXP: - case FLOOR: - case LOG: - case LOG10: - case MOD: - case PI: - case POWER: - case RADIANS: - case RAND: - case ROUND: - case SQRT: - case TRUNCATE: - case ACOS: - case ASIN: - case ATAN: - case ATAN2: - case COS: - case COT: - case SIN: - case TAN: - case STRING_LITERAL: - case DELIMITED_IDENTIFIER: - case REGULAR_IDENTIFIER: - case SCIENTIFIC_NUMBER: - case UNSIGNED_FLOAT: - case UNSIGNED_INTEGER: - param1 = NumericExpression(); - break; - default: - jj_la1[90] = jj_gen; - ; - } - end = jj_consume_token(RIGHT_PAR); - break; - case ROUND: - fct = jj_consume_token(ROUND); - jj_consume_token(LEFT_PAR); - param1 = NumericExpression(); - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case COMMA: - jj_consume_token(COMMA); - param2 = SignedInteger(); - break; - default: - jj_la1[91] = jj_gen; - ; - } - end = jj_consume_token(RIGHT_PAR); - break; - case SQRT: - fct = jj_consume_token(SQRT); - jj_consume_token(LEFT_PAR); - param1 = NumericExpression(); - end = jj_consume_token(RIGHT_PAR); - break; - case TRUNCATE: - fct = jj_consume_token(TRUNCATE); - jj_consume_token(LEFT_PAR); - param1 = NumericExpression(); - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case COMMA: - jj_consume_token(COMMA); - param2 = SignedInteger(); - break; - default: - jj_la1[92] = jj_gen; - ; - } - end = jj_consume_token(RIGHT_PAR); - break; - default: - jj_la1[93] = jj_gen; - jj_consume_token(-1); - throw new ParseException(); - } - if (param1 != null){ - MathFunction mf = queryFactory.createMathFunction(MathFunctionType.valueOf(fct.image.toUpperCase()), param1, param2); - mf.setPosition(new TextPosition(fct, end)); - { - if (true) - return mf; - } - }else{ - if (true) - return null; - } - }catch(Exception ex){ - { - if (true) - throw generateParseException(ex); - } + PointFunction pf = queryFactory.createPoint(coordSys, coords[0], coords[1]); + pf.setPosition(new TextPosition(start, end)); + { + if (true) + return pf; + } + }catch(Exception ex){ + { + if (true) + throw generateParseException(ex); } - throw new Error("Missing return statement in function"); - }finally{ - trace_return("MathFunction"); } + throw new Error("Missing return statement in function"); } - final public MathFunction TrigFunction() throws ParseException{ - trace_call("TrigFunction"); + final public GeometryFunction ExtractCoordSys() throws ParseException{ + Token start, end; + GeometryValue gvf; + start = jj_consume_token(COORDSYS); + jj_consume_token(LEFT_PAR); + gvf = GeometryExpression(); + end = jj_consume_token(RIGHT_PAR); + try{ + GeometryFunction gf = queryFactory.createExtractCoordSys(gvf); + gf.setPosition(new TextPosition(start, end)); + { + if (true) + return gf; + } + }catch(Exception ex){ + { + if (true) + throw generateParseException(ex); + } + } + throw new Error("Missing return statement in function"); + } + + /* ***************** */ + /* NUMERIC FUNCTIONS */ + /* ***************** */ + final public ADQLFunction NumericFunction() throws ParseException{ + ADQLFunction fct; + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case ABS: + case CEILING: + case DEGREES: + case EXP: + case FLOOR: + case LOG: + case LOG10: + case MOD: + case PI: + case POWER: + case RADIANS: + case RAND: + case ROUND: + case SQRT: + case TRUNCATE: + fct = MathFunction(); + break; + case ACOS: + case ASIN: + case ATAN: + case ATAN2: + case COS: + case COT: + case SIN: + case TAN: + fct = TrigFunction(); + break; + case CONTAINS: + case INTERSECTS: + case AREA: + case COORD1: + case COORD2: + case DISTANCE: + fct = GeometryFunction(); + break; + case REGULAR_IDENTIFIER: + fct = UserDefinedFunction(); + ((UserDefinedFunction)fct).setExpectedType('N'); + break; + default: + jj_la1[90] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + { + if (true) + return fct; + } + throw new Error("Missing return statement in function"); + } + + final public MathFunction MathFunction() throws ParseException{ + Token fct = null, end; + ADQLOperand param1 = null, param2 = null; + NumericConstant integerValue = null; try{ - Token fct = null, end; - ADQLOperand param1 = null, param2 = null; switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case ACOS: - fct = jj_consume_token(ACOS); + case ABS: + fct = jj_consume_token(ABS); jj_consume_token(LEFT_PAR); param1 = NumericExpression(); end = jj_consume_token(RIGHT_PAR); break; - case ASIN: - fct = jj_consume_token(ASIN); + case CEILING: + fct = jj_consume_token(CEILING); jj_consume_token(LEFT_PAR); param1 = NumericExpression(); end = jj_consume_token(RIGHT_PAR); break; - case ATAN: - fct = jj_consume_token(ATAN); + case DEGREES: + fct = jj_consume_token(DEGREES); jj_consume_token(LEFT_PAR); param1 = NumericExpression(); end = jj_consume_token(RIGHT_PAR); break; - case ATAN2: - fct = jj_consume_token(ATAN2); + case EXP: + fct = jj_consume_token(EXP); jj_consume_token(LEFT_PAR); param1 = NumericExpression(); - jj_consume_token(COMMA); - param2 = NumericExpression(); end = jj_consume_token(RIGHT_PAR); break; - case COS: - fct = jj_consume_token(COS); + case FLOOR: + fct = jj_consume_token(FLOOR); jj_consume_token(LEFT_PAR); param1 = NumericExpression(); end = jj_consume_token(RIGHT_PAR); break; - case COT: - fct = jj_consume_token(COT); + case LOG: + fct = jj_consume_token(LOG); jj_consume_token(LEFT_PAR); param1 = NumericExpression(); end = jj_consume_token(RIGHT_PAR); break; - case SIN: - fct = jj_consume_token(SIN); + case LOG10: + fct = jj_consume_token(LOG10); jj_consume_token(LEFT_PAR); param1 = NumericExpression(); end = jj_consume_token(RIGHT_PAR); break; - case TAN: - fct = jj_consume_token(TAN); + case MOD: + fct = jj_consume_token(MOD); jj_consume_token(LEFT_PAR); param1 = NumericExpression(); + jj_consume_token(COMMA); + param2 = NumericExpression(); end = jj_consume_token(RIGHT_PAR); break; - default: - jj_la1[94] = jj_gen; - jj_consume_token(-1); - throw new ParseException(); - } - try{ - if (param1 != null){ - MathFunction mf = queryFactory.createMathFunction(MathFunctionType.valueOf(fct.image.toUpperCase()), param1, param2); - mf.setPosition(new TextPosition(fct, end)); - { - if (true) - return mf; - } - }else{ - if (true) - return null; - } - }catch(Exception ex){ - { - if (true) - throw generateParseException(ex); - } - } - throw new Error("Missing return statement in function"); - }finally{ - trace_return("TrigFunction"); - } - } - - /* /!\ WARNING: The function name may be prefixed by "udf_" but there is no way to check it here ! */ - final public UserDefinedFunction UserDefinedFunction() throws ParseException{ - trace_call("UserDefinedFunction"); - try{ - Token fct, end; - Vector params = new Vector(); - ADQLOperand op; - fct = jj_consume_token(REGULAR_IDENTIFIER); - jj_consume_token(LEFT_PAR); - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case LEFT_PAR: - case PLUS: - case MINUS: - case AVG: - case MAX: - case MIN: - case SUM: - case COUNT: - case BOX: - case CENTROID: - case CIRCLE: - case POINT: - case POLYGON: - case REGION: - case CONTAINS: - case INTERSECTS: - case AREA: - case COORD1: - case COORD2: - case COORDSYS: - case DISTANCE: - case ABS: - case CEILING: - case DEGREES: - case EXP: - case FLOOR: - case LOG: - case LOG10: - case MOD: case PI: + fct = jj_consume_token(PI); + jj_consume_token(LEFT_PAR); + end = jj_consume_token(RIGHT_PAR); + break; case POWER: + fct = jj_consume_token(POWER); + jj_consume_token(LEFT_PAR); + param1 = NumericExpression(); + jj_consume_token(COMMA); + param2 = NumericExpression(); + end = jj_consume_token(RIGHT_PAR); + break; case RADIANS: + fct = jj_consume_token(RADIANS); + jj_consume_token(LEFT_PAR); + param1 = NumericExpression(); + end = jj_consume_token(RIGHT_PAR); + break; case RAND: + fct = jj_consume_token(RAND); + jj_consume_token(LEFT_PAR); + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case LEFT_PAR: + case PLUS: + case MINUS: + case AVG: + case MAX: + case MIN: + case SUM: + case COUNT: + case CONTAINS: + case INTERSECTS: + case AREA: + case COORD1: + case COORD2: + case DISTANCE: + case ABS: + case CEILING: + case DEGREES: + case EXP: + case FLOOR: + case LOG: + case LOG10: + case MOD: + case PI: + case POWER: + case RADIANS: + case RAND: + case ROUND: + case SQRT: + case TRUNCATE: + case ACOS: + case ASIN: + case ATAN: + case ATAN2: + case COS: + case COT: + case SIN: + case TAN: + case DELIMITED_IDENTIFIER: + case REGULAR_IDENTIFIER: + case SCIENTIFIC_NUMBER: + case UNSIGNED_FLOAT: + case UNSIGNED_INTEGER: + param1 = NumericExpression(); + break; + default: + jj_la1[91] = jj_gen; + ; + } + end = jj_consume_token(RIGHT_PAR); + break; case ROUND: + fct = jj_consume_token(ROUND); + jj_consume_token(LEFT_PAR); + param1 = NumericExpression(); + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case COMMA: + jj_consume_token(COMMA); + param2 = SignedInteger(); + break; + default: + jj_la1[92] = jj_gen; + ; + } + end = jj_consume_token(RIGHT_PAR); + break; case SQRT: + fct = jj_consume_token(SQRT); + jj_consume_token(LEFT_PAR); + param1 = NumericExpression(); + end = jj_consume_token(RIGHT_PAR); + break; case TRUNCATE: - case ACOS: - case ASIN: - case ATAN: - case ATAN2: - case COS: - case COT: - case SIN: - case TAN: - case STRING_LITERAL: - case DELIMITED_IDENTIFIER: - case REGULAR_IDENTIFIER: - case SCIENTIFIC_NUMBER: - case UNSIGNED_FLOAT: - case UNSIGNED_INTEGER: - op = ValueExpression(); - params.add(op); - label_13: while(true){ - switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ - case COMMA: - ; - break; - default: - jj_la1[95] = jj_gen; - break label_13; - } - jj_consume_token(COMMA); - op = ValueExpression(); - params.add(op); + fct = jj_consume_token(TRUNCATE); + jj_consume_token(LEFT_PAR); + param1 = NumericExpression(); + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case COMMA: + jj_consume_token(COMMA); + param2 = SignedInteger(); + break; + default: + jj_la1[93] = jj_gen; + ; } + end = jj_consume_token(RIGHT_PAR); break; default: - jj_la1[96] = jj_gen; - ; + jj_la1[94] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); } - end = jj_consume_token(RIGHT_PAR); - //System.out.println("INFO [ADQLParser]: \""+fct.image+"\" (from line "+fct.beginLine+" and column "+fct.beginColumn+" to line "+token.endLine+" and column "+(token.endColumn+1)+") is considered as an user defined function !"); - try{ - ADQLOperand[] parameters = new ADQLOperand[params.size()]; - for(int i = 0; i < params.size(); i++) - parameters[i] = params.get(i); - UserDefinedFunction udf = queryFactory.createUserDefinedFunction(fct.image, parameters); - udf.setPosition(new TextPosition(fct, end)); + if (param1 != null){ + MathFunction mf = queryFactory.createMathFunction(MathFunctionType.valueOf(fct.image.toUpperCase()), param1, param2); + mf.setPosition(new TextPosition(fct, end)); { if (true) - return udf; + return mf; } - }catch(UnsupportedOperationException uoe){ + }else{ + if (true) + return null; + } + }catch(Exception ex){ + { + if (true) + throw generateParseException(ex); + } + } + throw new Error("Missing return statement in function"); + } + + final public MathFunction TrigFunction() throws ParseException{ + Token fct = null, end; + ADQLOperand param1 = null, param2 = null; + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case ACOS: + fct = jj_consume_token(ACOS); + jj_consume_token(LEFT_PAR); + param1 = NumericExpression(); + end = jj_consume_token(RIGHT_PAR); + break; + case ASIN: + fct = jj_consume_token(ASIN); + jj_consume_token(LEFT_PAR); + param1 = NumericExpression(); + end = jj_consume_token(RIGHT_PAR); + break; + case ATAN: + fct = jj_consume_token(ATAN); + jj_consume_token(LEFT_PAR); + param1 = NumericExpression(); + end = jj_consume_token(RIGHT_PAR); + break; + case ATAN2: + fct = jj_consume_token(ATAN2); + jj_consume_token(LEFT_PAR); + param1 = NumericExpression(); + jj_consume_token(COMMA); + param2 = NumericExpression(); + end = jj_consume_token(RIGHT_PAR); + break; + case COS: + fct = jj_consume_token(COS); + jj_consume_token(LEFT_PAR); + param1 = NumericExpression(); + end = jj_consume_token(RIGHT_PAR); + break; + case COT: + fct = jj_consume_token(COT); + jj_consume_token(LEFT_PAR); + param1 = NumericExpression(); + end = jj_consume_token(RIGHT_PAR); + break; + case SIN: + fct = jj_consume_token(SIN); + jj_consume_token(LEFT_PAR); + param1 = NumericExpression(); + end = jj_consume_token(RIGHT_PAR); + break; + case TAN: + fct = jj_consume_token(TAN); + jj_consume_token(LEFT_PAR); + param1 = NumericExpression(); + end = jj_consume_token(RIGHT_PAR); + break; + default: + jj_la1[95] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + try{ + if (param1 != null){ + MathFunction mf = queryFactory.createMathFunction(MathFunctionType.valueOf(fct.image.toUpperCase()), param1, param2); + mf.setPosition(new TextPosition(fct, end)); { if (true) - throw new ParseException(uoe.getMessage(), new TextPosition(fct, token)); + return mf; } - }catch(Exception ex){ - { - if (true) - throw generateParseException(ex); + }else{ + if (true) + return null; + } + }catch(Exception ex){ + { + if (true) + throw generateParseException(ex); + } + } + throw new Error("Missing return statement in function"); + } + + final public UserDefinedFunction UserDefinedFunction() throws ParseException{ + Token fct, end; + Vector params = new Vector(); + ADQLOperand op; + fct = jj_consume_token(REGULAR_IDENTIFIER); + jj_consume_token(LEFT_PAR); + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case LEFT_PAR: + case PLUS: + case MINUS: + case AVG: + case MAX: + case MIN: + case SUM: + case COUNT: + case BOX: + case CENTROID: + case CIRCLE: + case POINT: + case POLYGON: + case REGION: + case CONTAINS: + case INTERSECTS: + case AREA: + case COORD1: + case COORD2: + case COORDSYS: + case DISTANCE: + case ABS: + case CEILING: + case DEGREES: + case EXP: + case FLOOR: + case LOG: + case LOG10: + case MOD: + case PI: + case POWER: + case RADIANS: + case RAND: + case ROUND: + case SQRT: + case TRUNCATE: + case ACOS: + case ASIN: + case ATAN: + case ATAN2: + case COS: + case COT: + case SIN: + case TAN: + case STRING_LITERAL: + case DELIMITED_IDENTIFIER: + case REGULAR_IDENTIFIER: + case SCIENTIFIC_NUMBER: + case UNSIGNED_FLOAT: + case UNSIGNED_INTEGER: + op = ValueExpression(); + params.add(op); + label_13: while(true){ + switch((jj_ntk == -1) ? jj_ntk() : jj_ntk){ + case COMMA: + ; + break; + default: + jj_la1[96] = jj_gen; + break label_13; + } + jj_consume_token(COMMA); + op = ValueExpression(); + params.add(op); } + break; + default: + jj_la1[97] = jj_gen; + ; + } + end = jj_consume_token(RIGHT_PAR); + //System.out.println("INFO [ADQLParser]: \""+fct.image+"\" (from line "+fct.beginLine+" and column "+fct.beginColumn+" to line "+token.endLine+" and column "+(token.endColumn+1)+") is considered as an user defined function !"); + try{ + // Build the parameters list: + ADQLOperand[] parameters = new ADQLOperand[params.size()]; + for(int i = 0; i < params.size(); i++) + parameters[i] = params.get(i); + // Create the UDF function: + UserDefinedFunction udf = queryFactory.createUserDefinedFunction(fct.image, parameters); + udf.setPosition(new TextPosition(fct, end)); + { + if (true) + return udf; + } + }catch(UnsupportedOperationException uoe){ + /* This catch clause is just for backward compatibility: + * if the createUserDefinedFunction(...) is overridden and + * the function can not be identified a such exception may be thrown). */ + { + if (true) + throw new ParseException(uoe.getMessage(), new TextPosition(fct, token)); + } + }catch(Exception ex){ + { + if (true) + throw generateParseException(ex); } - throw new Error("Missing return statement in function"); - }finally{ - trace_return("UserDefinedFunction"); } + throw new Error("Missing return statement in function"); } private boolean jj_2_1(int xla){ @@ -3727,251 +3510,385 @@ public class ADQLParser implements ADQLParserConstants { } } - private boolean jj_3R_16(){ + private boolean jj_2_14(int xla){ + jj_la = xla; + jj_lastpos = jj_scanpos = token; + try{ + return !jj_3_14(); + }catch(LookaheadSuccess ls){ + return true; + }finally{ + jj_save(13, xla); + } + } + + private boolean jj_2_15(int xla){ + jj_la = xla; + jj_lastpos = jj_scanpos = token; + try{ + return !jj_3_15(); + }catch(LookaheadSuccess ls){ + return true; + }finally{ + jj_save(14, xla); + } + } + + private boolean jj_2_16(int xla){ + jj_la = xla; + jj_lastpos = jj_scanpos = token; + try{ + return !jj_3_16(); + }catch(LookaheadSuccess ls){ + return true; + }finally{ + jj_save(15, xla); + } + } + + private boolean jj_3R_138(){ + if (jj_scan_token(CIRCLE)) + return true; if (jj_scan_token(LEFT_PAR)) return true; - if (jj_3R_28()) + if (jj_3R_169()) + return true; + if (jj_scan_token(COMMA)) + return true; + if (jj_3R_170()) + return true; + if (jj_scan_token(COMMA)) + return true; + if (jj_3R_108()) return true; if (jj_scan_token(RIGHT_PAR)) return true; return false; } - private boolean jj_3R_68(){ - if (jj_3R_23()) + private boolean jj_3R_126(){ + if (jj_scan_token(LEFT_PAR)) + return true; + if (jj_3R_27()) + return true; + if (jj_scan_token(RIGHT_PAR)) return true; return false; } - private boolean jj_3R_67(){ - if (jj_3R_116()) + private boolean jj_3R_137(){ + if (jj_scan_token(CENTROID)) + return true; + if (jj_scan_token(LEFT_PAR)) + return true; + if (jj_3R_122()) + return true; + if (jj_scan_token(RIGHT_PAR)) return true; return false; } - private boolean jj_3R_66(){ - if (jj_3R_115()) + private boolean jj_3R_125(){ + if (jj_3R_21()) + return true; + return false; + } + + private boolean jj_3R_46(){ + Token xsp; + xsp = jj_scanpos; + if (jj_3R_62()){ + jj_scanpos = xsp; + if (jj_3R_63()){ + jj_scanpos = xsp; + if (jj_3R_64()){ + jj_scanpos = xsp; + if (jj_3R_65()){ + jj_scanpos = xsp; + if (jj_3R_66()){ + jj_scanpos = xsp; + if (jj_3R_67()){ + jj_scanpos = xsp; + if (jj_3R_68()){ + jj_scanpos = xsp; + if (jj_3R_69()) + return true; + } + } + } + } + } + } + } + return false; + } + + private boolean jj_3R_148(){ + if (jj_scan_token(TOP)) + return true; + if (jj_scan_token(UNSIGNED_INTEGER)) return true; return false; } private boolean jj_3R_124(){ + if (jj_3R_22()) + return true; + return false; + } + + private boolean jj_3R_147(){ + if (jj_scan_token(QUANTIFIER)) + return true; + return false; + } + + private boolean jj_3R_112(){ + if (jj_scan_token(FULL)) + return true; + return false; + } + + private boolean jj_3R_48(){ + if (jj_scan_token(SELECT)) + return true; + Token xsp; + xsp = jj_scanpos; + if (jj_3R_147()) + jj_scanpos = xsp; + xsp = jj_scanpos; + if (jj_3R_148()) + jj_scanpos = xsp; + if (jj_3R_149()) + return true; + while(true){ + xsp = jj_scanpos; + if (jj_3R_150()){ + jj_scanpos = xsp; + break; + } + } + return false; + } + + private boolean jj_3R_136(){ if (jj_scan_token(BOX)) return true; if (jj_scan_token(LEFT_PAR)) return true; - if (jj_3R_157()) + if (jj_3R_169()) return true; if (jj_scan_token(COMMA)) return true; - if (jj_3R_158()) + if (jj_3R_170()) return true; if (jj_scan_token(COMMA)) return true; - if (jj_3R_102()) + if (jj_3R_108()) return true; if (jj_scan_token(COMMA)) return true; - if (jj_3R_102()) + if (jj_3R_108()) return true; if (jj_scan_token(RIGHT_PAR)) return true; return false; } - private boolean jj_3R_109(){ - if (jj_scan_token(FULL)) + private boolean jj_3R_182(){ + if (jj_3R_21()) return true; return false; } - private boolean jj_3R_122(){ - if (jj_3R_144()) + private boolean jj_3R_121(){ + if (jj_scan_token(LEFT_PAR)) + return true; + if (jj_3R_108()) + return true; + if (jj_scan_token(RIGHT_PAR)) return true; return false; } - private boolean jj_3R_49(){ - Token xsp; - xsp = jj_scanpos; - if (jj_3R_66()){ - jj_scanpos = xsp; - if (jj_3R_67()){ - jj_scanpos = xsp; - if (jj_3R_68()){ - jj_scanpos = xsp; - if (jj_3R_69()){ - jj_scanpos = xsp; - if (jj_3R_70()) - return true; - } - } - } - } + private boolean jj_3R_16(){ + if (jj_scan_token(LEFT_PAR)) + return true; + if (jj_3R_31()) + return true; + if (jj_scan_token(RIGHT_PAR)) + return true; return false; } - private boolean jj_3R_121(){ + private boolean jj_3R_120(){ if (jj_3R_143()) return true; return false; } - private boolean jj_3R_101(){ + private boolean jj_3R_76(){ Token xsp; xsp = jj_scanpos; if (jj_3R_124()){ jj_scanpos = xsp; if (jj_3R_125()){ jj_scanpos = xsp; - if (jj_3R_126()){ - jj_scanpos = xsp; - if (jj_3R_127()){ - jj_scanpos = xsp; - if (jj_3R_128()){ - jj_scanpos = xsp; - if (jj_3R_129()) - return true; - } - } - } + if (jj_3R_126()) + return true; } } return false; } - private boolean jj_3R_190(){ - if (jj_3R_23()) + private boolean jj_3R_119(){ + if (jj_3R_21()) return true; return false; } - private boolean jj_3R_120(){ + private boolean jj_3R_177(){ + if (jj_3R_158()) + return true; + return false; + } + + private boolean jj_3R_118(){ if (jj_3R_142()) return true; return false; } - private boolean jj_3R_119(){ - if (jj_3R_141()) + private boolean jj_3R_109(){ + Token xsp; + xsp = jj_scanpos; + if (jj_3R_136()){ + jj_scanpos = xsp; + if (jj_3R_137()){ + jj_scanpos = xsp; + if (jj_3R_138()){ + jj_scanpos = xsp; + if (jj_3R_139()){ + jj_scanpos = xsp; + if (jj_3R_140()){ + jj_scanpos = xsp; + if (jj_3R_141()) + return true; + } + } + } + } + } + return false; + } + + private boolean jj_3R_183(){ + if (jj_3R_46()) return true; return false; } - private boolean jj_3R_185(){ - if (jj_3R_147()) + private boolean jj_3R_175(){ + if (jj_3R_158()) return true; return false; } - private boolean jj_3R_183(){ - if (jj_3R_147()) + private boolean jj_3R_180(){ + if (jj_3R_21()) return true; return false; } - private boolean jj_3R_149(){ - if (jj_3R_41()) + private boolean jj_3R_181(){ + if (jj_3R_158()) return true; return false; } - private boolean jj_3R_188(){ - if (jj_3R_23()) + private boolean jj_3R_169(){ + if (jj_3R_27()) return true; return false; } - private boolean jj_3R_189(){ - if (jj_3R_147()) + private boolean jj_3R_115(){ + if (jj_scan_token(FULL)) return true; return false; } - private boolean jj_3R_105(){ - if (jj_scan_token(RIGHT)) + private boolean jj_3R_133(){ + if (jj_3R_155()) return true; return false; } - private boolean jj_3R_28(){ - if (jj_3R_44()) - return true; - if (jj_3R_118()) - return true; + private boolean jj_3R_74(){ Token xsp; xsp = jj_scanpos; - if (jj_3R_119()) - jj_scanpos = xsp; - xsp = jj_scanpos; - if (jj_3R_120()) - jj_scanpos = xsp; - xsp = jj_scanpos; - if (jj_3R_121()) - jj_scanpos = xsp; - xsp = jj_scanpos; - if (jj_3R_122()) + if (jj_3R_118()){ jj_scanpos = xsp; + if (jj_3R_119()){ + jj_scanpos = xsp; + if (jj_3R_120()){ + jj_scanpos = xsp; + if (jj_3R_121()) + return true; + } + } + } return false; } - private boolean jj_3R_157(){ - if (jj_3R_24()) + private boolean jj_3R_132(){ + if (jj_3R_154()) return true; return false; } - private boolean jj_3R_192(){ - Token xsp; - xsp = jj_scanpos; - if (jj_scan_token(8)){ - jj_scanpos = xsp; - if (jj_scan_token(9)) - return true; - } + private boolean jj_3R_179(){ + if (jj_3R_158()) + return true; return false; } - private boolean jj_3R_191(){ - Token xsp; - xsp = jj_scanpos; - if (jj_3R_192()) - jj_scanpos = xsp; - if (jj_scan_token(UNSIGNED_INTEGER)) + private boolean jj_3R_131(){ + if (jj_3R_153()) return true; return false; } - private boolean jj_3R_187(){ - if (jj_3R_147()) + private boolean jj_3R_130(){ + if (jj_3R_152()) return true; return false; } - private boolean jj_3R_108(){ + private boolean jj_3R_111(){ if (jj_scan_token(RIGHT)) return true; return false; } - private boolean jj_3R_98(){ + private boolean jj_3R_105(){ if (jj_scan_token(DISTANCE)) return true; if (jj_scan_token(LEFT_PAR)) return true; Token xsp; xsp = jj_scanpos; - if (jj_3R_187()){ + if (jj_3R_179()){ jj_scanpos = xsp; - if (jj_3R_188()) + if (jj_3R_180()) return true; } if (jj_scan_token(COMMA)) return true; xsp = jj_scanpos; - if (jj_3R_189()){ + if (jj_3R_181()){ jj_scanpos = xsp; - if (jj_3R_190()) + if (jj_3R_182()) return true; } if (jj_scan_token(RIGHT_PAR)) @@ -3979,16 +3896,16 @@ public class ADQLParser implements ADQLParserConstants { return false; } - private boolean jj_3R_97(){ + private boolean jj_3R_104(){ if (jj_scan_token(COORD2)) return true; if (jj_scan_token(LEFT_PAR)) return true; Token xsp; xsp = jj_scanpos; - if (jj_3R_185()){ + if (jj_3R_177()){ jj_scanpos = xsp; - if (jj_3R_186()) + if (jj_3R_178()) return true; } if (jj_scan_token(RIGHT_PAR)) @@ -3996,16 +3913,37 @@ public class ADQLParser implements ADQLParserConstants { return false; } - private boolean jj_3R_96(){ + private boolean jj_3R_31(){ + if (jj_3R_48()) + return true; + if (jj_3R_129()) + return true; + Token xsp; + xsp = jj_scanpos; + if (jj_3R_130()) + jj_scanpos = xsp; + xsp = jj_scanpos; + if (jj_3R_131()) + jj_scanpos = xsp; + xsp = jj_scanpos; + if (jj_3R_132()) + jj_scanpos = xsp; + xsp = jj_scanpos; + if (jj_3R_133()) + jj_scanpos = xsp; + return false; + } + + private boolean jj_3R_103(){ if (jj_scan_token(COORD1)) return true; if (jj_scan_token(LEFT_PAR)) return true; Token xsp; xsp = jj_scanpos; - if (jj_3R_183()){ + if (jj_3R_175()){ jj_scanpos = xsp; - if (jj_3R_184()) + if (jj_3R_176()) return true; } if (jj_scan_token(RIGHT_PAR)) @@ -4013,27 +3951,40 @@ public class ADQLParser implements ADQLParserConstants { return false; } - private boolean jj_3R_95(){ + private boolean jj_3R_102(){ if (jj_scan_token(AREA)) return true; if (jj_scan_token(LEFT_PAR)) return true; - if (jj_3R_64()) + if (jj_3R_122()) return true; if (jj_scan_token(RIGHT_PAR)) return true; return false; } - private boolean jj_3R_179(){ - if (jj_scan_token(COMMA)) - return true; - if (jj_3R_14()) + private boolean jj_3R_197(){ + Token xsp; + xsp = jj_scanpos; + if (jj_scan_token(8)){ + jj_scanpos = xsp; + if (jj_scan_token(9)) + return true; + } + return false; + } + + private boolean jj_3R_191(){ + Token xsp; + xsp = jj_scanpos; + if (jj_3R_197()) + jj_scanpos = xsp; + if (jj_scan_token(UNSIGNED_INTEGER)) return true; return false; } - private boolean jj_3R_94(){ + private boolean jj_3R_101(){ Token xsp; xsp = jj_scanpos; if (jj_scan_token(58)){ @@ -4043,94 +3994,164 @@ public class ADQLParser implements ADQLParserConstants { } if (jj_scan_token(LEFT_PAR)) return true; - if (jj_3R_64()) + if (jj_3R_122()) return true; if (jj_scan_token(COMMA)) return true; - if (jj_3R_64()) + if (jj_3R_122()) return true; if (jj_scan_token(RIGHT_PAR)) return true; return false; } - private boolean jj_3R_104(){ - if (jj_scan_token(LEFT)) + private boolean jj_3R_114(){ + if (jj_scan_token(RIGHT)) return true; return false; } - private boolean jj_3R_115(){ + private boolean jj_3R_201(){ + if (jj_scan_token(COMMA)) + return true; + if (jj_3R_14()) + return true; + return false; + } + + private boolean jj_3R_59(){ Token xsp; xsp = jj_scanpos; - if (jj_scan_token(99)){ + if (jj_3R_101()){ jj_scanpos = xsp; - if (jj_scan_token(100)){ + if (jj_3R_102()){ jj_scanpos = xsp; - if (jj_scan_token(101)) - return true; + if (jj_3R_103()){ + jj_scanpos = xsp; + if (jj_3R_104()){ + jj_scanpos = xsp; + if (jj_3R_105()) + return true; + } + } } } return false; } - private boolean jj_3R_61(){ + private boolean jj_3R_157(){ + if (jj_scan_token(COMMA)) + return true; + if (jj_3R_46()) + return true; + return false; + } + + private boolean jj_3R_170(){ + if (jj_3R_108()) + return true; + if (jj_scan_token(COMMA)) + return true; + if (jj_3R_108()) + return true; + return false; + } + + private boolean jj_3R_160(){ Token xsp; xsp = jj_scanpos; - if (jj_3R_104()){ + if (jj_scan_token(47)){ jj_scanpos = xsp; - if (jj_3R_105()){ + if (jj_scan_token(48)){ jj_scanpos = xsp; - if (jj_3R_106()) - return true; + if (jj_scan_token(49)){ + jj_scanpos = xsp; + if (jj_scan_token(50)) + return true; + } } } + if (jj_scan_token(LEFT_PAR)) + return true; xsp = jj_scanpos; - if (jj_scan_token(25)) + if (jj_scan_token(19)) jj_scanpos = xsp; + if (jj_3R_46()) + return true; + if (jj_scan_token(RIGHT_PAR)) + return true; return false; } - private boolean jj_3R_45(){ + private boolean jj_3R_142(){ Token xsp; xsp = jj_scanpos; - if (jj_scan_token(24)){ + if (jj_scan_token(99)){ jj_scanpos = xsp; - if (jj_3R_61()) - return true; + if (jj_scan_token(100)){ + jj_scanpos = xsp; + if (jj_scan_token(101)) + return true; + } } return false; } - private boolean jj_3R_52(){ + private boolean jj_3R_110(){ + if (jj_scan_token(LEFT)) + return true; + return false; + } + + private boolean jj_3R_70(){ Token xsp; xsp = jj_scanpos; - if (jj_3R_94()){ + if (jj_3R_110()){ jj_scanpos = xsp; - if (jj_3R_95()){ + if (jj_3R_111()){ jj_scanpos = xsp; - if (jj_3R_96()){ - jj_scanpos = xsp; - if (jj_3R_97()){ - jj_scanpos = xsp; - if (jj_3R_98()) - return true; - } - } + if (jj_3R_112()) + return true; } } + xsp = jj_scanpos; + if (jj_scan_token(25)) + jj_scanpos = xsp; return false; } - private boolean jj_3R_146(){ - if (jj_scan_token(COMMA)) + private boolean jj_3R_159(){ + if (jj_scan_token(COUNT)) + return true; + if (jj_scan_token(LEFT_PAR)) return true; - if (jj_3R_41()) + Token xsp; + xsp = jj_scanpos; + if (jj_scan_token(19)) + jj_scanpos = xsp; + xsp = jj_scanpos; + if (jj_scan_token(10)){ + jj_scanpos = xsp; + if (jj_3R_183()) + return true; + } + if (jj_scan_token(RIGHT_PAR)) return true; return false; } - private boolean jj_3R_178(){ + private boolean jj_3R_49(){ + Token xsp; + xsp = jj_scanpos; + if (jj_scan_token(24)){ + jj_scanpos = xsp; + if (jj_3R_70()) + return true; + } + return false; + } + + private boolean jj_3R_200(){ if (jj_scan_token(USING)) return true; if (jj_scan_token(LEFT_PAR)) @@ -4140,7 +4161,7 @@ public class ADQLParser implements ADQLParserConstants { Token xsp; while(true){ xsp = jj_scanpos; - if (jj_3R_179()){ + if (jj_3R_201()){ jj_scanpos = xsp; break; } @@ -4150,29 +4171,19 @@ public class ADQLParser implements ADQLParserConstants { return false; } - private boolean jj_3R_133(){ + private boolean jj_3R_37(){ if (jj_scan_token(STRING_LITERAL)) return true; return false; } - private boolean jj_3R_158(){ - if (jj_3R_102()) - return true; - if (jj_scan_token(COMMA)) - return true; - if (jj_3R_102()) - return true; - return false; - } - - private boolean jj_3R_116(){ + private boolean jj_3R_22(){ Token xsp; - if (jj_3R_133()) + if (jj_3R_37()) return true; while(true){ xsp = jj_scanpos; - if (jj_3R_133()){ + if (jj_3R_37()){ jj_scanpos = xsp; break; } @@ -4180,46 +4191,20 @@ public class ADQLParser implements ADQLParserConstants { return false; } - private boolean jj_3R_135(){ - Token xsp; - xsp = jj_scanpos; - if (jj_scan_token(47)){ - jj_scanpos = xsp; - if (jj_scan_token(48)){ - jj_scanpos = xsp; - if (jj_scan_token(49)){ - jj_scanpos = xsp; - if (jj_scan_token(50)) - return true; - } - } - } - if (jj_scan_token(LEFT_PAR)) - return true; - xsp = jj_scanpos; - if (jj_scan_token(19)) - jj_scanpos = xsp; - if (jj_3R_41()) - return true; - if (jj_scan_token(RIGHT_PAR)) - return true; - return false; - } - - private boolean jj_3R_107(){ + private boolean jj_3R_113(){ if (jj_scan_token(LEFT)) return true; return false; } - private boolean jj_3R_62(){ + private boolean jj_3R_71(){ Token xsp; xsp = jj_scanpos; - if (jj_3R_107()){ + if (jj_3R_113()){ jj_scanpos = xsp; - if (jj_3R_108()){ + if (jj_3R_114()){ jj_scanpos = xsp; - if (jj_3R_109()) + if (jj_3R_115()) return true; } } @@ -4229,83 +4214,103 @@ public class ADQLParser implements ADQLParserConstants { return false; } - private boolean jj_3R_177(){ + private boolean jj_3R_199(){ if (jj_scan_token(ON)) return true; - if (jj_3R_152()) + if (jj_3R_163()) return true; return false; } - private boolean jj_3R_134(){ - if (jj_scan_token(COUNT)) - return true; - if (jj_scan_token(LEFT_PAR)) - return true; + private boolean jj_3R_143(){ Token xsp; xsp = jj_scanpos; - if (jj_scan_token(19)) - jj_scanpos = xsp; - xsp = jj_scanpos; - if (jj_scan_token(10)){ + if (jj_3R_159()){ jj_scanpos = xsp; - if (jj_3R_149()) + if (jj_3R_160()) return true; } - if (jj_scan_token(RIGHT_PAR)) - return true; return false; } - private boolean jj_3R_46(){ + private boolean jj_3R_50(){ Token xsp; xsp = jj_scanpos; if (jj_scan_token(24)){ jj_scanpos = xsp; - if (jj_3R_62()) + if (jj_3R_71()) return true; } return false; } - private boolean jj_3R_30(){ + private boolean jj_3R_33(){ Token xsp; xsp = jj_scanpos; - if (jj_3R_46()) + if (jj_3R_50()) jj_scanpos = xsp; if (jj_scan_token(JOIN)) return true; - if (jj_3R_47()) + if (jj_3R_51()) return true; xsp = jj_scanpos; - if (jj_3R_177()){ + if (jj_3R_199()){ jj_scanpos = xsp; - if (jj_3R_178()) + if (jj_3R_200()) return true; } return false; } - private boolean jj_3R_29(){ + private boolean jj_3R_32(){ if (jj_scan_token(NATURAL)) return true; Token xsp; xsp = jj_scanpos; - if (jj_3R_45()) + if (jj_3R_49()) jj_scanpos = xsp; if (jj_scan_token(JOIN)) return true; - if (jj_3R_47()) + if (jj_3R_51()) return true; return false; } - private boolean jj_3R_117(){ + private boolean jj_3R_134(){ + if (jj_scan_token(LEFT_PAR)) + return true; + if (jj_3R_46()) + return true; + Token xsp; + while(true){ + xsp = jj_scanpos; + if (jj_3R_157()){ + jj_scanpos = xsp; + break; + } + } + if (jj_scan_token(RIGHT_PAR)) + return true; + return false; + } + + private boolean jj_3_16(){ + if (jj_3R_16()) + return true; + return false; + } + + private boolean jj_3R_107(){ Token xsp; xsp = jj_scanpos; - if (jj_3R_134()){ + if (jj_scan_token(35)) + jj_scanpos = xsp; + if (jj_scan_token(IN)) + return true; + xsp = jj_scanpos; + if (jj_3_16()){ jj_scanpos = xsp; - if (jj_3R_135()) + if (jj_3R_134()) return true; } return false; @@ -4314,21 +4319,21 @@ public class ADQLParser implements ADQLParserConstants { private boolean jj_3R_17(){ Token xsp; xsp = jj_scanpos; - if (jj_3R_29()){ + if (jj_3R_32()){ jj_scanpos = xsp; - if (jj_3R_30()) + if (jj_3R_33()) return true; } return false; } - private boolean jj_3R_176(){ + private boolean jj_3R_198(){ if (jj_3R_17()) return true; return false; } - private boolean jj_3R_171(){ + private boolean jj_3R_192(){ Token xsp; xsp = jj_scanpos; if (jj_scan_token(22)) @@ -4338,15 +4343,15 @@ public class ADQLParser implements ADQLParserConstants { return false; } - private boolean jj_3R_172(){ - if (jj_3R_63()) + private boolean jj_3R_193(){ + if (jj_3R_72()) return true; Token xsp; - if (jj_3R_176()) + if (jj_3R_198()) return true; while(true){ xsp = jj_scanpos; - if (jj_3R_176()){ + if (jj_3R_198()){ jj_scanpos = xsp; break; } @@ -4354,10 +4359,10 @@ public class ADQLParser implements ADQLParserConstants { return false; } - private boolean jj_3R_111(){ + private boolean jj_3R_117(){ if (jj_scan_token(LEFT_PAR)) return true; - if (jj_3R_172()) + if (jj_3R_193()) return true; if (jj_scan_token(RIGHT_PAR)) return true; @@ -4370,25 +4375,23 @@ public class ADQLParser implements ADQLParserConstants { return false; } - private boolean jj_3R_123(){ - if (jj_scan_token(LEFT_PAR)) + private boolean jj_3R_26(){ + Token xsp; + xsp = jj_scanpos; + if (jj_scan_token(35)) + jj_scanpos = xsp; + if (jj_scan_token(BETWEEN)) return true; - if (jj_3R_41()) + if (jj_3R_46()) return true; - Token xsp; - while(true){ - xsp = jj_scanpos; - if (jj_3R_146()){ - jj_scanpos = xsp; - break; - } - } - if (jj_scan_token(RIGHT_PAR)) + if (jj_scan_token(AND)) + return true; + if (jj_3R_46()) return true; return false; } - private boolean jj_3R_169(){ + private boolean jj_3R_190(){ Token xsp; xsp = jj_scanpos; if (jj_scan_token(45)){ @@ -4399,8 +4402,8 @@ public class ADQLParser implements ADQLParserConstants { return false; } - private boolean jj_3R_47(){ - if (jj_3R_63()) + private boolean jj_3R_51(){ + if (jj_3R_72()) return true; Token xsp; while(true){ @@ -4413,85 +4416,25 @@ public class ADQLParser implements ADQLParserConstants { return false; } - private boolean jj_3_13(){ - if (jj_3R_16()) - return true; - return false; - } - - private boolean jj_3R_100(){ - Token xsp; - xsp = jj_scanpos; - if (jj_scan_token(35)) - jj_scanpos = xsp; - if (jj_scan_token(IN)) - return true; - xsp = jj_scanpos; - if (jj_3_13()){ - jj_scanpos = xsp; - if (jj_3R_123()) - return true; - } - return false; - } - - private boolean jj_3_2(){ - if (jj_3R_16()) - return true; - Token xsp; - xsp = jj_scanpos; - if (jj_scan_token(22)) - jj_scanpos = xsp; - if (jj_3R_14()) - return true; - return false; - } - - private boolean jj_3R_110(){ - if (jj_3R_103()) - return true; - Token xsp; - xsp = jj_scanpos; - if (jj_3R_171()) - jj_scanpos = xsp; - return false; - } - - private boolean jj_3R_22(){ - Token xsp; - xsp = jj_scanpos; - if (jj_scan_token(35)) - jj_scanpos = xsp; - if (jj_scan_token(BETWEEN)) - return true; - if (jj_3R_41()) - return true; - if (jj_scan_token(AND)) - return true; - if (jj_3R_41()) - return true; - return false; - } - - private boolean jj_3R_54(){ - if (jj_3R_100()) + private boolean jj_3R_61(){ + if (jj_3R_107()) return true; return false; } - private boolean jj_3_10(){ - if (jj_3R_22()) + private boolean jj_3_13(){ + if (jj_3R_26()) return true; return false; } - private boolean jj_3R_53(){ - if (jj_3R_99()) + private boolean jj_3R_60(){ + if (jj_3R_106()) return true; return false; } - private boolean jj_3R_99(){ + private boolean jj_3R_106(){ Token xsp; xsp = jj_scanpos; if (jj_scan_token(12)){ @@ -4511,13 +4454,13 @@ public class ADQLParser implements ADQLParserConstants { } } } - if (jj_3R_41()) + if (jj_3R_46()) return true; return false; } - private boolean jj_3_12(){ - if (jj_3R_24()) + private boolean jj_3_15(){ + if (jj_3R_27()) return true; Token xsp; xsp = jj_scanpos; @@ -4528,46 +4471,54 @@ public class ADQLParser implements ADQLParserConstants { return false; } - private boolean jj_3R_63(){ + private boolean jj_3_2(){ + if (jj_3R_16()) + return true; Token xsp; xsp = jj_scanpos; - if (jj_3R_110()){ + if (jj_scan_token(22)) jj_scanpos = xsp; - if (jj_3_2()){ - jj_scanpos = xsp; - if (jj_3R_111()) - return true; - } - } + if (jj_3R_14()) + return true; return false; } - private boolean jj_3R_40(){ - if (jj_3R_41()) + private boolean jj_3R_116(){ + if (jj_3R_77()) + return true; + Token xsp; + xsp = jj_scanpos; + if (jj_3R_192()) + jj_scanpos = xsp; + return false; + } + + private boolean jj_3R_45(){ + if (jj_3R_46()) return true; Token xsp; xsp = jj_scanpos; - if (jj_3R_53()){ + if (jj_3R_60()){ jj_scanpos = xsp; - if (jj_3_10()){ + if (jj_3_13()){ jj_scanpos = xsp; - if (jj_3R_54()) + if (jj_3R_61()) return true; } } return false; } - private boolean jj_3_11(){ - if (jj_3R_23()) + private boolean jj_3_14(){ + if (jj_3R_21()) return true; if (jj_scan_token(IS)) return true; return false; } - private boolean jj_3R_39(){ - if (jj_3R_24()) + private boolean jj_3R_44(){ + if (jj_3R_27()) return true; Token xsp; xsp = jj_scanpos; @@ -4575,13 +4526,13 @@ public class ADQLParser implements ADQLParserConstants { jj_scanpos = xsp; if (jj_scan_token(LIKE)) return true; - if (jj_3R_24()) + if (jj_3R_27()) return true; return false; } - private boolean jj_3R_38(){ - if (jj_3R_23()) + private boolean jj_3R_43(){ + if (jj_3R_21()) return true; if (jj_scan_token(IS)) return true; @@ -4594,35 +4545,21 @@ public class ADQLParser implements ADQLParserConstants { return false; } - private boolean jj_3R_168(){ - if (jj_3R_42()) - return true; - return false; - } - - private boolean jj_3R_114(){ - if (jj_scan_token(COMMA)) - return true; - if (jj_3R_41()) - return true; - return false; - } - - private boolean jj_3R_155(){ + private boolean jj_3R_72(){ Token xsp; xsp = jj_scanpos; - if (jj_3R_168()){ + if (jj_3R_116()){ jj_scanpos = xsp; - if (jj_scan_token(101)) - return true; + if (jj_3_2()){ + jj_scanpos = xsp; + if (jj_3R_117()) + return true; + } } - xsp = jj_scanpos; - if (jj_3R_169()) - jj_scanpos = xsp; return false; } - private boolean jj_3R_37(){ + private boolean jj_3R_42(){ if (jj_scan_token(EXISTS)) return true; if (jj_3R_16()) @@ -4630,33 +4567,24 @@ public class ADQLParser implements ADQLParserConstants { return false; } - private boolean jj_3R_167(){ - if (jj_3R_42()) + private boolean jj_3R_146(){ + if (jj_scan_token(COMMA)) + return true; + if (jj_3R_46()) return true; return false; } - private boolean jj_3R_153(){ - Token xsp; - xsp = jj_scanpos; - if (jj_3R_167()){ - jj_scanpos = xsp; - if (jj_scan_token(101)) - return true; - } - return false; - } - - private boolean jj_3R_21(){ + private boolean jj_3R_25(){ Token xsp; xsp = jj_scanpos; - if (jj_3R_37()){ + if (jj_3R_42()){ jj_scanpos = xsp; - if (jj_3R_38()){ + if (jj_3R_43()){ jj_scanpos = xsp; - if (jj_3R_39()){ + if (jj_3R_44()){ jj_scanpos = xsp; - if (jj_3R_40()) + if (jj_3R_45()) return true; } } @@ -4664,100 +4592,111 @@ public class ADQLParser implements ADQLParserConstants { return false; } - private boolean jj_3R_60(){ - if (jj_scan_token(DOT)) - return true; - if (jj_3R_103()) + private boolean jj_3R_189(){ + if (jj_3R_36()) return true; return false; } - private boolean jj_3_9(){ - if (jj_3R_21()) - return true; + private boolean jj_3R_166(){ + Token xsp; + xsp = jj_scanpos; + if (jj_3R_189()){ + jj_scanpos = xsp; + if (jj_scan_token(101)) + return true; + } + xsp = jj_scanpos; + if (jj_3R_190()) + jj_scanpos = xsp; return false; } - private boolean jj_3R_23(){ - if (jj_3R_42()) + private boolean jj_3_12(){ + if (jj_3R_25()) return true; return false; } - private boolean jj_3R_174(){ + private boolean jj_3R_195(){ if (jj_scan_token(LEFT_PAR)) return true; - if (jj_3R_152()) + if (jj_3R_163()) return true; if (jj_scan_token(RIGHT_PAR)) return true; return false; } - private boolean jj_3R_173(){ - if (jj_3R_21()) + private boolean jj_3R_194(){ + if (jj_3R_25()) return true; return false; } - private boolean jj_3R_165(){ + private boolean jj_3R_188(){ + if (jj_3R_36()) + return true; + return false; + } + + private boolean jj_3R_186(){ Token xsp; xsp = jj_scanpos; - if (jj_3R_173()){ + if (jj_3R_194()){ jj_scanpos = xsp; - if (jj_3R_174()) + if (jj_3R_195()) return true; } return false; } - private boolean jj_3R_42(){ - if (jj_3R_14()) - return true; + private boolean jj_3R_164(){ Token xsp; xsp = jj_scanpos; - if (jj_3R_60()) + if (jj_3R_188()){ jj_scanpos = xsp; - return false; - } - - private boolean jj_3R_65(){ - if (jj_3R_41()) - return true; - Token xsp; - while(true){ - xsp = jj_scanpos; - if (jj_3R_114()){ - jj_scanpos = xsp; - break; - } + if (jj_scan_token(101)) + return true; } return false; } - private boolean jj_3R_132(){ + private boolean jj_3R_56(){ if (jj_scan_token(DOT)) return true; - if (jj_3R_14()) + if (jj_3R_77()) return true; return false; } - private boolean jj_3R_131(){ - if (jj_scan_token(DOT)) + private boolean jj_3R_21(){ + if (jj_3R_36()) return true; - if (jj_3R_14()) + return false; + } + + private boolean jj_3R_123(){ + if (jj_3R_46()) return true; + Token xsp; + while(true){ + xsp = jj_scanpos; + if (jj_3R_146()){ + jj_scanpos = xsp; + break; + } + } return false; } - private boolean jj_3R_175(){ + private boolean jj_3R_196(){ if (jj_scan_token(NOT)) return true; return false; } - private boolean jj_3R_166(){ + private boolean jj_3R_187(){ Token xsp; xsp = jj_scanpos; if (jj_scan_token(33)){ @@ -4766,14 +4705,14 @@ public class ADQLParser implements ADQLParserConstants { return true; } xsp = jj_scanpos; - if (jj_3R_175()) + if (jj_3R_196()) jj_scanpos = xsp; - if (jj_3R_165()) + if (jj_3R_186()) return true; return false; } - private boolean jj_3R_182(){ + private boolean jj_3R_174(){ if (jj_scan_token(COMMA)) return true; if (jj_3R_191()) @@ -4781,20 +4720,25 @@ public class ADQLParser implements ADQLParserConstants { return false; } - private boolean jj_3R_103(){ + private boolean jj_3R_36(){ if (jj_3R_14()) return true; Token xsp; xsp = jj_scanpos; - if (jj_3R_131()) - jj_scanpos = xsp; - xsp = jj_scanpos; - if (jj_3R_132()) + if (jj_3R_56()) jj_scanpos = xsp; return false; } - private boolean jj_3R_181(){ + private boolean jj_3R_128(){ + if (jj_scan_token(DOT)) + return true; + if (jj_3R_14()) + return true; + return false; + } + + private boolean jj_3R_173(){ if (jj_scan_token(COMMA)) return true; if (jj_3R_191()) @@ -4802,66 +4746,61 @@ public class ADQLParser implements ADQLParserConstants { return false; } - private boolean jj_3R_26(){ - if (jj_scan_token(DELIMITED_IDENTIFIER)) + private boolean jj_3R_127(){ + if (jj_scan_token(DOT)) + return true; + if (jj_3R_14()) return true; return false; } - private boolean jj_3R_25(){ - if (jj_scan_token(REGULAR_IDENTIFIER)) + private boolean jj_3R_185(){ + if (jj_scan_token(NOT)) return true; return false; } - private boolean jj_3R_14(){ + private boolean jj_3R_77(){ + if (jj_3R_14()) + return true; Token xsp; xsp = jj_scanpos; - if (jj_3R_25()){ + if (jj_3R_127()) + jj_scanpos = xsp; + xsp = jj_scanpos; + if (jj_3R_128()) jj_scanpos = xsp; - if (jj_3R_26()) - return true; - } return false; } - private boolean jj_3R_164(){ - if (jj_scan_token(NOT)) + private boolean jj_3R_29(){ + if (jj_scan_token(DELIMITED_IDENTIFIER)) return true; return false; } - private boolean jj_3R_156(){ - if (jj_scan_token(COMMA)) - return true; - if (jj_3R_155()) + private boolean jj_3R_28(){ + if (jj_scan_token(REGULAR_IDENTIFIER)) return true; return false; } - private boolean jj_3R_20(){ - if (jj_scan_token(REGULAR_IDENTIFIER)) - return true; - if (jj_scan_token(LEFT_PAR)) - return true; - Token xsp; - xsp = jj_scanpos; - if (jj_3R_65()) - jj_scanpos = xsp; - if (jj_scan_token(RIGHT_PAR)) + private boolean jj_3R_145(){ + if (jj_3R_109()) return true; return false; } - private boolean jj_3R_144(){ - if (jj_scan_token(ORDER_BY)) - return true; - if (jj_3R_155()) - return true; + private boolean jj_3R_163(){ Token xsp; + xsp = jj_scanpos; + if (jj_3R_185()) + jj_scanpos = xsp; + if (jj_3R_186()) + return true; while(true){ xsp = jj_scanpos; - if (jj_3R_156()){ + if (jj_3R_187()){ jj_scanpos = xsp; break; } @@ -4869,60 +4808,60 @@ public class ADQLParser implements ADQLParserConstants { return false; } - private boolean jj_3R_113(){ - if (jj_3R_101()) + private boolean jj_3R_24(){ + if (jj_scan_token(REGULAR_IDENTIFIER)) + return true; + if (jj_scan_token(LEFT_PAR)) + return true; + Token xsp; + xsp = jj_scanpos; + if (jj_3R_123()) + jj_scanpos = xsp; + if (jj_scan_token(RIGHT_PAR)) return true; return false; } - private boolean jj_3R_152(){ + private boolean jj_3R_14(){ Token xsp; xsp = jj_scanpos; - if (jj_3R_164()) + if (jj_3R_28()){ jj_scanpos = xsp; - if (jj_3R_165()) - return true; - while(true){ - xsp = jj_scanpos; - if (jj_3R_166()){ - jj_scanpos = xsp; - break; - } + if (jj_3R_29()) + return true; } return false; } - private boolean jj_3R_180(){ - if (jj_3R_102()) + private boolean jj_3R_172(){ + if (jj_3R_108()) return true; return false; } - private boolean jj_3R_143(){ - if (jj_scan_token(HAVING)) + private boolean jj_3R_167(){ + if (jj_scan_token(COMMA)) return true; - if (jj_3R_152()) + if (jj_3R_166()) return true; return false; } - private boolean jj_3R_154(){ - if (jj_scan_token(COMMA)) - return true; - if (jj_3R_153()) + private boolean jj_3R_144(){ + if (jj_3R_21()) return true; return false; } - private boolean jj_3R_142(){ - if (jj_scan_token(GROUP_BY)) + private boolean jj_3R_155(){ + if (jj_scan_token(ORDER_BY)) return true; - if (jj_3R_153()) + if (jj_3R_166()) return true; Token xsp; while(true){ xsp = jj_scanpos; - if (jj_3R_154()){ + if (jj_3R_167()){ jj_scanpos = xsp; break; } @@ -4930,161 +4869,181 @@ public class ADQLParser implements ADQLParserConstants { return false; } - private boolean jj_3R_93(){ + private boolean jj_3R_122(){ + Token xsp; + xsp = jj_scanpos; + if (jj_3R_144()){ + jj_scanpos = xsp; + if (jj_3R_145()) + return true; + } + return false; + } + + private boolean jj_3R_100(){ if (jj_scan_token(TAN)) return true; if (jj_scan_token(LEFT_PAR)) return true; - if (jj_3R_102()) + if (jj_3R_108()) return true; if (jj_scan_token(RIGHT_PAR)) return true; return false; } - private boolean jj_3R_92(){ + private boolean jj_3R_99(){ if (jj_scan_token(SIN)) return true; if (jj_scan_token(LEFT_PAR)) return true; - if (jj_3R_102()) + if (jj_3R_108()) return true; if (jj_scan_token(RIGHT_PAR)) return true; return false; } - private boolean jj_3R_91(){ + private boolean jj_3R_55(){ + if (jj_3R_76()) + return true; + return false; + } + + private boolean jj_3R_98(){ if (jj_scan_token(COT)) return true; if (jj_scan_token(LEFT_PAR)) return true; - if (jj_3R_102()) + if (jj_3R_108()) return true; if (jj_scan_token(RIGHT_PAR)) return true; return false; } - private boolean jj_3R_112(){ - if (jj_3R_23()) + private boolean jj_3_11(){ + if (jj_3R_24()) return true; return false; } - private boolean jj_3R_90(){ + private boolean jj_3R_97(){ if (jj_scan_token(COS)) return true; if (jj_scan_token(LEFT_PAR)) return true; - if (jj_3R_102()) + if (jj_3R_108()) return true; if (jj_scan_token(RIGHT_PAR)) return true; return false; } - private boolean jj_3R_64(){ - Token xsp; - xsp = jj_scanpos; - if (jj_3R_112()){ - jj_scanpos = xsp; - if (jj_3R_113()) - return true; - } - return false; - } - - private boolean jj_3R_89(){ + private boolean jj_3R_96(){ if (jj_scan_token(ATAN2)) return true; if (jj_scan_token(LEFT_PAR)) return true; - if (jj_3R_102()) + if (jj_3R_108()) return true; if (jj_scan_token(COMMA)) return true; - if (jj_3R_102()) + if (jj_3R_108()) return true; if (jj_scan_token(RIGHT_PAR)) return true; return false; } - private boolean jj_3R_88(){ + private boolean jj_3R_54(){ + if (jj_3R_75()) + return true; + return false; + } + + private boolean jj_3R_95(){ if (jj_scan_token(ATAN)) return true; if (jj_scan_token(LEFT_PAR)) return true; - if (jj_3R_102()) + if (jj_3R_108()) return true; if (jj_scan_token(RIGHT_PAR)) return true; return false; } - private boolean jj_3R_87(){ + private boolean jj_3R_35(){ + Token xsp; + xsp = jj_scanpos; + if (jj_3R_54()){ + jj_scanpos = xsp; + if (jj_3_11()){ + jj_scanpos = xsp; + if (jj_3R_55()) + return true; + } + } + return false; + } + + private boolean jj_3R_94(){ if (jj_scan_token(ASIN)) return true; if (jj_scan_token(LEFT_PAR)) return true; - if (jj_3R_102()) + if (jj_3R_108()) return true; if (jj_scan_token(RIGHT_PAR)) return true; return false; } - private boolean jj_3R_32(){ - if (jj_3R_49()) - return true; - return false; - } - - private boolean jj_3R_141(){ - if (jj_scan_token(WHERE)) + private boolean jj_3R_154(){ + if (jj_scan_token(HAVING)) return true; - if (jj_3R_152()) + if (jj_3R_163()) return true; return false; } - private boolean jj_3_8(){ - if (jj_3R_20()) + private boolean jj_3R_53(){ + if (jj_3R_74()) return true; return false; } - private boolean jj_3R_86(){ + private boolean jj_3R_93(){ if (jj_scan_token(ACOS)) return true; if (jj_scan_token(LEFT_PAR)) return true; - if (jj_3R_102()) + if (jj_3R_108()) return true; if (jj_scan_token(RIGHT_PAR)) return true; return false; } - private boolean jj_3R_51(){ + private boolean jj_3R_58(){ Token xsp; xsp = jj_scanpos; - if (jj_3R_86()){ + if (jj_3R_93()){ jj_scanpos = xsp; - if (jj_3R_87()){ + if (jj_3R_94()){ jj_scanpos = xsp; - if (jj_3R_88()){ + if (jj_3R_95()){ jj_scanpos = xsp; - if (jj_3R_89()){ + if (jj_3R_96()){ jj_scanpos = xsp; - if (jj_3R_90()){ + if (jj_3R_97()){ jj_scanpos = xsp; - if (jj_3R_91()){ + if (jj_3R_98()){ jj_scanpos = xsp; - if (jj_3R_92()){ + if (jj_3R_99()){ jj_scanpos = xsp; - if (jj_3R_93()) + if (jj_3R_100()) return true; } } @@ -5096,135 +5055,117 @@ public class ADQLParser implements ADQLParserConstants { return false; } - private boolean jj_3R_31(){ - if (jj_3R_48()) + private boolean jj_3R_165(){ + if (jj_scan_token(COMMA)) + return true; + if (jj_3R_164()) return true; return false; } - private boolean jj_3R_18(){ + private boolean jj_3R_153(){ + if (jj_scan_token(GROUP_BY)) + return true; + if (jj_3R_164()) + return true; Token xsp; - xsp = jj_scanpos; - if (jj_3R_31()){ - jj_scanpos = xsp; - if (jj_3_8()){ + while(true){ + xsp = jj_scanpos; + if (jj_3R_165()){ jj_scanpos = xsp; - if (jj_3R_32()) - return true; + break; } } return false; } - private boolean jj_3R_162(){ - if (jj_3R_49()) - return true; - return false; - } - - private boolean jj_3R_163(){ - if (jj_scan_token(AS)) - return true; - if (jj_3R_14()) - return true; - return false; - } - - private boolean jj_3R_140(){ - if (jj_scan_token(COMMA)) - return true; - if (jj_3R_47()) - return true; - return false; - } - - private boolean jj_3R_85(){ + private boolean jj_3R_92(){ if (jj_scan_token(TRUNCATE)) return true; if (jj_scan_token(LEFT_PAR)) return true; - if (jj_3R_102()) + if (jj_3R_108()) return true; Token xsp; xsp = jj_scanpos; - if (jj_3R_182()) + if (jj_3R_174()) jj_scanpos = xsp; if (jj_scan_token(RIGHT_PAR)) return true; return false; } - private boolean jj_3R_84(){ + private boolean jj_3R_91(){ if (jj_scan_token(SQRT)) return true; if (jj_scan_token(LEFT_PAR)) return true; - if (jj_3R_102()) + if (jj_3R_108()) return true; if (jj_scan_token(RIGHT_PAR)) return true; return false; } - private boolean jj_3R_83(){ + private boolean jj_3R_90(){ if (jj_scan_token(ROUND)) return true; if (jj_scan_token(LEFT_PAR)) return true; - if (jj_3R_102()) + if (jj_3R_108()) return true; Token xsp; xsp = jj_scanpos; - if (jj_3R_181()) + if (jj_3R_173()) jj_scanpos = xsp; if (jj_scan_token(RIGHT_PAR)) return true; return false; } - private boolean jj_3R_82(){ + private boolean jj_3R_89(){ if (jj_scan_token(RAND)) return true; if (jj_scan_token(LEFT_PAR)) return true; Token xsp; xsp = jj_scanpos; - if (jj_3R_180()) + if (jj_3R_172()) jj_scanpos = xsp; if (jj_scan_token(RIGHT_PAR)) return true; return false; } - private boolean jj_3R_81(){ + private boolean jj_3R_88(){ if (jj_scan_token(RADIANS)) return true; if (jj_scan_token(LEFT_PAR)) return true; - if (jj_3R_102()) + if (jj_3R_108()) return true; if (jj_scan_token(RIGHT_PAR)) return true; return false; } - private boolean jj_3R_80(){ + private boolean jj_3R_87(){ if (jj_scan_token(POWER)) return true; if (jj_scan_token(LEFT_PAR)) return true; - if (jj_3R_102()) + if (jj_3R_108()) return true; if (jj_scan_token(COMMA)) return true; - if (jj_3R_102()) + if (jj_3R_108()) return true; if (jj_scan_token(RIGHT_PAR)) return true; return false; } - private boolean jj_3R_79(){ + private boolean jj_3R_86(){ if (jj_scan_token(PI)) return true; if (jj_scan_token(LEFT_PAR)) @@ -5234,167 +5175,167 @@ public class ADQLParser implements ADQLParserConstants { return false; } - private boolean jj_3R_78(){ + private boolean jj_3R_152(){ + if (jj_scan_token(WHERE)) + return true; + if (jj_3R_163()) + return true; + return false; + } + + private boolean jj_3R_85(){ if (jj_scan_token(MOD)) return true; if (jj_scan_token(LEFT_PAR)) return true; - if (jj_3R_102()) + if (jj_3R_108()) return true; if (jj_scan_token(COMMA)) return true; - if (jj_3R_102()) + if (jj_3R_108()) return true; if (jj_scan_token(RIGHT_PAR)) return true; return false; } - private boolean jj_3R_77(){ + private boolean jj_3R_84(){ if (jj_scan_token(LOG10)) return true; if (jj_scan_token(LEFT_PAR)) return true; - if (jj_3R_102()) + if (jj_3R_108()) return true; if (jj_scan_token(RIGHT_PAR)) return true; return false; } - private boolean jj_3R_76(){ - if (jj_scan_token(LOG)) - return true; - if (jj_scan_token(LEFT_PAR)) - return true; - if (jj_3R_102()) + private boolean jj_3R_47(){ + if (jj_scan_token(CONCAT)) return true; - if (jj_scan_token(RIGHT_PAR)) + if (jj_3R_35()) return true; return false; } - private boolean jj_3R_118(){ - if (jj_scan_token(FROM)) + private boolean jj_3R_83(){ + if (jj_scan_token(LOG)) return true; - if (jj_3R_47()) + if (jj_scan_token(LEFT_PAR)) + return true; + if (jj_3R_108()) + return true; + if (jj_scan_token(RIGHT_PAR)) return true; - Token xsp; - while(true){ - xsp = jj_scanpos; - if (jj_3R_140()){ - jj_scanpos = xsp; - break; - } - } return false; } - private boolean jj_3R_75(){ + private boolean jj_3R_82(){ if (jj_scan_token(FLOOR)) return true; if (jj_scan_token(LEFT_PAR)) return true; - if (jj_3R_102()) + if (jj_3R_108()) return true; if (jj_scan_token(RIGHT_PAR)) return true; return false; } - private boolean jj_3R_74(){ + private boolean jj_3R_81(){ if (jj_scan_token(EXP)) return true; if (jj_scan_token(LEFT_PAR)) return true; - if (jj_3R_102()) + if (jj_3R_108()) return true; if (jj_scan_token(RIGHT_PAR)) return true; return false; } - private boolean jj_3R_73(){ - if (jj_scan_token(DEGREES)) + private boolean jj_3R_184(){ + if (jj_scan_token(AS)) return true; - if (jj_scan_token(LEFT_PAR)) + if (jj_3R_14()) return true; - if (jj_3R_102()) + return false; + } + + private boolean jj_3R_151(){ + if (jj_scan_token(COMMA)) return true; - if (jj_scan_token(RIGHT_PAR)) + if (jj_3R_51()) return true; return false; } - private boolean jj_3R_72(){ - if (jj_scan_token(CEILING)) + private boolean jj_3R_80(){ + if (jj_scan_token(DEGREES)) return true; if (jj_scan_token(LEFT_PAR)) return true; - if (jj_3R_102()) + if (jj_3R_108()) return true; if (jj_scan_token(RIGHT_PAR)) return true; return false; } - private boolean jj_3R_27(){ - if (jj_3R_14()) + private boolean jj_3R_79(){ + if (jj_scan_token(CEILING)) return true; - if (jj_scan_token(DOT)) + if (jj_scan_token(LEFT_PAR)) return true; - return false; - } - - private boolean jj_3R_43(){ - if (jj_scan_token(CONCAT)) + if (jj_3R_108()) return true; - if (jj_3R_18()) + if (jj_scan_token(RIGHT_PAR)) return true; return false; } - private boolean jj_3R_71(){ + private boolean jj_3R_78(){ if (jj_scan_token(ABS)) return true; if (jj_scan_token(LEFT_PAR)) return true; - if (jj_3R_102()) + if (jj_3R_108()) return true; if (jj_scan_token(RIGHT_PAR)) return true; return false; } - private boolean jj_3R_151(){ - if (jj_3R_41()) + private boolean jj_3R_27(){ + if (jj_3R_35()) return true; Token xsp; - xsp = jj_scanpos; - if (jj_3R_163()) - jj_scanpos = xsp; + while(true){ + xsp = jj_scanpos; + if (jj_3R_47()){ + jj_scanpos = xsp; + break; + } + } return false; } - private boolean jj_3R_15(){ - if (jj_3R_14()) - return true; - if (jj_scan_token(DOT)) + private boolean jj_3R_73(){ + if (jj_scan_token(MINUS)) return true; - Token xsp; - xsp = jj_scanpos; - if (jj_3R_27()) - jj_scanpos = xsp; return false; } - private boolean jj_3R_24(){ - if (jj_3R_18()) + private boolean jj_3R_129(){ + if (jj_scan_token(FROM)) + return true; + if (jj_3R_51()) return true; Token xsp; while(true){ xsp = jj_scanpos; - if (jj_3R_43()){ + if (jj_3R_151()){ jj_scanpos = xsp; break; } @@ -5402,38 +5343,38 @@ public class ADQLParser implements ADQLParserConstants { return false; } - private boolean jj_3R_50(){ + private boolean jj_3R_57(){ Token xsp; xsp = jj_scanpos; - if (jj_3R_71()){ + if (jj_3R_78()){ jj_scanpos = xsp; - if (jj_3R_72()){ + if (jj_3R_79()){ jj_scanpos = xsp; - if (jj_3R_73()){ + if (jj_3R_80()){ jj_scanpos = xsp; - if (jj_3R_74()){ + if (jj_3R_81()){ jj_scanpos = xsp; - if (jj_3R_75()){ + if (jj_3R_82()){ jj_scanpos = xsp; - if (jj_3R_76()){ + if (jj_3R_83()){ jj_scanpos = xsp; - if (jj_3R_77()){ + if (jj_3R_84()){ jj_scanpos = xsp; - if (jj_3R_78()){ + if (jj_3R_85()){ jj_scanpos = xsp; - if (jj_3R_79()){ + if (jj_3R_86()){ jj_scanpos = xsp; - if (jj_3R_80()){ + if (jj_3R_87()){ jj_scanpos = xsp; - if (jj_3R_81()){ + if (jj_3R_88()){ jj_scanpos = xsp; - if (jj_3R_82()){ + if (jj_3R_89()){ jj_scanpos = xsp; - if (jj_3R_83()){ + if (jj_3R_90()){ jj_scanpos = xsp; - if (jj_3R_84()){ + if (jj_3R_91()){ jj_scanpos = xsp; - if (jj_3R_85()) + if (jj_3R_92()) return true; } } @@ -5452,46 +5393,65 @@ public class ADQLParser implements ADQLParserConstants { return false; } - private boolean jj_3R_170(){ - if (jj_scan_token(MINUS)) + private boolean jj_3R_30(){ + if (jj_3R_14()) + return true; + if (jj_scan_token(DOT)) return true; return false; } - private boolean jj_3R_36(){ - if (jj_3R_20()) + private boolean jj_3R_41(){ + if (jj_3R_24()) return true; return false; } - private boolean jj_3R_35(){ - if (jj_3R_52()) + private boolean jj_3_10(){ + if (jj_3R_23()) return true; return false; } - private boolean jj_3R_34(){ - if (jj_3R_51()) + private boolean jj_3R_40(){ + if (jj_3R_59()) return true; return false; } - private boolean jj_3R_33(){ - if (jj_3R_50()) + private boolean jj_3R_52(){ + Token xsp; + xsp = jj_scanpos; + if (jj_scan_token(8)){ + jj_scanpos = xsp; + if (jj_3R_73()) + return true; + } + return false; + } + + private boolean jj_3R_39(){ + if (jj_3R_58()) return true; return false; } - private boolean jj_3R_19(){ + private boolean jj_3R_38(){ + if (jj_3R_57()) + return true; + return false; + } + + private boolean jj_3R_23(){ Token xsp; xsp = jj_scanpos; - if (jj_3R_33()){ + if (jj_3R_38()){ jj_scanpos = xsp; - if (jj_3R_34()){ + if (jj_3R_39()){ jj_scanpos = xsp; - if (jj_3R_35()){ + if (jj_3R_40()){ jj_scanpos = xsp; - if (jj_3R_36()) + if (jj_3R_41()) return true; } } @@ -5499,24 +5459,29 @@ public class ADQLParser implements ADQLParserConstants { return false; } - private boolean jj_3_7(){ - if (jj_3R_19()) + private boolean jj_3R_162(){ + if (jj_3R_46()) return true; + Token xsp; + xsp = jj_scanpos; + if (jj_3R_184()) + jj_scanpos = xsp; return false; } - private boolean jj_3R_161(){ + private boolean jj_3R_15(){ + if (jj_3R_14()) + return true; + if (jj_scan_token(DOT)) + return true; Token xsp; xsp = jj_scanpos; - if (jj_scan_token(8)){ + if (jj_3R_30()) jj_scanpos = xsp; - if (jj_3R_170()) - return true; - } return false; } - private boolean jj_3R_160(){ + private boolean jj_3R_168(){ Token xsp; xsp = jj_scanpos; if (jj_scan_token(10)){ @@ -5524,165 +5489,204 @@ public class ADQLParser implements ADQLParserConstants { if (jj_scan_token(11)) return true; } - if (jj_3R_130()) + if (jj_3R_135()) return true; return false; } - private boolean jj_3R_148(){ + private boolean jj_3R_34(){ Token xsp; xsp = jj_scanpos; - if (jj_3R_161()) + if (jj_3R_52()) jj_scanpos = xsp; xsp = jj_scanpos; - if (jj_3_7()){ + if (jj_3_10()){ jj_scanpos = xsp; - if (jj_3R_162()) + if (jj_3R_53()) return true; } return false; } - private boolean jj_3R_48(){ + private boolean jj_3R_75(){ if (jj_scan_token(COORDSYS)) return true; if (jj_scan_token(LEFT_PAR)) return true; - if (jj_3R_64()) + if (jj_3R_122()) return true; if (jj_scan_token(RIGHT_PAR)) return true; return false; } - private boolean jj_3R_150(){ - if (jj_scan_token(ASTERISK)) + private boolean jj_3R_156(){ + Token xsp; + xsp = jj_scanpos; + if (jj_scan_token(8)){ + jj_scanpos = xsp; + if (jj_scan_token(9)) + return true; + } + if (jj_3R_108()) return true; return false; } - private boolean jj_3_1(){ - if (jj_3R_14()) - return true; - if (jj_scan_token(DOT)) + private boolean jj_3R_19(){ + if (jj_3R_34()) return true; Token xsp; xsp = jj_scanpos; - if (jj_3R_15()) + if (jj_scan_token(8)){ jj_scanpos = xsp; - if (jj_scan_token(ASTERISK)) + if (jj_scan_token(9)){ + jj_scanpos = xsp; + if (jj_scan_token(10)){ + jj_scanpos = xsp; + if (jj_scan_token(11)) + return true; + } + } + } + return false; + } + + private boolean jj_3R_178(){ + if (jj_3R_21()) + return true; + return false; + } + + private boolean jj_3R_20(){ + if (jj_3R_35()) + return true; + if (jj_scan_token(CONCAT)) return true; return false; } - private boolean jj_3R_159(){ + private boolean jj_3R_176(){ + if (jj_3R_21()) + return true; + return false; + } + + private boolean jj_3R_171(){ if (jj_scan_token(COMMA)) return true; - if (jj_3R_158()) + if (jj_3R_170()) return true; return false; } - private boolean jj_3R_145(){ + private boolean jj_3R_135(){ + if (jj_3R_34()) + return true; Token xsp; xsp = jj_scanpos; - if (jj_scan_token(8)){ + if (jj_3R_168()) jj_scanpos = xsp; - if (jj_scan_token(9)) - return true; - } - if (jj_3R_102()) - return true; return false; } - private boolean jj_3R_138(){ - Token xsp; - xsp = jj_scanpos; - if (jj_3R_150()){ - jj_scanpos = xsp; - if (jj_3_1()){ - jj_scanpos = xsp; - if (jj_3R_151()) - return true; - } - } + private boolean jj_3R_158(){ + if (jj_scan_token(POINT)) + return true; + if (jj_scan_token(LEFT_PAR)) + return true; + if (jj_3R_169()) + return true; + if (jj_scan_token(COMMA)) + return true; + if (jj_3R_170()) + return true; + if (jj_scan_token(RIGHT_PAR)) + return true; return false; } - private boolean jj_3R_186(){ - if (jj_3R_23()) + private boolean jj_3_9(){ + if (jj_3R_22()) return true; return false; } - private boolean jj_3R_184(){ - if (jj_3R_23()) + private boolean jj_3R_161(){ + if (jj_scan_token(ASTERISK)) return true; return false; } - private boolean jj_3R_130(){ - if (jj_3R_148()) + private boolean jj_3_1(){ + if (jj_3R_14()) + return true; + if (jj_scan_token(DOT)) return true; Token xsp; xsp = jj_scanpos; - if (jj_3R_160()) + if (jj_3R_15()) jj_scanpos = xsp; + if (jj_scan_token(ASTERISK)) + return true; return false; } - private boolean jj_3R_147(){ - if (jj_scan_token(POINT)) - return true; - if (jj_scan_token(LEFT_PAR)) - return true; - if (jj_3R_157()) - return true; - if (jj_scan_token(COMMA)) - return true; - if (jj_3R_158()) - return true; - if (jj_scan_token(RIGHT_PAR)) + private boolean jj_3_8(){ + if (jj_3R_21()) return true; return false; } - private boolean jj_3R_129(){ + private boolean jj_3R_141(){ if (jj_scan_token(REGION)) return true; if (jj_scan_token(LEFT_PAR)) return true; - if (jj_3R_24()) + if (jj_3R_27()) return true; if (jj_scan_token(RIGHT_PAR)) return true; return false; } - private boolean jj_3R_139(){ - if (jj_scan_token(COMMA)) + private boolean jj_3_7(){ + if (jj_scan_token(REGULAR_IDENTIFIER)) return true; - if (jj_3R_138()) + if (jj_scan_token(LEFT_PAR)) return true; return false; } private boolean jj_3_6(){ - if (jj_3R_18()) - return true; - if (jj_scan_token(CONCAT)) + if (jj_scan_token(LEFT_PAR)) return true; return false; } private boolean jj_3_5(){ - if (jj_scan_token(COORDSYS)) - return true; + Token xsp; + xsp = jj_scanpos; + if (jj_scan_token(63)){ + jj_scanpos = xsp; + if (jj_3R_20()) + return true; + } return false; } private boolean jj_3_4(){ + Token xsp; + xsp = jj_scanpos; + if (jj_3R_18()){ + jj_scanpos = xsp; + if (jj_3R_19()) + return true; + } + return false; + } + + private boolean jj_3R_18(){ Token xsp; xsp = jj_scanpos; if (jj_scan_token(8)){ @@ -5693,190 +5697,124 @@ public class ADQLParser implements ADQLParserConstants { return false; } - private boolean jj_3R_128(){ - if (jj_scan_token(POLYGON)) - return true; - if (jj_scan_token(LEFT_PAR)) - return true; - if (jj_3R_157()) - return true; - if (jj_scan_token(COMMA)) - return true; - if (jj_3R_158()) - return true; - if (jj_scan_token(COMMA)) - return true; - if (jj_3R_158()) - return true; - if (jj_scan_token(COMMA)) - return true; - if (jj_3R_158()) + private boolean jj_3R_69(){ + if (jj_3R_34()) return true; + return false; + } + + private boolean jj_3R_149(){ Token xsp; - while(true){ - xsp = jj_scanpos; - if (jj_3R_159()){ + xsp = jj_scanpos; + if (jj_3R_161()){ + jj_scanpos = xsp; + if (jj_3_1()){ jj_scanpos = xsp; - break; + if (jj_3R_162()) + return true; } } - if (jj_scan_token(RIGHT_PAR)) - return true; return false; } - private boolean jj_3R_137(){ - if (jj_scan_token(TOP)) - return true; - if (jj_scan_token(UNSIGNED_INTEGER)) + private boolean jj_3R_68(){ + if (jj_3R_35()) return true; return false; } - private boolean jj_3R_136(){ - if (jj_scan_token(QUANTIFIER)) + private boolean jj_3R_67(){ + if (jj_3R_21()) return true; return false; } - private boolean jj_3R_102(){ - if (jj_3R_130()) + private boolean jj_3R_108(){ + if (jj_3R_135()) return true; Token xsp; xsp = jj_scanpos; - if (jj_3R_145()) + if (jj_3R_156()) jj_scanpos = xsp; return false; } - private boolean jj_3R_106(){ - if (jj_scan_token(FULL)) + private boolean jj_3R_66(){ + if (jj_3R_109()) return true; return false; } - private boolean jj_3R_127(){ - if (jj_3R_147()) + private boolean jj_3R_140(){ + if (jj_scan_token(POLYGON)) return true; - return false; - } - - private boolean jj_3R_44(){ - if (jj_scan_token(SELECT)) + if (jj_scan_token(LEFT_PAR)) return true; - Token xsp; - xsp = jj_scanpos; - if (jj_3R_136()) - jj_scanpos = xsp; - xsp = jj_scanpos; - if (jj_3R_137()) - jj_scanpos = xsp; - if (jj_3R_138()) + if (jj_3R_169()) + return true; + if (jj_scan_token(COMMA)) + return true; + if (jj_3R_170()) + return true; + if (jj_scan_token(COMMA)) + return true; + if (jj_3R_170()) + return true; + if (jj_scan_token(COMMA)) + return true; + if (jj_3R_170()) return true; + Token xsp; while(true){ xsp = jj_scanpos; - if (jj_3R_139()){ + if (jj_3R_171()){ jj_scanpos = xsp; break; } } - return false; - } - - private boolean jj_3R_59(){ - if (jj_3R_102()) - return true; - return false; - } - - private boolean jj_3R_58(){ - if (jj_3R_24()) + if (jj_scan_token(RIGHT_PAR)) return true; return false; } - private boolean jj_3R_57(){ + private boolean jj_3R_65(){ if (jj_3R_24()) return true; return false; } - private boolean jj_3R_70(){ + private boolean jj_3R_64(){ if (jj_scan_token(LEFT_PAR)) return true; - if (jj_3R_41()) + if (jj_3R_46()) return true; if (jj_scan_token(RIGHT_PAR)) return true; return false; } - private boolean jj_3R_56(){ - if (jj_3R_102()) + private boolean jj_3R_63(){ + if (jj_3R_27()) return true; return false; } - private boolean jj_3R_126(){ - if (jj_scan_token(CIRCLE)) - return true; - if (jj_scan_token(LEFT_PAR)) - return true; - if (jj_3R_157()) - return true; - if (jj_scan_token(COMMA)) - return true; + private boolean jj_3R_139(){ if (jj_3R_158()) return true; - if (jj_scan_token(COMMA)) - return true; - if (jj_3R_102()) - return true; - if (jj_scan_token(RIGHT_PAR)) - return true; return false; } - private boolean jj_3R_69(){ - if (jj_3R_117()) + private boolean jj_3R_150(){ + if (jj_scan_token(COMMA)) return true; - return false; - } - - private boolean jj_3R_55(){ - if (jj_3R_101()) + if (jj_3R_149()) return true; return false; } - private boolean jj_3R_41(){ - Token xsp; - xsp = jj_scanpos; - if (jj_3R_55()){ - jj_scanpos = xsp; - if (jj_3R_56()){ - jj_scanpos = xsp; - if (jj_3R_57()){ - jj_scanpos = xsp; - if (jj_3R_58()){ - jj_scanpos = xsp; - if (jj_3R_59()) - return true; - } - } - } - } - return false; - } - - private boolean jj_3R_125(){ - if (jj_scan_token(CENTROID)) - return true; - if (jj_scan_token(LEFT_PAR)) - return true; - if (jj_3R_64()) - return true; - if (jj_scan_token(RIGHT_PAR)) + private boolean jj_3R_62(){ + if (jj_3R_108()) return true; return false; } @@ -5892,7 +5830,7 @@ public class ADQLParser implements ADQLParserConstants { private Token jj_scanpos, jj_lastpos; private int jj_la; private int jj_gen; - final private int[] jj_la1 = new int[97]; + final private int[] jj_la1 = new int[98]; static private int[] jj_la1_0; static private int[] jj_la1_1; static private int[] jj_la1_2; @@ -5905,22 +5843,22 @@ public class ADQLParser implements ADQLParserConstants { } private static void jj_la1_init_0(){ - jj_la1_0 = new int[]{0x41,0x0,0x0,0x0,0x0,0x80000,0x100000,0x20,0x0,0x0,0x400000,0x400,0x304,0x20,0x20,0x20,0x0,0x10,0x10,0x10,0x0,0x0,0x0,0x0,0x400000,0x400000,0x400000,0x0,0x4,0x3d800000,0x1c000000,0x2000000,0x1d000000,0x1d000000,0x1c000000,0x2000000,0x1d000000,0x1d000000,0x20,0xc0000000,0x3d800000,0x0,0x0,0x0,0x300,0x300,0x4,0x0,0x304,0x300,0x300,0xc00,0xc00,0x300,0x300,0x4,0x80,0x0,0x4,0x0,0x0,0x0,0x0,0x0,0x4,0x0,0x0,0x3f000,0x0,0x0,0x304,0x3f000,0x0,0x0,0x20,0x4,0x80000,0x704,0x0,0x80000,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x20,0x0,0x0,0x304,0x20,0x20,0x0,0x0,0x20,0x304,}; + jj_la1_0 = new int[]{0x41,0x0,0x0,0x0,0x0,0x80000,0x100000,0x20,0x0,0x0,0x400000,0x400,0x304,0x20,0x20,0x20,0x0,0x10,0x10,0x10,0x0,0x0,0x0,0x0,0x400000,0x400000,0x400000,0x0,0x4,0x3d800000,0x1c000000,0x2000000,0x1d000000,0x1d000000,0x1c000000,0x2000000,0x1d000000,0x1d000000,0x20,0xc0000000,0x3d800000,0x0,0x0,0x0,0x300,0x300,0x4,0x4,0x0,0x304,0x300,0x300,0xc00,0xc00,0x300,0x300,0x4,0x80,0x0,0x4,0x0,0x0,0x0,0x0,0x0,0x4,0x0,0x0,0x3f000,0x0,0x0,0x304,0x3f000,0x0,0x0,0x20,0x4,0x80000,0x704,0x0,0x80000,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x20,0x0,0x0,0x304,0x20,0x20,0x0,0x0,0x20,0x304,}; } private static void jj_la1_init_1(){ - jj_la1_1 = new int[]{0x0,0x1,0x400,0x800,0x1000,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0xffff8000,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x6000,0x6000,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0xf8000,0x3f00000,0x7c0f8000,0x0,0x0,0x0,0x0,0x0,0x0,0xf8000,0x0,0x80000000,0xf8000,0x3f00000,0x8,0x6,0x6,0x8,0x0,0x8,0x8,0x0,0x108,0x200,0xffff8000,0x0,0x8,0x8,0x0,0x0,0x0,0xffff8000,0x78000,0x0,0xf8000,0xc000000,0x800000,0x800000,0x800000,0x800000,0x7c000000,0x0,0x3f00000,0x7c000000,0x7c0f8000,0x0,0x0,0x0,0x0,0x0,0xffff8000,}; + jj_la1_1 = new int[]{0x0,0x1,0x400,0x800,0x1000,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0xffff8000,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x6000,0x6000,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0xf8000,0x0,0x3f00000,0x7c0f8000,0x0,0x0,0x0,0x0,0x0,0x0,0xf8000,0x0,0x80000000,0x0,0x3f00000,0x8,0x6,0x6,0x8,0x0,0x8,0x8,0x0,0x108,0x200,0xffff8000,0x0,0x8,0x8,0x0,0x0,0x0,0xffff8000,0x78000,0x0,0xf8000,0xc000000,0x800000,0x800000,0x800000,0x800000,0x7c000000,0x0,0x3f00000,0x7c000000,0x7c0f8000,0x0,0x0,0x0,0x0,0x0,0xffff8000,}; } private static void jj_la1_init_2(){ - jj_la1_2 = new int[]{0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x20ffffff,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x20000000,0x0,0x0,0x0,0x0,0x20000000,0x0,0x20ffffff,0x0,0x0,0x0,0x0,0x0,0x0,0x20000000,0x0,0x0,0x20000000,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x20ffffff,0x0,0x0,0x0,0x0,0x0,0x0,0x20ffffff,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x1,0x0,0x0,0xffffff,0x20ffffff,0x0,0x0,0xfffe,0xff0000,0x0,0x20ffffff,}; + jj_la1_2 = new int[]{0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x20ffffff,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x20000000,0x0,0x0,0x0,0x0,0x0,0x20000000,0x0,0xffffff,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x20000000,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x20ffffff,0x0,0x0,0x0,0x0,0x0,0x0,0x20ffffff,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x1,0x0,0x0,0xffffff,0xffffff,0x0,0x0,0xfffe,0xff0000,0x0,0x20ffffff,}; } private static void jj_la1_init_3(){ - jj_la1_3 = new int[]{0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x3,0x3,0x0,0x0,0x3b,0x0,0x0,0x0,0x3,0x0,0x0,0x0,0x23,0x23,0x0,0x0,0x0,0x3,0x0,0x3,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x38,0x30,0x0,0x0,0x3b,0x0,0x3b,0x0,0x0,0x0,0x0,0x0,0x0,0x3b,0x0,0x0,0x3b,0x3,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x3b,0x0,0x0,0x0,0x0,0x0,0x0,0x3b,0x0,0x0,0x0,0x0,0x3,0x3,0x3,0x3,0x0,0x0,0x0,0x2,0x3b,0x0,0x0,0x0,0x0,0x0,0x3b,}; + jj_la1_3 = new int[]{0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x3,0x3,0x0,0x0,0x3b,0x0,0x0,0x0,0x3,0x0,0x0,0x0,0x23,0x23,0x0,0x0,0x0,0x3,0x0,0x3,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x38,0x30,0x0,0x0,0x3b,0x3,0x0,0x3b,0x0,0x0,0x0,0x0,0x0,0x0,0x3b,0x0,0x0,0x3,0x3,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x3b,0x0,0x0,0x0,0x0,0x0,0x0,0x3b,0x0,0x0,0x0,0x0,0x3,0x3,0x3,0x3,0x0,0x0,0x0,0x2,0x3b,0x0,0x0,0x0,0x0,0x0,0x3b,}; } - final private JJCalls[] jj_2_rtns = new JJCalls[13]; + final private JJCalls[] jj_2_rtns = new JJCalls[16]; private boolean jj_rescan = false; private int jj_gc = 0; @@ -5940,7 +5878,7 @@ public class ADQLParser implements ADQLParserConstants { token = new Token(); jj_ntk = -1; jj_gen = 0; - for(int i = 0; i < 97; i++) + for(int i = 0; i < 98; i++) jj_la1[i] = -1; for(int i = 0; i < jj_2_rtns.length; i++) jj_2_rtns[i] = new JJCalls(); @@ -5962,7 +5900,7 @@ public class ADQLParser implements ADQLParserConstants { token = new Token(); jj_ntk = -1; jj_gen = 0; - for(int i = 0; i < 97; i++) + for(int i = 0; i < 98; i++) jj_la1[i] = -1; for(int i = 0; i < jj_2_rtns.length; i++) jj_2_rtns[i] = new JJCalls(); @@ -5975,7 +5913,7 @@ public class ADQLParser implements ADQLParserConstants { token = new Token(); jj_ntk = -1; jj_gen = 0; - for(int i = 0; i < 97; i++) + for(int i = 0; i < 98; i++) jj_la1[i] = -1; for(int i = 0; i < jj_2_rtns.length; i++) jj_2_rtns[i] = new JJCalls(); @@ -5988,7 +5926,7 @@ public class ADQLParser implements ADQLParserConstants { token = new Token(); jj_ntk = -1; jj_gen = 0; - for(int i = 0; i < 97; i++) + for(int i = 0; i < 98; i++) jj_la1[i] = -1; for(int i = 0; i < jj_2_rtns.length; i++) jj_2_rtns[i] = new JJCalls(); @@ -6000,7 +5938,7 @@ public class ADQLParser implements ADQLParserConstants { token = new Token(); jj_ntk = -1; jj_gen = 0; - for(int i = 0; i < 97; i++) + for(int i = 0; i < 98; i++) jj_la1[i] = -1; for(int i = 0; i < jj_2_rtns.length; i++) jj_2_rtns[i] = new JJCalls(); @@ -6012,7 +5950,7 @@ public class ADQLParser implements ADQLParserConstants { token = new Token(); jj_ntk = -1; jj_gen = 0; - for(int i = 0; i < 97; i++) + for(int i = 0; i < 98; i++) jj_la1[i] = -1; for(int i = 0; i < jj_2_rtns.length; i++) jj_2_rtns[i] = new JJCalls(); @@ -6038,7 +5976,6 @@ public class ADQLParser implements ADQLParserConstants { } } } - trace_token(token, ""); return token; } token = oldToken; @@ -6086,7 +6023,6 @@ public class ADQLParser implements ADQLParserConstants { token = token.next = token_source.getNextToken(); jj_ntk = -1; jj_gen++; - trace_token(token, " (in getNextToken)"); return token; } @@ -6150,7 +6086,7 @@ public class ADQLParser implements ADQLParserConstants { la1tokens[jj_kind] = true; jj_kind = -1; } - for(int i = 0; i < 97; i++){ + for(int i = 0; i < 98; i++){ if (jj_la1[i] == jj_gen){ for(int j = 0; j < 32; j++){ if ((jj_la1_0[i] & (1 << j)) != 0){ @@ -6185,68 +6121,15 @@ public class ADQLParser implements ADQLParserConstants { return new ParseException(token, exptokseq, tokenImage); } - private int trace_indent = 0; - private boolean trace_enabled = true; - /** Enable tracing. */ - final public void enable_tracing(){ - trace_enabled = true; - } + final public void enable_tracing(){} /** Disable tracing. */ - final public void disable_tracing(){ - trace_enabled = false; - } - - private void trace_call(String s){ - if (trace_enabled){ - for(int i = 0; i < trace_indent; i++){ - System.out.print(" "); - } - System.out.println("Call: " + s); - } - trace_indent = trace_indent + 2; - } - - private void trace_return(String s){ - trace_indent = trace_indent - 2; - if (trace_enabled){ - for(int i = 0; i < trace_indent; i++){ - System.out.print(" "); - } - System.out.println("Return: " + s); - } - } - - private void trace_token(Token t, String where){ - if (trace_enabled){ - for(int i = 0; i < trace_indent; i++){ - System.out.print(" "); - } - System.out.print("Consumed token: <" + tokenImage[t.kind]); - if (t.kind != 0 && !tokenImage[t.kind].equals("\"" + t.image + "\"")){ - System.out.print(": \"" + t.image + "\""); - } - System.out.println(" at line " + t.beginLine + " column " + t.beginColumn + ">" + where); - } - } - - private void trace_scan(Token t1, int t2){ - if (trace_enabled){ - for(int i = 0; i < trace_indent; i++){ - System.out.print(" "); - } - System.out.print("Visited token: <" + tokenImage[t1.kind]); - if (t1.kind != 0 && !tokenImage[t1.kind].equals("\"" + t1.image + "\"")){ - System.out.print(": \"" + t1.image + "\""); - } - System.out.println(" at line " + t1.beginLine + " column " + t1.beginColumn + ">; Expected token: <" + tokenImage[t2] + ">"); - } - } + final public void disable_tracing(){} private void jj_rescan_token(){ jj_rescan = true; - for(int i = 0; i < 13; i++){ + for(int i = 0; i < 16; i++){ try{ JJCalls p = jj_2_rtns[i]; do{ @@ -6293,6 +6176,15 @@ public class ADQLParser implements ADQLParserConstants { case 12: jj_3_13(); break; + case 13: + jj_3_14(); + break; + case 14: + jj_3_15(); + break; + case 15: + jj_3_16(); + break; } } p = p.next; diff --git a/src/adql/parser/ADQLQueryFactory.java b/src/adql/parser/ADQLQueryFactory.java index 12887f4..4bcfdf2 100644 --- a/src/adql/parser/ADQLQueryFactory.java +++ b/src/adql/parser/ADQLQueryFactory.java @@ -16,13 +16,13 @@ package adql.parser; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2012-2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomishes Rechen Institute (ARI) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.util.Collection; -import java.util.Vector; +import adql.db.FunctionDef; import adql.parser.IdentifierItems.IdentifierItem; import adql.query.ADQLOrder; import adql.query.ADQLQuery; @@ -83,26 +83,29 @@ import adql.query.operand.function.geometry.RegionFunction; *

To customize the object representation you merely have to extends the appropriate functions of this class.

* * @author Grégory Mantelet (CDS;ARI) - * @version 1.3 (05/2014) + * @version 1.4 (06/2015) * * @see ADQLParser */ public class ADQLQueryFactory { - protected boolean allowUnknownFunctions = false; - + /** + * Type of table JOIN. + * + * @author Grégory Mantelet (CDS) + * @version 1.0 (08/2011) + */ public static enum JoinType{ CROSS, INNER, OUTER_LEFT, OUTER_RIGHT, OUTER_FULL; } + /** + * Create a query factory. + */ public ADQLQueryFactory(){ ; } - public ADQLQueryFactory(boolean allowUnknownFunctions){ - this.allowUnknownFunctions = allowUnknownFunctions; - } - public ADQLQuery createQuery() throws Exception{ return new ADQLQuery(); } @@ -268,21 +271,29 @@ public class ADQLQueryFactory { /** *

Creates the user defined functions called as the given name and with the given parameters.

- *

IMPORTANT: This function must be overridden if some user defined functions are available.

+ * + *

+ * By default, this function returns a {@link DefaultUDF} instance. It is generic enough to cover every kind of functions. + * But you can of course override this function in order to return your own instance of {@link UserDefinedFunction}. + * In this case, you may not forget to call the super function (super.createUserDefinedFunction(name, params)) so that + * all other unknown functions are still returned as {@link DefaultUDF} instances. + *

+ * + *

IMPORTANT: + * The tests done to check whether a user defined function is allowed/managed in this implementation, is done later by the parser. + * Only declared UDF will pass the test of the parser. For that, you should give it a list of allowed UDFs (each UDF will be then + * represented by a {@link FunctionDef} object). + *

* * @param name Name of the user defined function to create. * @param params Parameters of the user defined function to create. * - * @return The corresponding user defined function. + * @return The corresponding user defined function (by default an instance of {@link DefaultUDF}). * - * @throws Exception An {@link UnsupportedOperationException} by default, otherwise any other type of error may be - * thrown if there is a problem while creating the function. + * @throws Exception If there is a problem while creating the function. */ public UserDefinedFunction createUserDefinedFunction(String name, ADQLOperand[] params) throws Exception{ - if (allowUnknownFunctions) - return new DefaultUDF(name, params); - else - throw new UnsupportedOperationException("No ADQL function called \"" + name + "\" !"); + return new DefaultUDF(name, params); } public DistanceFunction createDistance(PointFunction point1, PointFunction point2) throws Exception{ @@ -317,7 +328,7 @@ public class ADQLQueryFactory { return new RegionFunction(param); } - public PolygonFunction createPolygon(ADQLOperand coordSys, Vector coords) throws Exception{ + public PolygonFunction createPolygon(ADQLOperand coordSys, Collection coords) throws Exception{ return new PolygonFunction(coordSys, coords); } @@ -375,14 +386,14 @@ public class ADQLQueryFactory { /** * Replace {@link #createOrder(int, boolean, TextPosition)}. - * @since 1.3 + * @since 1.4 */ public ADQLOrder createOrder(final int ind, final boolean desc) throws Exception{ return new ADQLOrder(ind, desc); } /** - * @deprecated since 1.3 ; Replaced by {@link #createOrder(int, boolean)} + * @deprecated since 1.4 ; Replaced by {@link #createOrder(int, boolean)} */ @Deprecated public ADQLOrder createOrder(final int ind, final boolean desc, final TextPosition position) throws Exception{ diff --git a/src/adql/parser/QueryChecker.java b/src/adql/parser/QueryChecker.java index 0c15a16..dec0c61 100644 --- a/src/adql/parser/QueryChecker.java +++ b/src/adql/parser/QueryChecker.java @@ -17,7 +17,7 @@ package adql.parser; * along with ADQLLibrary. If not, see . * * Copyright 2012-2013 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomisches Rechen Institute (ARI) + * Astronomisches Rechen Institut (ARI) */ import adql.db.DBChecker; diff --git a/src/adql/parser/TokenMgrError.java b/src/adql/parser/TokenMgrError.java index 6e87508..b41b384 100644 --- a/src/adql/parser/TokenMgrError.java +++ b/src/adql/parser/TokenMgrError.java @@ -3,13 +3,7 @@ package adql.parser; /** Token Manager Error. */ -/** - * TODO Javadoc of TokenMgrError ! - * - * @author Grégory Mantelet (CDS) - * @version 08/2011 - * - */ + @SuppressWarnings("all") public class TokenMgrError extends Error { @@ -129,6 +123,7 @@ public class TokenMgrError extends Error { * * from this method for such cases in the release version of your parser. */ + @Override public String getMessage(){ return super.getMessage(); } diff --git a/src/adql/parser/adqlGrammar.jj b/src/adql/parser/adqlGrammar.jj index cc2d75e..442b9f7 100644 --- a/src/adql/parser/adqlGrammar.jj +++ b/src/adql/parser/adqlGrammar.jj @@ -14,7 +14,7 @@ * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2012-2014 - UDS/Centre de DonnM-CM-)es astronomiques de Strasbourg (CDS), + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), * Astronomisches Rechen Institute (ARI) */ @@ -26,7 +26,7 @@ * If the syntax is not conform to the ADQL definition an error message is printed else it will be the message "Correct syntax". * * Author: Grégory Mantelet (CDS;ARI) - gmantele@ari.uni-heidelberg.de -* Version: 1.2 (03/2014) +* Version: 1.4 (06/2015) */ /* ########### */ @@ -35,7 +35,7 @@ options { STATIC = false; IGNORE_CASE = true; - DEBUG_PARSER = true; + DEBUG_PARSER = false; } /* ########## */ @@ -89,7 +89,7 @@ import adql.translator.TranslationException; * @see ADQLQueryFactory * * @author Grégory Mantelet (CDS;ARI) - gmantele@ari.uni-heidelberg.de -* @version 1.2 (03/2014) +* @version 1.4 (06/2015) */ public class ADQLParser { @@ -108,9 +108,6 @@ public class ADQLParser { /** The first token of a table/column name. This token is extracted by {@link #Identifier()}. */ private Token currentIdentifierToken = null; - /** List of all allowed coordinate systems. */ - private ArrayList allowedCoordSys = new ArrayList(); - /** * Builds an ADQL parser without a query to parse. */ @@ -348,24 +345,6 @@ public class ADQLParser { return Query(); } - public final void addCoordinateSystem(final String coordSys){ - allowedCoordSys.add(coordSys); - } - - public final void setCoordinateSystems(final Collection coordSys){ - allowedCoordSys.clear(); - if (coordSys != null) - allowedCoordSys.addAll(coordSys); - } - - public final boolean isAllowedCoordSys(final String coordSys) { - for(String cs : allowedCoordSys){ - if (cs.equalsIgnoreCase(coordSys)) - return true; - } - return false; - } - public final void setDebug(boolean debug){ if (debug) enable_tracing(); else disable_tracing(); @@ -1114,29 +1093,48 @@ NumericConstant SignedInteger(): {Token sign=null, number; NumericConstant cst;} /* *********** */ /* EXPRESSIONS */ /* *********** */ -ADQLOperand ValueExpressionPrimary(): {ADQLColumn column; ADQLOperand op; Token left,right;} { - try{ +ADQLOperand NumericValueExpressionPrimary(): {String expr; ADQLColumn column; ADQLOperand op; Token left, right;} { + try{ (// unsigned_value_specification op=UnsignedNumeric() {return op;} - // string - | op=String() {return op;} // column_reference - | column=Column() {return column;} + | column=Column() {column.setExpectedType('N'); return column;} // set_function_specification | op=SqlFunction() {return op;} // LEFT_PAR value_expression RIGHT_PAR - | (left= op=ValueExpression() right=) { WrappedOperand wop = queryFactory.createWrappedOperand(op); wop.setPosition(new TextPosition(left, right)); return wop;}) - }catch(Exception ex){ throw generateParseException(ex); + | (left= op=NumericExpression() right=) { WrappedOperand wop = queryFactory.createWrappedOperand(op); wop.setPosition(new TextPosition(left, right)); return wop;}) + }catch(Exception ex){ + throw generateParseException(ex); + } +} + +ADQLOperand StringValueExpressionPrimary(): {StringConstant expr; ADQLColumn column; ADQLOperand op;} { + try{ + (// string + expr=String() {return expr;} + // column_reference + | column=Column() {column.setExpectedType('S'); return column;} + // LEFT_PAR value_expression RIGHT_PAR + | ( (op=StringExpression()) ) {return queryFactory.createWrappedOperand(op);}) + }catch(Exception ex){ + throw generateParseException(ex); } } ADQLOperand ValueExpression(): {ADQLOperand valueExpr = null; } { - (valueExpr=GeometryValueFunction() - | LOOKAHEAD( | ) valueExpr=NumericExpression() - | LOOKAHEAD() valueExpr=StringExpression() - | LOOKAHEAD(StringFactor() ) valueExpr=StringExpression() - | valueExpr=NumericExpression()) - {return valueExpr;} + try{ + (LOOKAHEAD((|) | (Factor() (|||))) valueExpr=NumericExpression() + | LOOKAHEAD( | (StringFactor() )) valueExpr=StringExpression() + | LOOKAHEAD() valueExpr=ValueExpression() { valueExpr = queryFactory.createWrappedOperand(valueExpr); } + | LOOKAHEAD( ) valueExpr=UserDefinedFunction() + | valueExpr=GeometryValueFunction() + | LOOKAHEAD(Column()) valueExpr=Column() + | LOOKAHEAD(String()) valueExpr=StringFactor() + | valueExpr=Factor()) + {return valueExpr;} + }catch(Exception ex){ + throw generateParseException(ex); + } } ADQLOperand NumericExpression(): {Token sign=null; ADQLOperand leftOp, rightOp=null;} { @@ -1175,8 +1173,8 @@ ADQLOperand NumericTerm(): {Token sign=null; ADQLOperand leftOp, rightOp=null;} ADQLOperand Factor(): {boolean negative = false; Token minusSign = null; ADQLOperand op;} { ( - ( | (minusSign= {negative = true;}))? - (LOOKAHEAD(2) op=NumericFunction() | op=ValueExpressionPrimary()) + ( | ( {negative = true;}))? + (LOOKAHEAD(2) op=NumericFunction() | op=NumericValueExpressionPrimary()) ) { @@ -1222,17 +1220,17 @@ ADQLOperand StringExpression(): {ADQLOperand leftOp; ADQLOperand rightOp = null; ADQLOperand StringFactor(): {ADQLOperand op;} { (op=ExtractCoordSys() - | LOOKAHEAD(2) op=UserDefinedFunction() - | op=ValueExpressionPrimary()) + | LOOKAHEAD(2) op=UserDefinedFunction() { ((UserDefinedFunction)op).setExpectedType('S'); } + | op=StringValueExpressionPrimary()) {return op;} } GeometryValue GeometryExpression(): {ADQLColumn col = null; GeometryFunction gf = null;} { (col=Column() | gf=GeometryValueFunction()) { - if (col != null) + if (col != null){ col.setExpectedType('G'); return new GeometryValue(col); - else + }else return new GeometryValue(gf); } } @@ -1444,24 +1442,26 @@ GeometryFunction GeometryFunction(): {Token fct=null, end; GeometryValue gvf1=GeometryExpression() end=) {gf = queryFactory.createArea(gvf1);} - | (fct= (p1=Point() {gf = queryFactory.createCoord1(p1);} | col1=Column() {gf = queryFactory.createCoord1(col1);}) end=) - | (fct= (p1=Point() {gf = queryFactory.createCoord2(p1);} | col1=Column() {gf = queryFactory.createCoord2(col1);}) end=) + | (fct= (p1=Point() {gf = queryFactory.createCoord1(p1);} | col1=Column() {col1.setExpectedType('G'); gf = queryFactory.createCoord1(col1);}) end=) + | (fct= (p1=Point() {gf = queryFactory.createCoord2(p1);} | col1=Column() {col1.setExpectedType('G'); gf = queryFactory.createCoord2(col1);}) end=) | (fct= (p1=Point()|col1=Column()) { if (p1 != null) gvp1 = new GeometryValue(p1); - else + else{ col1.setExpectedType('G'); gvp1 = new GeometryValue(col1); + } } (p2=Point()|col2=Column()) { if (p2 != null) gvp2 = new GeometryValue(p2); - else + else{ col2.setExpectedType('G'); gvp2 = new GeometryValue(col2); + } } end= {gf = queryFactory.createDistance(gvp1, gvp2);} @@ -1477,19 +1477,9 @@ GeometryFunction GeometryFunction(): {Token fct=null, end; GeometryValue 0){ - TextPosition position = new TextPosition(oldToken.next, token); - if (coordSys == null) - throw new ParseException("A coordinate system must always be provided !", position); - if (coordSys instanceof StringConstant && !isAllowedCoordSys(((StringConstant)coordSys).getValue())) - throw new ParseException("\""+coordSys.toADQL()+"\" is not an allowed coordinate systems !", position); - } - - return coordSys; - } + { return coordSys; } } GeometryFunction GeometryValueFunction(): {Token fct=null, end=null; ADQLOperand coordSys; ADQLOperand width, height; ADQLOperand[] coords, tmp; Vector vCoords; ADQLOperand op=null; GeometryValue gvf = null; GeometryFunction gf = null;} { @@ -1569,7 +1559,7 @@ ADQLFunction NumericFunction(): {ADQLFunction fct;} { (fct=MathFunction() | fct=TrigFunction() | fct=GeometryFunction() - | fct=UserDefinedFunction()) + | fct=UserDefinedFunction() { ((UserDefinedFunction)fct).setExpectedType('N'); }) {return fct;} } @@ -1624,19 +1614,23 @@ MathFunction TrigFunction(): {Token fct=null, end; ADQLOperand param1=null, para } } -/* /!\ WARNING: The function name may be prefixed by "udf_" but there is no way to check it here ! */ UserDefinedFunction UserDefinedFunction(): {Token fct, end; Vector params = new Vector(); ADQLOperand op;} { fct= (op=ValueExpression() {params.add(op);} ( op=ValueExpression() {params.add(op);})*)? end= { //System.out.println("INFO [ADQLParser]: \""+fct.image+"\" (from line "+fct.beginLine+" and column "+fct.beginColumn+" to line "+token.endLine+" and column "+(token.endColumn+1)+") is considered as an user defined function !"); try{ + // Build the parameters list: ADQLOperand[] parameters = new ADQLOperand[params.size()]; for(int i=0; i. * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.util.Iterator; @@ -27,8 +28,8 @@ import java.util.Vector; * *

Since it is a list, it is possible to add, remove, modify and iterate on a such object.

* - * @author Grégory Mantelet (CDS) - * @version 06/2011 + * @author Grégory Mantelet (CDS;ARI) + * @version 1.4 (06/2015) * * @see ClauseADQL * @see ClauseConstraints @@ -44,7 +45,7 @@ public abstract class ADQLList< T extends ADQLObject > implements ADQLObject, It private final Vector list = new Vector(); /** Position inside an ADQL query string. - * @since 1.3 */ + * @since 1.4 */ private TextPosition position = null; /** @@ -195,7 +196,7 @@ public abstract class ADQLList< T extends ADQLObject > implements ADQLObject, It * Sets the position at which this {@link ADQLList} has been found in the original ADQL query string. * * @param pos Position of this {@link ADQLList}. - * @since 1.3 + * @since 1.4 */ public final void setPosition(final TextPosition position){ this.position = position; @@ -203,12 +204,15 @@ public abstract class ADQLList< T extends ADQLObject > implements ADQLObject, It @Override public String toADQL(){ - String adql = (getName() == null) ? "" : (getName() + " "); + StringBuffer adql = new StringBuffer((getName() == null) ? "" : (getName() + " ")); - for(int i = 0; i < size(); i++) - adql += ((i == 0) ? "" : (" " + getSeparator(i) + " ")) + get(i).toADQL(); + for(int i = 0; i < size(); i++){ + if (i > 0) + adql.append(" " + getSeparator(i) + " "); + adql.append(get(i).toADQL()); + } - return adql; + return adql.toString(); } @Override diff --git a/src/adql/query/ADQLObject.java b/src/adql/query/ADQLObject.java index 85ebaa0..fee5aef 100644 --- a/src/adql/query/ADQLObject.java +++ b/src/adql/query/ADQLObject.java @@ -18,8 +18,8 @@ import adql.search.ISearchHandler; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomishes Rechen Institute (ARI) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institute (ARI) */ /** @@ -33,7 +33,7 @@ import adql.search.ISearchHandler; *

* * @author Grégory Mantelet (CDS;ARI) - * @version 1.3 (05/2014) + * @version 1.4 (06/2015) */ public interface ADQLObject { @@ -51,7 +51,7 @@ public interface ADQLObject { * @return Position of this ADQL item in the ADQL query, * or NULL if not written originally in the query (for example, if added afterwards. * - * @since 1.3 + * @since 1.4 */ public TextPosition getPosition(); diff --git a/src/adql/query/ADQLQuery.java b/src/adql/query/ADQLQuery.java index 3f47294..1372e71 100644 --- a/src/adql/query/ADQLQuery.java +++ b/src/adql/query/ADQLQuery.java @@ -16,8 +16,8 @@ package adql.query; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2012-2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomishes Rechen Institute (ARI) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.util.ArrayList; @@ -38,7 +38,7 @@ import adql.search.ISearchHandler; *

The resulting object of the {@link ADQLParser} is an object of this class.

* * @author Grégory Mantelet (CDS;ARI) - * @version 1.3 (05/2014) + * @version 1.4 (06/2015) */ public class ADQLQuery implements ADQLObject { @@ -61,7 +61,7 @@ public class ADQLQuery implements ADQLObject { private ClauseADQL orderBy; /** Position of this Query (or sub-query) inside the whole given ADQL query string. - * @since 1.3*/ + * @since 1.4 */ private TextPosition position = null; /** @@ -271,7 +271,7 @@ public class ADQLQuery implements ADQLObject { * Set the position of this {@link ADQLQuery} (or sub-query) inside the whole given ADQL query string. * * @param position New position of this {@link ADQLQuery}. - * @since 1.3 + * @since 1.4 */ public final void setPosition(final TextPosition position){ this.position = position; @@ -301,7 +301,12 @@ public class ADQLQuery implements ADQLObject { ADQLOperand operand = item.getOperand(); if (item instanceof SelectAllColumns){ try{ - columns.addAll(from.getDBColumns()); + // If "{table}.*", add all columns of the specified table: + if (((SelectAllColumns)item).getAdqlTable() != null) + columns.addAll(((SelectAllColumns)item).getAdqlTable().getDBColumns()); + // Otherwise ("*"), add all columns of all selected tables: + else + columns.addAll(from.getDBColumns()); }catch(ParseException pe){ // Here, this error should not occur any more, since it must have been caught by the DBChecker! } @@ -465,4 +470,4 @@ public class ADQLQuery implements ADQLObject { return adql.toString(); } -} \ No newline at end of file +} diff --git a/src/adql/query/ClauseADQL.java b/src/adql/query/ClauseADQL.java index 7116b41..30bf6ab 100644 --- a/src/adql/query/ClauseADQL.java +++ b/src/adql/query/ClauseADQL.java @@ -17,7 +17,7 @@ package adql.query; * along with ADQLLibrary. If not, see . * * Copyright 2012-2013 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomisches Rechen Institute (ARI) + * Astronomisches Rechen Institut (ARI) */ /** diff --git a/src/adql/query/SelectAllColumns.java b/src/adql/query/SelectAllColumns.java index 0096fb0..33dc27e 100644 --- a/src/adql/query/SelectAllColumns.java +++ b/src/adql/query/SelectAllColumns.java @@ -20,8 +20,8 @@ import adql.query.from.ADQLTable; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2012-2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomishes Rechen Institute (ARI) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ /** @@ -29,7 +29,7 @@ import adql.query.from.ADQLTable; * It means: 'select all columns'. * * @author Grégory Mantelet (CDS;ARI) - * @version 1.3 (05/2014) + * @version 1.4 (06/2015) */ public final class SelectAllColumns extends SelectItem { @@ -108,7 +108,7 @@ public final class SelectAllColumns extends SelectItem { * @param table An {@link ADQLTable} (MUST NOT BE NULL). */ public final void setAdqlTable(final ADQLTable table){ - if (table == null){ + if (table != null){ adqlTable = table; query = null; setPosition(null); diff --git a/src/adql/query/SelectItem.java b/src/adql/query/SelectItem.java index 41795cc..803101a 100644 --- a/src/adql/query/SelectItem.java +++ b/src/adql/query/SelectItem.java @@ -16,8 +16,8 @@ package adql.query; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2012-2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomishes Rechen Institute (ARI) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institute (ARI) */ import java.util.NoSuchElementException; @@ -30,7 +30,7 @@ import adql.query.operand.ADQLOperand; *

It merely encapsulates an operand and allows to associate to it an alias (according to the following syntax: "SELECT operand AS alias").

* * @author Grégory Mantelet (CDS;ARI) - * @version 1.3 (05/2014) + * @version 1.4 (06/2015) * * @see ClauseSelect */ @@ -46,7 +46,7 @@ public class SelectItem implements ADQLObject { private boolean caseSensitive = false; /** Position of this Select item in the ADQL query string. - * @since 1.3 */ + * @since 1.4 */ private TextPosition position = null; /** @@ -172,7 +172,7 @@ public class SelectItem implements ADQLObject { * Set the position of this {@link SelectItem} in the given ADQL query string. * * @param position New position of this {@link SelectItem}. - * @since 1.3 + * @since 1.4 */ public final void setPosition(final TextPosition position){ this.position = position; diff --git a/src/adql/query/TextPosition.java b/src/adql/query/TextPosition.java index 5bda46a..5324eba 100644 --- a/src/adql/query/TextPosition.java +++ b/src/adql/query/TextPosition.java @@ -16,8 +16,8 @@ package adql.query; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomishes Rechen Institute (ARI) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institute (ARI) */ import adql.parser.Token; @@ -27,7 +27,7 @@ import adql.parser.Token; * It is particularly used to localize columns and tables in the original ADQL query. * * @author Grégory Mantelet (CDS;ARI) - * @version 05/2014 + * @version 1.4 (06/2015) */ public class TextPosition { @@ -94,7 +94,7 @@ public class TextPosition { * Builds a copy of the given position. * * @param positionToCopy Position to copy. - * @since 1.3 + * @since 1.4 */ public TextPosition(final TextPosition positionToCopy){ this(positionToCopy.beginLine, positionToCopy.beginColumn, positionToCopy.endLine, positionToCopy.endColumn); @@ -105,7 +105,7 @@ public class TextPosition { * * @param startPos Start position (only beginLine and beginColumn will be used). * @param endPos End position (only endLine and endColumn will be used). - * @since 1.3 + * @since 1.4 */ public TextPosition(final TextPosition startPos, final TextPosition endPos){ this(startPos.beginLine, startPos.beginColumn, endPos.endLine, endPos.endColumn); diff --git a/src/adql/query/constraint/Between.java b/src/adql/query/constraint/Between.java index 430fec1..d321f18 100644 --- a/src/adql/query/constraint/Between.java +++ b/src/adql/query/constraint/Between.java @@ -16,8 +16,8 @@ package adql.query.constraint; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomishes Rechen Institute (ARI) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institute (ARI) */ import java.util.NoSuchElementException; @@ -34,7 +34,7 @@ import adql.query.operand.ADQLOperand; * between the value of the two other operands, else it returns false.

* * @author Grégory Mantelet (CDS;ARI) - * @version 1.3 (05/2014) + * @version 1.4 (06/2015) */ public class Between implements ADQLConstraint { @@ -51,7 +51,7 @@ public class Between implements ADQLConstraint { private boolean notBetween = false; /** Position of this {@link Between} in the given ADQL query string. - * @since 1.3 */ + * @since 1.4 */ private TextPosition position = null; /** @@ -183,7 +183,7 @@ public class Between implements ADQLConstraint { * Set the position of this {@link Between} in the given ADQL query string. * * @param position New position of this {@link Between}. - * @since 1.3 + * @since 1.4 */ public final void setPosition(final TextPosition position){ this.position = position; diff --git a/src/adql/query/constraint/Comparison.java b/src/adql/query/constraint/Comparison.java index 14b2d1b..1c0d236 100644 --- a/src/adql/query/constraint/Comparison.java +++ b/src/adql/query/constraint/Comparison.java @@ -16,8 +16,8 @@ package adql.query.constraint; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomishes Rechen Institute (ARI) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institute (ARI) */ import java.util.NoSuchElementException; @@ -31,7 +31,7 @@ import adql.query.operand.ADQLOperand; * Represents a comparison (numeric or not) between two operands. * * @author Grégory Mantelet (CDS;ARI) - * @version 1.3 (05/2014) + * @version 1.4 (06/2015) * * @see ComparisonOperator */ @@ -47,7 +47,7 @@ public class Comparison implements ADQLConstraint { private ADQLOperand rightOperand; /** Position of this {@link Comparison} in the given ADQL query string. - * @since 1.3 */ + * @since 1.4 */ private TextPosition position = null; /** @@ -173,7 +173,7 @@ public class Comparison implements ADQLConstraint { * Set the position of this {@link Comparison} in the given ADQL query string. * * @param position New position of this {@link Comparison}. - * @since 1.3 + * @since 1.4 */ public final void setPosition(final TextPosition position){ this.position = position; diff --git a/src/adql/query/constraint/Exists.java b/src/adql/query/constraint/Exists.java index 92c1197..52af88f 100644 --- a/src/adql/query/constraint/Exists.java +++ b/src/adql/query/constraint/Exists.java @@ -16,8 +16,8 @@ package adql.query.constraint; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomishes Rechen Institute (ARI) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institute (ARI) */ import java.util.NoSuchElementException; @@ -33,7 +33,7 @@ import adql.query.TextPosition; *

This function returns true if the sub-query given in parameter returns at least one result, else it returns false.

* * @author Grégory Mantelet (CDS;ARI) - * @version 1.3 (05/2014) + * @version 1.4 (06/2015) */ public class Exists implements ADQLConstraint { @@ -41,7 +41,7 @@ public class Exists implements ADQLConstraint { private ADQLQuery subQuery; /** Position of this {@link Exists} in the given ADQL query string. - * @since 1.3 */ + * @since 1.4 */ private TextPosition position = null; /** @@ -97,7 +97,7 @@ public class Exists implements ADQLConstraint { * Set the position of this {@link Exists} in the given ADQL query string. * * @param position New position of this {@link Exists}. - * @since 1.3 + * @since 1.4 */ public final void setPosition(final TextPosition position){ this.position = position; diff --git a/src/adql/query/constraint/In.java b/src/adql/query/constraint/In.java index a507984..67f37e4 100644 --- a/src/adql/query/constraint/In.java +++ b/src/adql/query/constraint/In.java @@ -16,8 +16,8 @@ package adql.query.constraint; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomishes Rechen Institute (ARI) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institute (ARI) */ import java.util.NoSuchElementException; @@ -37,7 +37,7 @@ import adql.query.operand.ADQLOperand; * either in the given values list or in the results of the given sub-query, else it returns false.

* * @author Grégory Mantelet (CDS;ARI) - * @version 1.3 (05/2014) + * @version 1.4 (06/2015) */ public class In implements ADQLConstraint { @@ -54,7 +54,7 @@ public class In implements ADQLConstraint { private boolean notIn = false; /** Position of this {@link In} in the given ADQL query string. - * @since 1.3 */ + * @since 1.4 */ private TextPosition position = null; /** @@ -278,7 +278,7 @@ public class In implements ADQLConstraint { * Set the position of this {@link In} in the given ADQL query string. * * @param position New position of this {@link In}. - * @since 1.3 + * @since 1.4 */ public final void setPosition(final TextPosition position){ this.position = position; diff --git a/src/adql/query/constraint/IsNull.java b/src/adql/query/constraint/IsNull.java index 529a140..56c6cdf 100644 --- a/src/adql/query/constraint/IsNull.java +++ b/src/adql/query/constraint/IsNull.java @@ -16,8 +16,8 @@ package adql.query.constraint; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomishes Rechen Institute (ARI) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institute (ARI) */ import java.util.NoSuchElementException; @@ -31,7 +31,7 @@ import adql.query.operand.ADQLColumn; * Represents a comparison between a column to the NULL value. * * @author Grégory Mantelet (CDS;ARI) - * @version 1.3 (05/2014) + * @version 1.4 (06/2015) */ public class IsNull implements ADQLConstraint { @@ -42,7 +42,7 @@ public class IsNull implements ADQLConstraint { private boolean isNotNull = false; /** Position of this {@link IsNull} in the given ADQL query string. - * @since 1.3 */ + * @since 1.4 */ private TextPosition position = null; /** @@ -131,7 +131,7 @@ public class IsNull implements ADQLConstraint { * Set the position of this {@link IsNull} in the given ADQL query string. * * @param position New position of this {@link IsNull}. - * @since 1.3 + * @since 1.4 */ public final void setPosition(final TextPosition position){ this.position = position; diff --git a/src/adql/query/constraint/NotConstraint.java b/src/adql/query/constraint/NotConstraint.java index 8d6f400..9f5a757 100644 --- a/src/adql/query/constraint/NotConstraint.java +++ b/src/adql/query/constraint/NotConstraint.java @@ -16,8 +16,8 @@ package adql.query.constraint; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomishes Rechen Institute (ARI) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institute (ARI) */ import java.util.NoSuchElementException; @@ -30,14 +30,14 @@ import adql.query.TextPosition; * Lets apply the logical operator NOT on any constraint. * * @author Grégory Mantelet (CDS;ARI) - * @version 1.3 (05/2014) + * @version 1.4 (06/2015) */ public class NotConstraint implements ADQLConstraint { private ADQLConstraint constraint; /** Position of this {@link NotConstraint} in the ADQL query string. - * @since 1.3 */ + * @since 1.4 */ private TextPosition position = null; /** @@ -71,7 +71,7 @@ public class NotConstraint implements ADQLConstraint { * Set the position of this {@link NotConstraint} in the given ADQL query string. * * @param position New position of this {@link NotConstraint}. - * @since 1.3 + * @since 1.4 */ public final void setPosition(final TextPosition position){ this.position = position; diff --git a/src/adql/query/from/ADQLJoin.java b/src/adql/query/from/ADQLJoin.java index 6cbc8c5..31b4f74 100644 --- a/src/adql/query/from/ADQLJoin.java +++ b/src/adql/query/from/ADQLJoin.java @@ -16,8 +16,8 @@ package adql.query.from; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2012-2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomishes Rechen Institute (ARI) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.util.ArrayList; @@ -30,7 +30,7 @@ import java.util.NoSuchElementException; import adql.db.DBColumn; import adql.db.DBCommonColumn; import adql.db.SearchColumnList; -import adql.db.exception.UnresolvedJoin; +import adql.db.exception.UnresolvedJoinException; import adql.query.ADQLIterator; import adql.query.ADQLObject; import adql.query.ClauseConstraints; @@ -42,7 +42,7 @@ import adql.query.operand.ADQLColumn; * Defines a join between two "tables". * * @author Grégory Mantelet (CDS;ARI) - * @version 1.3 (05/2014) + * @version 1.4 (06/2015) */ public abstract class ADQLJoin implements ADQLObject, FromContent { @@ -62,7 +62,7 @@ public abstract class ADQLJoin implements ADQLObject, FromContent { protected ArrayList lstColumns = null; /** Position of this {@link ADQLJoin} in the given ADQL query string. - * @since 1.3 */ + * @since 1.4 */ private TextPosition position = null; /* ************ */ @@ -363,7 +363,7 @@ public abstract class ADQLJoin implements ADQLObject, FromContent { } @Override - public SearchColumnList getDBColumns() throws UnresolvedJoin{ + public SearchColumnList getDBColumns() throws UnresolvedJoinException{ SearchColumnList list = new SearchColumnList(); SearchColumnList leftList = leftTable.getDBColumns(); SearchColumnList rightList = rightTable.getDBColumns(); @@ -431,20 +431,20 @@ public abstract class ADQLJoin implements ADQLObject, FromContent { } } - public final static DBColumn findExactlyOneColumn(final String columnName, final byte caseSensitive, final SearchColumnList list, final boolean leftList) throws UnresolvedJoin{ + public final static DBColumn findExactlyOneColumn(final String columnName, final byte caseSensitive, final SearchColumnList list, final boolean leftList) throws UnresolvedJoinException{ DBColumn result = findAtMostOneColumn(columnName, caseSensitive, list, leftList); if (result == null) - throw new UnresolvedJoin("Column \"" + columnName + "\" specified in USING clause does not exist in " + (leftList ? "left" : "right") + " table!"); + throw new UnresolvedJoinException("Column \"" + columnName + "\" specified in USING clause does not exist in " + (leftList ? "left" : "right") + " table!"); else return result; } - public final static DBColumn findAtMostOneColumn(final String columnName, final byte caseSensitive, final SearchColumnList list, final boolean leftList) throws UnresolvedJoin{ + public final static DBColumn findAtMostOneColumn(final String columnName, final byte caseSensitive, final SearchColumnList list, final boolean leftList) throws UnresolvedJoinException{ ArrayList result = list.search(null, null, null, columnName, caseSensitive); if (result.isEmpty()) return null; else if (result.size() > 1) - throw new UnresolvedJoin("Common column name \"" + columnName + "\" appears more than once in " + (leftList ? "left" : "right") + " table!"); + throw new UnresolvedJoinException("Common column name \"" + columnName + "\" appears more than once in " + (leftList ? "left" : "right") + " table!"); else return result.get(0); } @@ -486,4 +486,4 @@ public abstract class ADQLJoin implements ADQLObject, FromContent { @Override public abstract ADQLObject getCopy() throws Exception; -} \ No newline at end of file +} diff --git a/src/adql/query/from/FromContent.java b/src/adql/query/from/FromContent.java index e7ad5c2..a3df9cc 100644 --- a/src/adql/query/from/FromContent.java +++ b/src/adql/query/from/FromContent.java @@ -16,15 +16,15 @@ package adql.query.from; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2012-2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomishes Rechen Institute (ARI) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.util.ArrayList; import adql.db.DBColumn; import adql.db.SearchColumnList; -import adql.db.exception.UnresolvedJoin; +import adql.db.exception.UnresolvedJoinException; import adql.query.ADQLObject; import adql.query.TextPosition; @@ -33,7 +33,7 @@ import adql.query.TextPosition; * It could be either a table ({@link ADQLTable}) or a join ({@link ADQLJoin}). * * @author Grégory Mantelet (CDS;ARI) - * @version 1.3 (05/2014) + * @version 1.4 (06/2015) */ public interface FromContent extends ADQLObject { @@ -43,9 +43,9 @@ public interface FromContent extends ADQLObject { *

Note: In the most cases, this list is generated on the fly !

* * @return All the available {@link DBColumn}s. - * @throws UnresolvedJoin If a join is not possible. + * @throws UnresolvedJoinException If a join is not possible. */ - public SearchColumnList getDBColumns() throws UnresolvedJoin; + public SearchColumnList getDBColumns() throws UnresolvedJoinException; /** * Gets all {@link ADQLTable} instances contained in this FROM part (itself included, if it is an {@link ADQLTable}). @@ -72,7 +72,7 @@ public interface FromContent extends ADQLObject { * Set the position of this {@link FromContent} in the given ADQL query string. * * @param position New position of this {@link FromContent}. - * @since 1.3 + * @since 1.4 */ public void setPosition(final TextPosition position); diff --git a/src/adql/query/operand/ADQLColumn.java b/src/adql/query/operand/ADQLColumn.java index 590f8b5..eebce23 100644 --- a/src/adql/query/operand/ADQLColumn.java +++ b/src/adql/query/operand/ADQLColumn.java @@ -16,7 +16,8 @@ package adql.query.operand; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import adql.db.DBColumn; @@ -30,10 +31,10 @@ import adql.query.from.ADQLTable; /** * Represents the complete (literal) reference to a column ({schema(s)}.{table}.{column}). * - * @author Grégory Mantelet (CDS) - * @version 07/2011 + * @author Grégory Mantelet (CDS;ARI) + * @version 1.4 (06/2015) */ -public class ADQLColumn implements ADQLOperand { +public class ADQLColumn implements ADQLOperand, UnknownType { /** Position in the original ADQL query string. */ private TextPosition position = null; @@ -59,6 +60,10 @@ public class ADQLColumn implements ADQLOperand { /** The {@link ADQLTable} which is supposed to contain this column. By default, this field is automatically filled by {@link adql.db.DBChecker}. */ private ADQLTable adqlTable = null; + /** Type expected by the parser. + * @since 1.3 */ + private char expectedType = '?'; + /* ************ */ /* CONSTRUCTORS */ /* ************ */ @@ -452,14 +457,29 @@ public class ADQLColumn implements ADQLOperand { /* ***************** */ /* INHERITED METHODS */ /* ***************** */ + @Override + public char getExpectedType(){ + return expectedType; + } + + @Override + public void setExpectedType(final char c){ + expectedType = c; + } + @Override public boolean isNumeric(){ - return true; + return (dbLink == null || dbLink.getDatatype() == null || dbLink.getDatatype().isNumeric()); } @Override public boolean isString(){ - return true; + return (dbLink == null || dbLink.getDatatype() == null || dbLink.getDatatype().isString()); + } + + @Override + public boolean isGeometry(){ + return (dbLink == null || dbLink.getDatatype() == null || dbLink.getDatatype().isGeometry()); } @Override diff --git a/src/adql/query/operand/ADQLOperand.java b/src/adql/query/operand/ADQLOperand.java index 61dc647..413546b 100644 --- a/src/adql/query/operand/ADQLOperand.java +++ b/src/adql/query/operand/ADQLOperand.java @@ -16,22 +16,42 @@ package adql.query.operand; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import adql.query.ADQLObject; /** *

Any ADQL operand (an operation, a constant, a column name, a function, ...) must implement this interface - * and indicates whether it corresponds to a numeric or a string value.

+ * and indicates whether it corresponds to a numeric, a string or a geometrical region value.

* - * @author Grégory Mantelet (CDS) - * @version 11/2010 + * @author Grégory Mantelet (CDS;ARI) + * @version 1.3 (10/2014) */ public interface ADQLOperand extends ADQLObject { + /** + * Tell whether this operand is numeric or not. + * + * @return true if this operand is numeric, false otherwise. + */ public boolean isNumeric(); + /** + * Tell whether this operand is a string or not. + * + * @return true if this operand is a string, false otherwise. + */ public boolean isString(); + /** + * Tell whether this operand is a geometrical region or not. + * + * @return true if this operand is a geometry, false otherwise. + * + * @since 1.3 + */ + public boolean isGeometry(); + } diff --git a/src/adql/query/operand/Concatenation.java b/src/adql/query/operand/Concatenation.java index a09b790..24536c7 100644 --- a/src/adql/query/operand/Concatenation.java +++ b/src/adql/query/operand/Concatenation.java @@ -16,7 +16,8 @@ package adql.query.operand; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import adql.query.ADQLList; @@ -25,8 +26,8 @@ import adql.query.ADQLObject; /** * Represents a concatenation in ADQL (ex: "_s_ra" || ':' || "_s_dec"). * - * @author Grégory Mantelet (CDS) - * @version 11/2010 + * @author Grégory Mantelet (CDS;ARI) + * @version 1.3 (10/2014) */ public final class Concatenation extends ADQLList implements ADQLOperand { @@ -65,12 +66,19 @@ public final class Concatenation extends ADQLList implements ADQLOp return "||"; } + @Override public final boolean isNumeric(){ return false; } + @Override public final boolean isString(){ return true; } + @Override + public final boolean isGeometry(){ + return false; + } + } \ No newline at end of file diff --git a/src/adql/query/operand/NegativeOperand.java b/src/adql/query/operand/NegativeOperand.java index 39bb608..7b4bc59 100644 --- a/src/adql/query/operand/NegativeOperand.java +++ b/src/adql/query/operand/NegativeOperand.java @@ -16,8 +16,8 @@ package adql.query.operand; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomishes Rechen Institute (ARI) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.util.NoSuchElementException; @@ -30,13 +30,14 @@ import adql.query.TextPosition; * Lets putting a minus sign in front of any numeric operand. * * @author Grégory Mantelet (CDS;ARI) - * @version 05/2014 + * @version 1.4 (06/2015) */ public final class NegativeOperand implements ADQLOperand { /** The negativated operand. */ private ADQLOperand operand; - /** Position of this operand. */ + /** Position of this operand. + * @since 1.4 */ private TextPosition position = null; /** @@ -93,12 +94,20 @@ public final class NegativeOperand implements ADQLOperand { * Sets the position at which this {@link NegativeOperand} has been found in the original ADQL query string. * * @param pos Position of this {@link NegativeOperand}. - * @since 1.3 + * @since 1.4 */ public final void setPosition(final TextPosition position){ this.position = position; } + /** Always returns false. + * @see adql.query.operand.ADQLOperand#isGeometry() + */ + @Override + public final boolean isGeometry(){ + return false; + } + @Override public ADQLObject getCopy() throws Exception{ NegativeOperand copy = new NegativeOperand((ADQLOperand)operand.getCopy()); diff --git a/src/adql/query/operand/NumericConstant.java b/src/adql/query/operand/NumericConstant.java index a0c93fd..53e4937 100644 --- a/src/adql/query/operand/NumericConstant.java +++ b/src/adql/query/operand/NumericConstant.java @@ -16,8 +16,8 @@ package adql.query.operand; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomishes Rechen Institute (ARI) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import adql.query.ADQLIterator; @@ -29,12 +29,14 @@ import adql.query.TextPosition; * A numeric (integer, double, ...) constant. * * @author Grégory Mantelet (CDS;ARI) - * @version 05/2014 + * @version 1.4 (06/2015) */ public final class NumericConstant implements ADQLOperand { private String value; - /** Position of this operand. */ + + /** Position of this operand. + * @since 1.4 */ private TextPosition position = null; /** @@ -175,12 +177,20 @@ public final class NumericConstant implements ADQLOperand { * Sets the position at which this {@link NumericConstant} has been found in the original ADQL query string. * * @param pos Position of this {@link NumericConstant}. - * @since 1.3 + * @since 1.4 */ public final void setPosition(final TextPosition position){ this.position = position; } + /** Always returns false. + * @see adql.query.operand.ADQLOperand#isGeometry() + */ + @Override + public final boolean isGeometry(){ + return false; + } + @Override public ADQLObject getCopy(){ return new NumericConstant(this); diff --git a/src/adql/query/operand/Operation.java b/src/adql/query/operand/Operation.java index 09daee2..1418ae1 100644 --- a/src/adql/query/operand/Operation.java +++ b/src/adql/query/operand/Operation.java @@ -16,8 +16,8 @@ package adql.query.operand; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomishes Rechen Institute (ARI) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.util.NoSuchElementException; @@ -30,7 +30,7 @@ import adql.query.TextPosition; * It represents a simple numeric operation (sum, difference, multiplication and division). * * @author Grégory Mantelet (CDS;ARI) - * @version 05/2014 + * @version 1.4 (06/2015) * * @see OperationType */ @@ -48,7 +48,8 @@ public class Operation implements ADQLOperand { /** Part of the operation at the right of the operator. */ private ADQLOperand rightOperand; - /** Position of the operation in the ADQL query string. */ + /** Position of the operation in the ADQL query string. + * @since 1.4 */ private TextPosition position = null; /** @@ -194,12 +195,20 @@ public class Operation implements ADQLOperand { * Sets the position at which this {@link WrappedOperand} has been found in the original ADQL query string. * * @param pos Position of this {@link WrappedOperand}. - * @since 1.3 + * @since 1.4 */ public final void setPosition(final TextPosition position){ this.position = position; } + /** Always returns false. + * @see adql.query.operand.ADQLOperand#isGeometry() + */ + @Override + public final boolean isGeometry(){ + return false; + } + @Override public ADQLObject getCopy() throws Exception{ return new Operation(this); @@ -269,4 +278,4 @@ public class Operation implements ADQLOperand { return leftOperand.toADQL() + operation.toADQL() + rightOperand.toADQL(); } -} \ No newline at end of file +} diff --git a/src/adql/query/operand/StringConstant.java b/src/adql/query/operand/StringConstant.java index 1551912..998bda9 100644 --- a/src/adql/query/operand/StringConstant.java +++ b/src/adql/query/operand/StringConstant.java @@ -16,8 +16,8 @@ package adql.query.operand; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomishes Rechen Institute (ARI) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import adql.query.ADQLIterator; @@ -29,12 +29,14 @@ import adql.query.TextPosition; * A string constant. * * @author Grégory Mantelet (CDS;ARI) - * @version 05/2014 + * @version 1.4 (06/2015) */ public final class StringConstant implements ADQLOperand { private String value; - /** Position of this operand. */ + + /** Position of this operand. + * @since 1.4 */ private TextPosition position = null; public StringConstant(String value){ @@ -72,12 +74,17 @@ public final class StringConstant implements ADQLOperand { * Sets the position at which this {@link StringConstant} has been found in the original ADQL query string. * * @param pos Position of this {@link StringConstant}. - * @since 1.3 + * @since 1.4 */ public final void setPosition(final TextPosition position){ this.position = position; } + @Override + public final boolean isGeometry(){ + return false; + } + @Override public ADQLObject getCopy(){ return new StringConstant(this); @@ -85,7 +92,7 @@ public final class StringConstant implements ADQLOperand { @Override public String getName(){ - return "'" + value + "'"; + return toADQL(); } @Override @@ -95,7 +102,7 @@ public final class StringConstant implements ADQLOperand { @Override public String toADQL(){ - return "'" + value + "'"; + return "'" + value.replaceAll("'", "''") + "'"; } } diff --git a/src/adql/query/operand/UnknownType.java b/src/adql/query/operand/UnknownType.java new file mode 100644 index 0000000..bae9d9b --- /dev/null +++ b/src/adql/query/operand/UnknownType.java @@ -0,0 +1,52 @@ +package adql.query.operand; + +/* + * This file is part of ADQLLibrary. + * + * ADQLLibrary is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ADQLLibrary is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ADQLLibrary. If not, see . + * + * Copyright 2014 - Astronomisches Rechen Institut (ARI) + */ + +import adql.query.operand.function.UserDefinedFunction; + +/** + *

Operand whose the type can not be known at the parsing time. + * A post-parsing step with column metadata is needed to resolved their types.

+ * + *

Note: + * For the moment, only two operands are concerned: columns ({@link ADQLColumn}) and user defined functions ({@link UserDefinedFunction}). + *

+ * + * @author Grégory Mantelet (ARI) + * @version 1.3 (10/2014) + * @since 1.3 + */ +public interface UnknownType extends ADQLOperand { + + /** + * Get the type expected by the syntactic parser according to the context. + * + * @return Expected type: 'n' or 'N' for numeric, 's' or 'S' for string, 'g' or 'G' for geometry. + */ + public char getExpectedType(); + + /** + * Set the type expected for this operand. + * + * @param c Expected type: 'n' or 'N' for numeric, 's' or 'S' for string, 'g' or 'G' for geometry. + */ + public void setExpectedType(final char c); + +} diff --git a/src/adql/query/operand/WrappedOperand.java b/src/adql/query/operand/WrappedOperand.java index 92ab7bf..ca36d7a 100644 --- a/src/adql/query/operand/WrappedOperand.java +++ b/src/adql/query/operand/WrappedOperand.java @@ -16,8 +16,8 @@ package adql.query.operand; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomishes Rechen Institute (ARI) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.util.NoSuchElementException; @@ -30,13 +30,14 @@ import adql.query.TextPosition; * Lets wrapping an operand by parenthesis. * * @author Grégory Mantelet (CDS;ARI) - * @version 05/2014 + * @version 1.4 (06/2015) */ public class WrappedOperand implements ADQLOperand { /** The wrapped operand. */ private ADQLOperand operand; - /** Position of this operand. */ + /** Position of this operand. + * @since 1.4 */ private TextPosition position = null; /** @@ -80,12 +81,17 @@ public class WrappedOperand implements ADQLOperand { * Sets the position at which this {@link WrappedOperand} has been found in the original ADQL query string. * * @param pos Position of this {@link WrappedOperand}. - * @since 1.3 + * @since 1.4 */ public final void setPosition(final TextPosition position){ this.position = position; } + @Override + public final boolean isGeometry(){ + return operand.isGeometry(); + } + @Override public ADQLObject getCopy() throws Exception{ return new WrappedOperand((ADQLOperand)operand.getCopy()); diff --git a/src/adql/query/operand/function/ADQLFunction.java b/src/adql/query/operand/function/ADQLFunction.java index ed9dac7..b22ec56 100644 --- a/src/adql/query/operand/function/ADQLFunction.java +++ b/src/adql/query/operand/function/ADQLFunction.java @@ -16,8 +16,8 @@ package adql.query.operand.function; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2012-2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomishes Rechen Institute (ARI) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institute (ARI) */ import java.util.Iterator; @@ -32,12 +32,12 @@ import adql.query.operand.ADQLOperand; * Represents any kind of function. * * @author Grégory Mantelet (CDS;ARI) - * @version 1.3 (05/2014) + * @version 1.4 (06/2015) */ public abstract class ADQLFunction implements ADQLOperand { /** Position of this {@link ADQLFunction} in the ADQL query string. - * @since 1.3 */ + * @since 1.4 */ private TextPosition position = null; @Override @@ -49,7 +49,7 @@ public abstract class ADQLFunction implements ADQLOperand { * Set the position of this {@link ADQLFunction} in the ADQL query string. * * @param position New position of this {@link ADQLFunction} - * @since 1.3 + * @since 1.4 */ public final void setPosition(final TextPosition position){ this.position = position; diff --git a/src/adql/query/operand/function/DefaultUDF.java b/src/adql/query/operand/function/DefaultUDF.java index 2ab08bd..268de91 100644 --- a/src/adql/query/operand/function/DefaultUDF.java +++ b/src/adql/query/operand/function/DefaultUDF.java @@ -16,33 +16,42 @@ package adql.query.operand.function; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2012-2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), Astronomisches Rechen Institute (ARI) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ +import adql.db.FunctionDef; import adql.query.ADQLList; import adql.query.ADQLObject; import adql.query.ClauseADQL; import adql.query.TextPosition; import adql.query.operand.ADQLOperand; +import adql.translator.ADQLTranslator; +import adql.translator.TranslationException; /** * It represents any function which is not managed by ADQL. * * @author Grégory Mantelet (CDS;ARI) - * @version 1.3 (05/2014) + * @version 1.4 (06/2015) */ public final class DefaultUDF extends UserDefinedFunction { - /** Its parameters. */ + /** Define/Describe this user defined function. + * This object gives the return type and the number and type of all parameters. */ + protected FunctionDef definition = null; + + /** Its parsed parameters. */ protected final ADQLList parameters; + /** Parsed name of this UDF. */ protected final String functionName; /** * Creates a user function. * @param params Parameters of the function. */ - public DefaultUDF(final String name, ADQLOperand[] params) throws NullPointerException{ + public DefaultUDF(final String name, final ADQLOperand[] params) throws NullPointerException{ functionName = name; parameters = new ClauseADQL(); if (params != null){ @@ -58,25 +67,64 @@ public final class DefaultUDF extends UserDefinedFunction { * @throws Exception If there is an error during the copy. */ @SuppressWarnings("unchecked") - public DefaultUDF(DefaultUDF toCopy) throws Exception{ + public DefaultUDF(final DefaultUDF toCopy) throws Exception{ functionName = toCopy.functionName; parameters = (ADQLList)(toCopy.parameters.getCopy()); setPosition((toCopy.getPosition() == null) ? null : new TextPosition(toCopy.getPosition()));; } + /** + * Get the signature/definition/description of this user defined function. + * The returned object provides information on the return type and the number and type of parameters. + * + * @return Definition of this function. (MAY be NULL) + */ + public final FunctionDef getDefinition(){ + return definition; + } + + /** + *

Let set the signature/definition/description of this user defined function.

+ * + *

IMPORTANT: + * No particular checks are done here except on the function name which MUST + * be the same (case insensitive) as the name of the given definition. + * Advanced checks must have been done before calling this setter. + *

+ * + * @param def The definition applying to this parsed UDF, or NULL if none has been found. + * + * @throws IllegalArgumentException If the name in the given definition does not match the name of this parsed function. + * + * @since 1.3 + */ + public final void setDefinition(final FunctionDef def) throws IllegalArgumentException{ + if (def != null && (def.name == null || !functionName.equalsIgnoreCase(def.name))) + throw new IllegalArgumentException("The parsed function name (" + functionName + ") does not match to the name of the given UDF definition (" + def.name + ")."); + + this.definition = def; + } + @Override public final boolean isNumeric(){ - return true; + return (definition == null || definition.isNumeric()); } @Override public final boolean isString(){ - return true; + return (definition == null || definition.isString()); + } + + @Override + public final boolean isGeometry(){ + return (definition == null || definition.isGeometry()); } @Override public ADQLObject getCopy() throws Exception{ - return new DefaultUDF(this); + DefaultUDF copy = new DefaultUDF(this); + copy.setDefinition(definition); + return copy; } @Override @@ -115,4 +163,17 @@ public final class DefaultUDF extends UserDefinedFunction { return oldParam; } + @Override + public String translate(final ADQLTranslator caller) throws TranslationException{ + StringBuffer sql = new StringBuffer(functionName); + sql.append('('); + for(int i = 0; i < parameters.size(); i++){ + if (i > 0) + sql.append(',').append(' '); + sql.append(caller.translate(parameters.get(i))); + } + sql.append(')'); + return sql.toString(); + } + } diff --git a/src/adql/query/operand/function/MathFunction.java b/src/adql/query/operand/function/MathFunction.java index 88e06e0..49e1a4a 100644 --- a/src/adql/query/operand/function/MathFunction.java +++ b/src/adql/query/operand/function/MathFunction.java @@ -16,8 +16,8 @@ package adql.query.operand.function; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2012-2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomisches Rechen Institute (ARI) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import adql.query.ADQLObject; @@ -28,7 +28,7 @@ import adql.query.operand.ADQLOperand; * It represents any basic mathematical function. * * @author Grégory Mantelet (CDS;ARI) - * @version 1.3 (05/2014) + * @version 1.4 (06/2015) * * @see MathFunctionType */ @@ -151,6 +151,11 @@ public class MathFunction extends ADQLFunction { return false; } + @Override + public final boolean isGeometry(){ + return false; + } + @Override public ADQLOperand[] getParameters(){ switch(getNbParameters()){ diff --git a/src/adql/query/operand/function/SQLFunction.java b/src/adql/query/operand/function/SQLFunction.java index 588eba8..5d0748e 100644 --- a/src/adql/query/operand/function/SQLFunction.java +++ b/src/adql/query/operand/function/SQLFunction.java @@ -16,8 +16,8 @@ package adql.query.operand.function; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2011,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomishes Rechen Institute (ARI) + * Copyright 2011-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institute (ARI) */ import adql.query.ADQLObject; @@ -28,7 +28,7 @@ import adql.query.operand.ADQLOperand; * It represents any SQL function (COUNT, MAX, MIN, AVG, SUM, etc...). * * @author Grégory Mantelet (CDS;ARI) - * @version 1.3 (05/2014) + * @version 1.4 (06/2015) * * @see SQLFunctionType */ @@ -139,6 +139,11 @@ public class SQLFunction extends ADQLFunction { return false; } + @Override + public final boolean isGeometry(){ + return false; + } + @Override public ADQLOperand[] getParameters(){ if (param != null) @@ -183,4 +188,4 @@ public class SQLFunction extends ADQLFunction { return getName() + "(" + (distinct ? "DISTINCT " : "") + param.toADQL() + ")"; } -} \ No newline at end of file +} diff --git a/src/adql/query/operand/function/UserDefinedFunction.java b/src/adql/query/operand/function/UserDefinedFunction.java index 39d8778..53c2474 100644 --- a/src/adql/query/operand/function/UserDefinedFunction.java +++ b/src/adql/query/operand/function/UserDefinedFunction.java @@ -16,17 +16,68 @@ package adql.query.operand.function; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ +import adql.query.operand.UnknownType; +import adql.translator.ADQLTranslator; +import adql.translator.TranslationException; + /** * Function defined by the user (i.e. PSQL functions). * - * @author Grégory Mantelet (CDS) - * @version 01/2012 + * @author Grégory Mantelet (CDS;ARI) + * @version 1.3 (02/2015) * * @see DefaultUDF */ -public abstract class UserDefinedFunction extends ADQLFunction { +public abstract class UserDefinedFunction extends ADQLFunction implements UnknownType { + + /** Type expected by the parser. + * @since 1.3 */ + private char expectedType = '?'; + + @Override + public char getExpectedType(){ + return expectedType; + } + + @Override + public void setExpectedType(final char c){ + expectedType = c; + } + + /** + *

Translate this User Defined Function into the language supported by the given translator.

+ * + *

VERY IMPORTANT: This function MUST NOT use {@link ADQLTranslator#translate(UserDefinedFunction)} to translate itself. + * The given {@link ADQLTranslator} must be used ONLY to translate UDF's operands.

+ * + *

Implementation example (extract of {@link DefaultUDF#translate(ADQLTranslator)}):

+ *
+	 * public String translate(final ADQLTranslator caller) throws TranslationException{
+	 * 	StringBuffer sql = new StringBuffer(functionName);
+	 * 	sql.append('(');
+	 * 	for(int i = 0; i < parameters.size(); i++){
+	 *		if (i > 0)
+	 *			sql.append(',').append(' ');
+	 * 		sql.append(caller.translate(parameters.get(i)));
+	 *	}
+	 *	sql.append(')');
+	 *	return sql.toString();
+	 * }
+	 * 
+ * + * + * @param caller Translator to use in order to translate ONLY function parameters. + * + * @return The translation of this UDF into the language supported by the given translator. + * + * @throws TranslationException If one of the parameters can not be translated. + * + * @since 1.3 + */ + public abstract String translate(final ADQLTranslator caller) throws TranslationException; } diff --git a/src/adql/query/operand/function/geometry/AreaFunction.java b/src/adql/query/operand/function/geometry/AreaFunction.java index aa59e83..bb9003d 100644 --- a/src/adql/query/operand/function/geometry/AreaFunction.java +++ b/src/adql/query/operand/function/geometry/AreaFunction.java @@ -16,8 +16,8 @@ package adql.query.operand.function.geometry; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomishes Rechen Institute (ARI) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import adql.query.ADQLObject; @@ -34,7 +34,7 @@ import adql.query.operand.ADQLOperand; *

Inappropriate geometries for this construct (e.g. POINT) SHOULD either return zero or throw an error message. This choice must be done in an extended class of {@link AreaFunction}.

* * @author Grégory Mantelet (CDS;ARI) - * @version 1.3 (05/2014) + * @version 1.4 (06/2015) */ public class AreaFunction extends GeometryFunction { @@ -108,6 +108,11 @@ public class AreaFunction extends GeometryFunction { return false; } + @Override + public boolean isGeometry(){ + return false; + } + @Override public ADQLOperand[] getParameters(){ return new ADQLOperand[]{parameter.getValue()}; @@ -147,4 +152,4 @@ public class AreaFunction extends GeometryFunction { throw new ArrayIndexOutOfBoundsException("No " + index + "-th parameter for the function \"" + getName() + "\" !"); } -} \ No newline at end of file +} diff --git a/src/adql/query/operand/function/geometry/BoxFunction.java b/src/adql/query/operand/function/geometry/BoxFunction.java index 485c098..7585fd6 100644 --- a/src/adql/query/operand/function/geometry/BoxFunction.java +++ b/src/adql/query/operand/function/geometry/BoxFunction.java @@ -16,8 +16,8 @@ package adql.query.operand.function.geometry; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomishes Rechen Institute (ARI) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import adql.query.ADQLObject; @@ -38,9 +38,8 @@ import adql.query.operand.ADQLOperand; * In this second example the coordinates of the center position are extracted from a coordinate's column reference. *

* - * * @author Grégory Mantelet (CDS;ARI) - * @version 1.3 (05/2014) + * @version 1.4 (06/2015) */ public class BoxFunction extends GeometryFunction { @@ -110,6 +109,11 @@ public class BoxFunction extends GeometryFunction { @Override public boolean isString(){ + return false; + } + + @Override + public boolean isGeometry(){ return true; } diff --git a/src/adql/query/operand/function/geometry/CentroidFunction.java b/src/adql/query/operand/function/geometry/CentroidFunction.java index 8ef5af3..23a2b78 100644 --- a/src/adql/query/operand/function/geometry/CentroidFunction.java +++ b/src/adql/query/operand/function/geometry/CentroidFunction.java @@ -16,8 +16,8 @@ package adql.query.operand.function.geometry; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomishes Rechen Institute (ARI) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import adql.query.ADQLObject; @@ -36,7 +36,7 @@ import adql.query.operand.ADQLOperand; *

* * @author Grégory Mantelet (CDS;ARI) - * @version 1.3 (05/2014) + * @version 1.4 (06/2015) */ public class CentroidFunction extends GeometryFunction { @@ -88,6 +88,11 @@ public class CentroidFunction extends GeometryFunction { return false; } + @Override + public boolean isGeometry(){ + return false; + } + @Override public ADQLOperand[] getParameters(){ return new ADQLOperand[]{parameter.getValue()}; @@ -127,4 +132,4 @@ public class CentroidFunction extends GeometryFunction { throw new ArrayIndexOutOfBoundsException("No " + index + "-th parameter for the function \"" + getName() + "\" !"); } -} \ No newline at end of file +} diff --git a/src/adql/query/operand/function/geometry/CircleFunction.java b/src/adql/query/operand/function/geometry/CircleFunction.java index aa0efdb..3d3dc6b 100644 --- a/src/adql/query/operand/function/geometry/CircleFunction.java +++ b/src/adql/query/operand/function/geometry/CircleFunction.java @@ -16,8 +16,8 @@ package adql.query.operand.function.geometry; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomishes Rechen Institute (ARI) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import adql.query.ADQLObject; @@ -36,7 +36,7 @@ import adql.query.operand.ADQLOperand; *

* * @author Grégory Mantelet (CDS;ARI) - * @version 1.3 (05/2014) + * @version 1.4 (06/2015) */ public class CircleFunction extends GeometryFunction { @@ -100,6 +100,11 @@ public class CircleFunction extends GeometryFunction { @Override public boolean isString(){ + return false; + } + + @Override + public boolean isGeometry(){ return true; } diff --git a/src/adql/query/operand/function/geometry/ContainsFunction.java b/src/adql/query/operand/function/geometry/ContainsFunction.java index 0a99325..e26fa97 100644 --- a/src/adql/query/operand/function/geometry/ContainsFunction.java +++ b/src/adql/query/operand/function/geometry/ContainsFunction.java @@ -16,8 +16,8 @@ package adql.query.operand.function.geometry; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomishes Rechen Institute (ARI) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import adql.query.ADQLObject; @@ -42,7 +42,7 @@ import adql.query.operand.ADQLOperand; *

* * @author Grégory Mantelet (CDS;ARI) - * @version 1.3 (05/2014) + * @version 1.4 (06/2015) */ public class ContainsFunction extends GeometryFunction { @@ -101,6 +101,11 @@ public class ContainsFunction extends GeometryFunction { return false; } + @Override + public boolean isGeometry(){ + return false; + } + /** * @return The leftParam. */ @@ -186,4 +191,4 @@ public class ContainsFunction extends GeometryFunction { return replaced; } -} \ No newline at end of file +} diff --git a/src/adql/query/operand/function/geometry/DistanceFunction.java b/src/adql/query/operand/function/geometry/DistanceFunction.java index f7aa889..fb49a93 100644 --- a/src/adql/query/operand/function/geometry/DistanceFunction.java +++ b/src/adql/query/operand/function/geometry/DistanceFunction.java @@ -16,8 +16,8 @@ package adql.query.operand.function.geometry; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomishes Rechen Institute (ARI) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import adql.query.ADQLObject; @@ -35,7 +35,7 @@ import adql.query.operand.ADQLOperand; * coordinate system with GEOCENTER reference position.

* * @author Grégory Mantelet (CDS;ARI) - * @version 1.3 (05/2014) + * @version 1.4 (06/2015) */ public class DistanceFunction extends GeometryFunction { @@ -99,6 +99,11 @@ public class DistanceFunction extends GeometryFunction { return false; } + @Override + public boolean isGeometry(){ + return false; + } + /** * Gets the first point. * @@ -200,4 +205,4 @@ public class DistanceFunction extends GeometryFunction { return replaced; } -} \ No newline at end of file +} diff --git a/src/adql/query/operand/function/geometry/ExtractCoord.java b/src/adql/query/operand/function/geometry/ExtractCoord.java index 570b79a..edff24a 100644 --- a/src/adql/query/operand/function/geometry/ExtractCoord.java +++ b/src/adql/query/operand/function/geometry/ExtractCoord.java @@ -16,8 +16,8 @@ package adql.query.operand.function.geometry; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomishes Rechen Institute (ARI) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import adql.query.ADQLObject; @@ -36,7 +36,7 @@ import adql.query.operand.ADQLOperand; *

* * @author Grégory Mantelet (CDS;ARI) - * @version 1.3 (05/2014) + * @version 1.4 (06/2015) */ public class ExtractCoord extends GeometryFunction { @@ -98,6 +98,11 @@ public class ExtractCoord extends GeometryFunction { return false; } + @Override + public boolean isGeometry(){ + return false; + } + @Override public ADQLOperand[] getParameters(){ return new ADQLOperand[]{point.getValue()}; @@ -137,4 +142,4 @@ public class ExtractCoord extends GeometryFunction { throw new ArrayIndexOutOfBoundsException("No " + index + "-th parameter for the function \"" + getName() + "\" !"); } -} \ No newline at end of file +} diff --git a/src/adql/query/operand/function/geometry/ExtractCoordSys.java b/src/adql/query/operand/function/geometry/ExtractCoordSys.java index 15cfa30..3865c2d 100644 --- a/src/adql/query/operand/function/geometry/ExtractCoordSys.java +++ b/src/adql/query/operand/function/geometry/ExtractCoordSys.java @@ -16,8 +16,8 @@ package adql.query.operand.function.geometry; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomishes Rechen Institute (ARI) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import adql.query.ADQLObject; @@ -36,7 +36,7 @@ import adql.query.operand.ADQLOperand; *

* * @author Grégory Mantelet (CDS;ARI) - * @version 1.3 (05/2014) + * @version 1.4 (06/2015) */ public class ExtractCoordSys extends GeometryFunction { @@ -85,6 +85,11 @@ public class ExtractCoordSys extends GeometryFunction { return true; } + @Override + public boolean isGeometry(){ + return false; + } + @Override public ADQLOperand[] getParameters(){ return new ADQLOperand[]{geomExpr.getValue()}; @@ -124,4 +129,4 @@ public class ExtractCoordSys extends GeometryFunction { throw new ArrayIndexOutOfBoundsException("No " + index + "-th parameter for the function " + getName() + " !"); } -} \ No newline at end of file +} diff --git a/src/adql/query/operand/function/geometry/GeometryFunction.java b/src/adql/query/operand/function/geometry/GeometryFunction.java index 7eb29e0..1892001 100644 --- a/src/adql/query/operand/function/geometry/GeometryFunction.java +++ b/src/adql/query/operand/function/geometry/GeometryFunction.java @@ -16,22 +16,24 @@ package adql.query.operand.function.geometry; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomishes Rechen Institute (ARI) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ +import adql.parser.ParseException; import adql.query.ADQLIterator; import adql.query.ADQLObject; import adql.query.TextPosition; import adql.query.operand.ADQLColumn; import adql.query.operand.ADQLOperand; +import adql.query.operand.StringConstant; import adql.query.operand.function.ADQLFunction; /** *

It represents any geometric function of ADQL.

* * @author Grégory Mantelet (CDS;ARI) - * @version 1.3 (05/2014) + * @version 1.4 (06/2015) */ public abstract class GeometryFunction extends ADQLFunction { @@ -83,13 +85,13 @@ public abstract class GeometryFunction extends ADQLFunction { * @param coordSys Its new coordinate system. * @throws UnsupportedOperationException If this function is not associated with a coordinate system. * @throws NullPointerException If the given operand is null. - * @throws Exception If the given operand is not a string. + * @throws ParseException If the given operand is not a string. */ - public void setCoordinateSystem(ADQLOperand coordSys) throws UnsupportedOperationException, NullPointerException, Exception{ + public void setCoordinateSystem(ADQLOperand coordSys) throws UnsupportedOperationException, NullPointerException, ParseException{ if (coordSys == null) - throw new NullPointerException(""); + this.coordSys = new StringConstant(""); else if (!coordSys.isString()) - throw new Exception("A coordinate system must be a string literal: \"" + coordSys.toADQL() + "\" is not a string operand !"); + throw new ParseException("A coordinate system must be a string literal: \"" + coordSys.toADQL() + "\" is not a string operand!"); else{ this.coordSys = coordSys; setPosition(null); @@ -101,13 +103,13 @@ public abstract class GeometryFunction extends ADQLFunction { * which, in general, is either a GeometryFunction or a Column. * * @author Grégory Mantelet (CDS;ARI) - * @version 05/2014 + * @version 1.4 (06/2015) */ public static final class GeometryValue< F extends GeometryFunction > implements ADQLOperand { private ADQLColumn column; private F geomFunct; /** Position of this {@link GeometryValue} in the ADQL query string. - * @since 1.3 */ + * @since 1.4 */ private TextPosition position = null; public GeometryValue(ADQLColumn col) throws NullPointerException{ @@ -172,6 +174,11 @@ public abstract class GeometryFunction extends ADQLFunction { return position; } + @Override + public boolean isGeometry(){ + return getValue().isGeometry(); + } + @Override public ADQLObject getCopy() throws Exception{ return new GeometryValue(this); @@ -192,4 +199,4 @@ public abstract class GeometryFunction extends ADQLFunction { return getValue().toADQL(); } } -} \ No newline at end of file +} diff --git a/src/adql/query/operand/function/geometry/IntersectsFunction.java b/src/adql/query/operand/function/geometry/IntersectsFunction.java index 046f5e4..8b5d8b9 100644 --- a/src/adql/query/operand/function/geometry/IntersectsFunction.java +++ b/src/adql/query/operand/function/geometry/IntersectsFunction.java @@ -16,8 +16,8 @@ package adql.query.operand.function.geometry; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomishes Rechen Institute (ARI) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import adql.query.ADQLObject; @@ -43,7 +43,7 @@ import adql.query.operand.ADQLOperand; *

* * @author Grégory Mantelet (CDS;ARI) - * @version 1.3 (05/2014) + * @version 1.4 (06/2015) */ public class IntersectsFunction extends GeometryFunction { @@ -102,6 +102,11 @@ public class IntersectsFunction extends GeometryFunction { return false; } + @Override + public boolean isGeometry(){ + return false; + } + /** * @return The leftParam. */ @@ -187,4 +192,4 @@ public class IntersectsFunction extends GeometryFunction { return replaced; } -} \ No newline at end of file +} diff --git a/src/adql/query/operand/function/geometry/PointFunction.java b/src/adql/query/operand/function/geometry/PointFunction.java index 38a6b45..1d5fd26 100644 --- a/src/adql/query/operand/function/geometry/PointFunction.java +++ b/src/adql/query/operand/function/geometry/PointFunction.java @@ -16,8 +16,8 @@ package adql.query.operand.function.geometry; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomishes Rechen Institute (ARI) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import adql.query.ADQLObject; @@ -35,7 +35,7 @@ import adql.query.operand.ADQLOperand; * to the ICRS coordinate system with GEOCENTER reference position.

* * @author Grégory Mantelet (CDS;ARI) - * @version 1.3 (05/2014) + * @version 1.4 (06/2015) */ public class PointFunction extends GeometryFunction { @@ -59,7 +59,7 @@ public class PointFunction extends GeometryFunction { super(coordinateSystem); if (firstCoord == null || secondCoord == null) - throw new NullPointerException("The POINT function must have non-null coordinates !"); + throw new NullPointerException("The POINT function must have non-null coordinates!"); coord1 = firstCoord; coord2 = secondCoord; @@ -148,6 +148,11 @@ public class PointFunction extends GeometryFunction { @Override public boolean isString(){ + return false; + } + + @Override + public boolean isGeometry(){ return true; } diff --git a/src/adql/query/operand/function/geometry/PolygonFunction.java b/src/adql/query/operand/function/geometry/PolygonFunction.java index 3dfede4..a4d07a2 100644 --- a/src/adql/query/operand/function/geometry/PolygonFunction.java +++ b/src/adql/query/operand/function/geometry/PolygonFunction.java @@ -16,10 +16,11 @@ package adql.query.operand.function.geometry; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomishes Rechen Institute (ARI) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ +import java.util.Collection; import java.util.Vector; import adql.query.ADQLObject; @@ -41,7 +42,7 @@ import adql.query.operand.ADQLOperand; * according to the STC coordinate system with GEOCENTER reference position.

* * @author Grégory Mantelet (CDS;ARI) - * @version 1.3 (05/2014) + * @version 1.4 (06/2015) */ public class PolygonFunction extends GeometryFunction { @@ -79,7 +80,7 @@ public class PolygonFunction extends GeometryFunction { * @throws NullPointerException If one of the parameters is null. * @throws Exception If there is another error. */ - public PolygonFunction(ADQLOperand coordSystem, Vector coords) throws UnsupportedOperationException, NullPointerException, Exception{ + public PolygonFunction(ADQLOperand coordSystem, Collection coords) throws UnsupportedOperationException, NullPointerException, Exception{ super(coordSystem); if (coords == null || coords.size() < 6) throw new NullPointerException("A POLYGON function must have at least 3 2-D coordinates !"); @@ -119,6 +120,11 @@ public class PolygonFunction extends GeometryFunction { @Override public boolean isString(){ + return false; + } + + @Override + public boolean isGeometry(){ return true; } diff --git a/src/adql/query/operand/function/geometry/RegionFunction.java b/src/adql/query/operand/function/geometry/RegionFunction.java index 404423d..30e9e00 100644 --- a/src/adql/query/operand/function/geometry/RegionFunction.java +++ b/src/adql/query/operand/function/geometry/RegionFunction.java @@ -16,8 +16,8 @@ package adql.query.operand.function.geometry; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomisches Rechen Institute (ARI) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import adql.query.ADQLObject; @@ -38,7 +38,7 @@ import adql.query.operand.ADQLOperand; * Inappropriate geometries for this construct SHOULD throw an error message, to be defined by the service making use of ADQL.

* * @author Grégory Mantelet (CDS;ARI) - * @version 1.3 (05/2014) + * @version 1.4 (06/2015) */ public class RegionFunction extends GeometryFunction { @@ -88,6 +88,11 @@ public class RegionFunction extends GeometryFunction { @Override public boolean isString(){ + return false; + } + + @Override + public boolean isGeometry(){ return true; } diff --git a/src/adql/translator/ADQLTranslator.java b/src/adql/translator/ADQLTranslator.java index 7ec6bf9..1174f08 100644 --- a/src/adql/translator/ADQLTranslator.java +++ b/src/adql/translator/ADQLTranslator.java @@ -16,7 +16,8 @@ package adql.translator; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import adql.query.ADQLList; @@ -28,7 +29,6 @@ import adql.query.ClauseSelect; import adql.query.ColumnReference; import adql.query.SelectAllColumns; import adql.query.SelectItem; - import adql.query.constraint.ADQLConstraint; import adql.query.constraint.Between; import adql.query.constraint.Comparison; @@ -60,11 +60,11 @@ import adql.query.operand.function.geometry.DistanceFunction; import adql.query.operand.function.geometry.ExtractCoord; import adql.query.operand.function.geometry.ExtractCoordSys; import adql.query.operand.function.geometry.GeometryFunction; +import adql.query.operand.function.geometry.GeometryFunction.GeometryValue; import adql.query.operand.function.geometry.IntersectsFunction; import adql.query.operand.function.geometry.PointFunction; import adql.query.operand.function.geometry.PolygonFunction; import adql.query.operand.function.geometry.RegionFunction; -import adql.query.operand.function.geometry.GeometryFunction.GeometryValue; /** * Translates ADQL objects into any language (i.e. SQL). diff --git a/src/adql/translator/JDBCTranslator.java b/src/adql/translator/JDBCTranslator.java new file mode 100644 index 0000000..0ba1b0d --- /dev/null +++ b/src/adql/translator/JDBCTranslator.java @@ -0,0 +1,908 @@ +package adql.translator; + +/* + * This file is part of ADQLLibrary. + * + * ADQLLibrary is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ADQLLibrary is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ADQLLibrary. If not, see . + * + * Copyright 2015 - Astronomisches Rechen Institut (ARI) + */ + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; + +import tap.data.DataReadException; +import adql.db.DBColumn; +import adql.db.DBTable; +import adql.db.DBType; +import adql.db.STCS.Region; +import adql.db.exception.UnresolvedJoinException; +import adql.parser.ParseException; +import adql.query.ADQLList; +import adql.query.ADQLObject; +import adql.query.ADQLOrder; +import adql.query.ADQLQuery; +import adql.query.ClauseConstraints; +import adql.query.ClauseSelect; +import adql.query.ColumnReference; +import adql.query.IdentifierField; +import adql.query.SelectAllColumns; +import adql.query.SelectItem; +import adql.query.constraint.ADQLConstraint; +import adql.query.constraint.Between; +import adql.query.constraint.Comparison; +import adql.query.constraint.ConstraintsGroup; +import adql.query.constraint.Exists; +import adql.query.constraint.In; +import adql.query.constraint.IsNull; +import adql.query.constraint.NotConstraint; +import adql.query.from.ADQLJoin; +import adql.query.from.ADQLTable; +import adql.query.from.FromContent; +import adql.query.operand.ADQLColumn; +import adql.query.operand.ADQLOperand; +import adql.query.operand.Concatenation; +import adql.query.operand.NegativeOperand; +import adql.query.operand.NumericConstant; +import adql.query.operand.Operation; +import adql.query.operand.StringConstant; +import adql.query.operand.WrappedOperand; +import adql.query.operand.function.ADQLFunction; +import adql.query.operand.function.MathFunction; +import adql.query.operand.function.SQLFunction; +import adql.query.operand.function.SQLFunctionType; +import adql.query.operand.function.UserDefinedFunction; +import adql.query.operand.function.geometry.AreaFunction; +import adql.query.operand.function.geometry.BoxFunction; +import adql.query.operand.function.geometry.CentroidFunction; +import adql.query.operand.function.geometry.CircleFunction; +import adql.query.operand.function.geometry.ContainsFunction; +import adql.query.operand.function.geometry.DistanceFunction; +import adql.query.operand.function.geometry.ExtractCoord; +import adql.query.operand.function.geometry.ExtractCoordSys; +import adql.query.operand.function.geometry.GeometryFunction; +import adql.query.operand.function.geometry.GeometryFunction.GeometryValue; +import adql.query.operand.function.geometry.IntersectsFunction; +import adql.query.operand.function.geometry.PointFunction; +import adql.query.operand.function.geometry.PolygonFunction; +import adql.query.operand.function.geometry.RegionFunction; + +/** + *

Implementation of {@link ADQLTranslator} which translates ADQL queries in SQL queries.

+ * + *

+ * It is already able to translate all SQL standard features, but lets abstract the translation of all + * geometrical functions. So, this translator must be extended as {@link PostgreSQLTranslator} and + * {@link PgSphereTranslator} are doing. + *

+ * + *

Note: + * Its default implementation of the SQL syntax has been inspired by the PostgreSQL one. + * However, it should work also with SQLite and MySQL, but some translations might be needed + * (as it is has been done for PostgreSQL about the mathematical functions). + *

+ * + *

PostgreSQLTranslator and PgSphereTranslator

+ * + *

+ * {@link PgSphereTranslator} extends {@link PostgreSQLTranslator} and is able to translate geometrical + * functions according to the syntax given by PgSphere. But it can also convert geometrical types + * (from and toward the database), translate PgSphere regions into STC expression and vice-versa. + *

+ * + *

+ * {@link PostgreSQLTranslator} overwrites the translation of mathematical functions whose some have + * a different name or signature. Besides, it is also implementing the translation of the geometrical + * functions. However, it does not really translate them. It is just returning the ADQL expression + * (by calling {@link #getDefaultADQLFunction(ADQLFunction)}). + * And so, of course, the execution of a SQL query containing geometrical functions and translated + * using this translator will not work. It is just a default implementation in case there is no interest + * of these geometrical functions. + *

+ * + *

SQL with or without case sensitivity?

+ * + *

+ * In ADQL and in SQL, it is possible to tell the parser to respect the exact case or not of an identifier (schema, table or column name) + * by surrounding it with double quotes. However ADQL identifiers and SQL ones may be different. In that way, the case sensitivity specified + * in ADQL on the different identifiers can not be kept in SQL. That's why this translator lets specify a general rule on which types of + * SQL identifier must be double quoted. This can be done by implementing the abstract function {@link #isCaseSensitive(IdentifierField)}. + * The functions translating column and table names will call this function in order to surround the identifiers by double quotes or not. + * So, be careful if you want to override the functions translating columns and tables! + *

+ * + *

Translation of "SELECT TOP"

+ * + *

+ * The default behavior of this translator is to translate the ADQL "TOP" into the SQL "LIMIT" at the end of the query. + * This is ok for some DBMS, but not all. So, if your DBMS does not know the "LIMIT" keyword, you should override the function + * translating the whole query: {@link #translate(ADQLQuery)}. Here is its current implementation: + *

+ *
+ * 	StringBuffer sql = new StringBuffer(translate(query.getSelect()));
+ * 	sql.append("\nFROM ").append(translate(query.getFrom()));
+ *	if (!query.getWhere().isEmpty())
+ *		sql.append('\n').append(translate(query.getWhere()));
+ *	if (!query.getGroupBy().isEmpty())
+ *		sql.append('\n').append(translate(query.getGroupBy()));
+ *	if (!query.getHaving().isEmpty())
+ *		sql.append('\n').append(translate(query.getHaving()));
+ *	if (!query.getOrderBy().isEmpty())
+ *		sql.append('\n').append(translate(query.getOrderBy()));
+ *	if (query.getSelect().hasLimit())
+ *		sql.append("\nLimit ").append(query.getSelect().getLimit());
+ *	return sql.toString();
+ * 
+ * + *

Translation of ADQL functions

+ * + *

+ * All ADQL functions are by default not translated. Consequently, the SQL translation is + * actually the ADQL expression. Generally the ADQL expression is generic enough. However some mathematical functions may need + * to be translated differently. For instance {@link PostgreSQLTranslator} is translating differently: LOG, LOG10, RAND and TRUNC. + *

+ * + *

Note: + * Geometrical regions and types have not been managed here. They stay abstract because it is obviously impossible to have a generic + * translation and conversion ; it totally depends from the database system. + *

+ * + *

Translation of "FROM" with JOINs

+ * + *

+ * The FROM clause is translated into SQL as written in ADQL. There is no differences except the identifiers that are replaced. + * The tables' aliases and their case sensitivity are kept like in ADQL. + *

+ * + * @author Grégory Mantelet (ARI) + * @version 1.3 (05/2015) + * @since 1.3 + * + * @see PostgreSQLTranslator + * @see PgSphereTranslator + */ +public abstract class JDBCTranslator implements ADQLTranslator { + + /** + *

Tell whether the specified identifier MUST be translated so that being interpreted case sensitively or not. + * By default, an identifier that must be translated with case sensitivity will be surrounded by double quotes. + * But, if this function returns FALSE, the SQL name will be written just as given in the metadata, without double quotes.

+ * + *

WARNING: + * An {@link IdentifierField} object can be a SCHEMA, TABLE, COLUMN and ALIAS. However, in this translator, + * aliases are translated like in ADQL (so, with the same case sensitivity specification as in ADQL). + * So, this function will never be used to know the case sensitivity to apply to an alias. It is then + * useless to write a special behavior for the ALIAS value. + *

+ * + * @param field The identifier whose the case sensitive to apply is asked. + * + * @return true if the specified identifier must be translated case sensitivity, false otherwise (included if ALIAS or NULL). + */ + public abstract boolean isCaseSensitive(final IdentifierField field); + + /** + *

Get the qualified DB name of the schema containing the given table.

+ * + *

Note: + * This function will, by default, add double quotes if the schema name must be case sensitive in the SQL query. + * This information is provided by {@link #isCaseSensitive(IdentifierField)}. + *

+ * + * @param table A table of the schema whose the qualified DB name is asked. + * + * @return The qualified (with DB catalog name prefix if any, and with double quotes if needed) DB schema name, + * or an empty string if there is no schema or no DB name. + */ + public String getQualifiedSchemaName(final DBTable table){ + if (table == null || table.getDBSchemaName() == null) + return ""; + + StringBuffer buf = new StringBuffer(); + + if (table.getDBCatalogName() != null) + appendIdentifier(buf, table.getDBCatalogName(), IdentifierField.CATALOG).append('.'); + + appendIdentifier(buf, table.getDBSchemaName(), IdentifierField.SCHEMA); + + return buf.toString(); + } + + /** + *

Get the qualified DB name of the given table.

+ * + *

Note: + * This function will, by default, add double quotes if the table name must be case sensitive in the SQL query. + * This information is provided by {@link #isCaseSensitive(IdentifierField)}. + *

+ * + * @param table The table whose the qualified DB name is asked. + * + * @return The qualified (with DB catalog and schema prefix if any, and with double quotes if needed) DB table name, + * or an empty string if the given table is NULL or if there is no DB name. + * + * @see #getTableName(DBTable, boolean) + */ + public String getQualifiedTableName(final DBTable table){ + return getTableName(table, true); + } + + /** + *

Get the DB name of the given table. + * The second parameter lets specify whether the table name must be prefixed by the qualified schema name or not.

+ * + *

Note: + * This function will, by default, add double quotes if the table name must be case sensitive in the SQL query. + * This information is provided by {@link #isCaseSensitive(IdentifierField)}. + *

+ * + * @param table The table whose the DB name is asked. + * @param withSchema true if the qualified schema name must prefix the table name, false otherwise. + * + * @return The DB table name (prefixed by the qualified schema name if asked, and with double quotes if needed), + * or an empty string if the given table is NULL or if there is no DB name. + * + * @since 2.0 + */ + public String getTableName(final DBTable table, final boolean withSchema){ + if (table == null) + return ""; + + StringBuffer buf = new StringBuffer(); + if (withSchema){ + buf.append(getQualifiedSchemaName(table)); + if (buf.length() > 0) + buf.append('.'); + } + appendIdentifier(buf, table.getDBName(), IdentifierField.TABLE); + + return buf.toString(); + } + + /** + *

Get the DB name of the given column

+ * + *

Note: + * This function will, by default, add double quotes if the column name must be case sensitive in the SQL query. + * This information is provided by {@link #isCaseSensitive(IdentifierField)}. + *

+ * + *

Caution: + * The given column may be NULL and in this case an empty string will be returned. + * But if the given column is not NULL, its DB name MUST NOT BE NULL! + *

+ * + * @param column The column whose the DB name is asked. + * + * @return The DB column name (with double quotes if needed), + * or an empty string if the given column is NULL. + */ + public String getColumnName(final DBColumn column){ + return (column == null) ? "" : appendIdentifier(new StringBuffer(), column.getDBName(), IdentifierField.COLUMN).toString(); + } + + /** + * Appends the given identifier in the given StringBuffer. + * + * @param str The string buffer. + * @param id The identifier to append. + * @param field The type of identifier (column, table, schema, catalog or alias ?). + * + * @return The string buffer + identifier. + */ + public final StringBuffer appendIdentifier(final StringBuffer str, final String id, final IdentifierField field){ + return appendIdentifier(str, id, isCaseSensitive(field)); + } + + /** + * Appends the given identifier to the given StringBuffer. + * + * @param str The string buffer. + * @param id The identifier to append. + * @param caseSensitive true to format the identifier so that preserving the case sensitivity, false otherwise. + * + * @return The string buffer + identifier. + */ + public static final StringBuffer appendIdentifier(final StringBuffer str, final String id, final boolean caseSensitive){ + if (caseSensitive) + return str.append('"').append(id).append('"'); + else + return str.append(id); + } + + @Override + @SuppressWarnings({"unchecked","rawtypes"}) + public String translate(ADQLObject obj) throws TranslationException{ + if (obj instanceof ADQLQuery) + return translate((ADQLQuery)obj); + else if (obj instanceof ADQLList) + return translate((ADQLList)obj); + else if (obj instanceof SelectItem) + return translate((SelectItem)obj); + else if (obj instanceof ColumnReference) + return translate((ColumnReference)obj); + else if (obj instanceof ADQLTable) + return translate((ADQLTable)obj); + else if (obj instanceof ADQLJoin) + return translate((ADQLJoin)obj); + else if (obj instanceof ADQLOperand) + return translate((ADQLOperand)obj); + else if (obj instanceof ADQLConstraint) + return translate((ADQLConstraint)obj); + else + return obj.toADQL(); + } + + @Override + public String translate(ADQLQuery query) throws TranslationException{ + StringBuffer sql = new StringBuffer(translate(query.getSelect())); + + sql.append("\nFROM ").append(translate(query.getFrom())); + + if (!query.getWhere().isEmpty()) + sql.append('\n').append(translate(query.getWhere())); + + if (!query.getGroupBy().isEmpty()) + sql.append('\n').append(translate(query.getGroupBy())); + + if (!query.getHaving().isEmpty()) + sql.append('\n').append(translate(query.getHaving())); + + if (!query.getOrderBy().isEmpty()) + sql.append('\n').append(translate(query.getOrderBy())); + + if (query.getSelect().hasLimit()) + sql.append("\nLimit ").append(query.getSelect().getLimit()); + + return sql.toString(); + } + + /* *************************** */ + /* ****** LIST & CLAUSE ****** */ + /* *************************** */ + @Override + public String translate(ADQLList list) throws TranslationException{ + if (list instanceof ClauseSelect) + return translate((ClauseSelect)list); + else if (list instanceof ClauseConstraints) + return translate((ClauseConstraints)list); + else + return getDefaultADQLList(list); + } + + /** + * Gets the default SQL output for a list of ADQL objects. + * + * @param list List to format into SQL. + * + * @return The corresponding SQL. + * + * @throws TranslationException If there is an error during the translation. + */ + protected String getDefaultADQLList(ADQLList list) throws TranslationException{ + String sql = (list.getName() == null) ? "" : (list.getName() + " "); + + for(int i = 0; i < list.size(); i++) + sql += ((i == 0) ? "" : (" " + list.getSeparator(i) + " ")) + translate(list.get(i)); + + return sql; + } + + @Override + public String translate(ClauseSelect clause) throws TranslationException{ + String sql = null; + + for(int i = 0; i < clause.size(); i++){ + if (i == 0){ + sql = clause.getName() + (clause.distinctColumns() ? " DISTINCT" : ""); + }else + sql += " " + clause.getSeparator(i); + + sql += " " + translate(clause.get(i)); + } + + return sql; + } + + @Override + public String translate(ClauseConstraints clause) throws TranslationException{ + if (clause instanceof ConstraintsGroup) + return "(" + getDefaultADQLList(clause) + ")"; + else + return getDefaultADQLList(clause); + } + + @Override + public String translate(SelectItem item) throws TranslationException{ + if (item instanceof SelectAllColumns) + return translate((SelectAllColumns)item); + + StringBuffer translation = new StringBuffer(translate(item.getOperand())); + if (item.hasAlias()){ + translation.append(" AS "); + appendIdentifier(translation, item.getAlias(), item.isCaseSensitive()); + }else{ + translation.append(" AS "); + appendIdentifier(translation, item.getName(), true); + } + + return translation.toString(); + } + + @Override + public String translate(SelectAllColumns item) throws TranslationException{ + HashMap mapAlias = new HashMap(); + + // Fetch the full list of columns to display: + Iterable dbCols = null; + if (item.getAdqlTable() != null && item.getAdqlTable().getDBLink() != null){ + ADQLTable table = item.getAdqlTable(); + dbCols = table.getDBLink(); + if (table.hasAlias()){ + String key = getQualifiedTableName(table.getDBLink()); + mapAlias.put(key, table.isCaseSensitive(IdentifierField.ALIAS) ? ("\"" + table.getAlias() + "\"") : table.getAlias()); + } + }else if (item.getQuery() != null){ + try{ + dbCols = item.getQuery().getFrom().getDBColumns(); + }catch(UnresolvedJoinException pe){ + throw new TranslationException("Due to a join problem, the ADQL to SQL translation can not be completed!", pe); + } + ArrayList tables = item.getQuery().getFrom().getTables(); + for(ADQLTable table : tables){ + if (table.hasAlias()){ + String key = getQualifiedTableName(table.getDBLink()); + mapAlias.put(key, table.isCaseSensitive(IdentifierField.ALIAS) ? ("\"" + table.getAlias() + "\"") : table.getAlias()); + } + } + } + + // Write the DB name of all these columns: + if (dbCols != null){ + StringBuffer cols = new StringBuffer(); + for(DBColumn col : dbCols){ + if (cols.length() > 0) + cols.append(','); + if (col.getTable() != null){ + String fullDbName = getQualifiedTableName(col.getTable()); + if (mapAlias.containsKey(fullDbName)) + appendIdentifier(cols, mapAlias.get(fullDbName), false).append('.'); + else + cols.append(fullDbName).append('.'); + } + appendIdentifier(cols, col.getDBName(), IdentifierField.COLUMN); + cols.append(" AS \"").append(col.getADQLName()).append('\"'); + } + return (cols.length() > 0) ? cols.toString() : item.toADQL(); + }else{ + return item.toADQL(); + } + } + + @Override + public String translate(ColumnReference ref) throws TranslationException{ + if (ref instanceof ADQLOrder) + return translate((ADQLOrder)ref); + else + return getDefaultColumnReference(ref); + } + + /** + * Gets the default SQL output for a column reference. + * + * @param ref The column reference to format into SQL. + * + * @return The corresponding SQL. + * + * @throws TranslationException If there is an error during the translation. + */ + protected String getDefaultColumnReference(ColumnReference ref) throws TranslationException{ + if (ref.isIndex()){ + return "" + ref.getColumnIndex(); + }else{ + if (ref.getDBLink() == null){ + return (ref.isCaseSensitive() ? ("\"" + ref.getColumnName() + "\"") : ref.getColumnName()); + }else{ + DBColumn dbCol = ref.getDBLink(); + StringBuffer colName = new StringBuffer(); + // Use the table alias if any: + if (ref.getAdqlTable() != null && ref.getAdqlTable().hasAlias()) + appendIdentifier(colName, ref.getAdqlTable().getAlias(), ref.getAdqlTable().isCaseSensitive(IdentifierField.ALIAS)).append('.'); + + // Use the DBTable if any: + else if (dbCol.getTable() != null) + colName.append(getQualifiedTableName(dbCol.getTable())).append('.'); + + appendIdentifier(colName, dbCol.getDBName(), IdentifierField.COLUMN); + + return colName.toString(); + } + } + } + + @Override + public String translate(ADQLOrder order) throws TranslationException{ + return getDefaultColumnReference(order) + (order.isDescSorting() ? " DESC" : " ASC"); + } + + /* ************************** */ + /* ****** TABLE & JOIN ****** */ + /* ************************** */ + @Override + public String translate(FromContent content) throws TranslationException{ + if (content instanceof ADQLTable) + return translate((ADQLTable)content); + else if (content instanceof ADQLJoin) + return translate((ADQLJoin)content); + else + return content.toADQL(); + } + + @Override + public String translate(ADQLTable table) throws TranslationException{ + StringBuffer sql = new StringBuffer(); + + // CASE: SUB-QUERY: + if (table.isSubQuery()) + sql.append('(').append(translate(table.getSubQuery())).append(')'); + + // CASE: TABLE REFERENCE: + else{ + // Use the corresponding DB table, if known: + if (table.getDBLink() != null) + sql.append(getQualifiedTableName(table.getDBLink())); + // Otherwise, use the whole table name given in the ADQL query: + else + sql.append(table.getFullTableName()); + } + + // Add the table alias, if any: + if (table.hasAlias()){ + sql.append(" AS "); + appendIdentifier(sql, table.getAlias(), table.isCaseSensitive(IdentifierField.ALIAS)); + } + + return sql.toString(); + } + + @Override + public String translate(ADQLJoin join) throws TranslationException{ + StringBuffer sql = new StringBuffer(translate(join.getLeftTable())); + + if (join.isNatural()) + sql.append(" NATURAL"); + + sql.append(' ').append(join.getJoinType()).append(' ').append(translate(join.getRightTable())).append(' '); + + if (!join.isNatural()){ + if (join.getJoinCondition() != null) + sql.append(translate(join.getJoinCondition())); + else if (join.hasJoinedColumns()){ + StringBuffer cols = new StringBuffer(); + Iterator it = join.getJoinedColumns(); + while(it.hasNext()){ + ADQLColumn item = it.next(); + if (cols.length() > 0) + cols.append(", "); + if (item.getDBLink() == null) + appendIdentifier(cols, item.getColumnName(), item.isCaseSensitive(IdentifierField.COLUMN)); + else + appendIdentifier(cols, item.getDBLink().getDBName(), IdentifierField.COLUMN); + } + sql.append("USING (").append(cols).append(')'); + } + } + + return sql.toString(); + } + + /* ********************* */ + /* ****** OPERAND ****** */ + /* ********************* */ + @Override + public String translate(ADQLOperand op) throws TranslationException{ + if (op instanceof ADQLColumn) + return translate((ADQLColumn)op); + else if (op instanceof Concatenation) + return translate((Concatenation)op); + else if (op instanceof NegativeOperand) + return translate((NegativeOperand)op); + else if (op instanceof NumericConstant) + return translate((NumericConstant)op); + else if (op instanceof StringConstant) + return translate((StringConstant)op); + else if (op instanceof WrappedOperand) + return translate((WrappedOperand)op); + else if (op instanceof Operation) + return translate((Operation)op); + else if (op instanceof ADQLFunction) + return translate((ADQLFunction)op); + else + return op.toADQL(); + } + + @Override + public String translate(ADQLColumn column) throws TranslationException{ + // Use its DB name if known: + if (column.getDBLink() != null){ + DBColumn dbCol = column.getDBLink(); + StringBuffer colName = new StringBuffer(); + // Use the table alias if any: + if (column.getAdqlTable() != null && column.getAdqlTable().hasAlias()) + appendIdentifier(colName, column.getAdqlTable().getAlias(), column.getAdqlTable().isCaseSensitive(IdentifierField.ALIAS)).append('.'); + + // Use the DBTable if any: + else if (dbCol.getTable() != null && dbCol.getTable().getDBName() != null) + colName.append(getQualifiedTableName(dbCol.getTable())).append('.'); + + // Otherwise, use the prefix of the column given in the ADQL query: + else if (column.getTableName() != null) + colName = column.getFullColumnPrefix().append('.'); + + appendIdentifier(colName, dbCol.getDBName(), IdentifierField.COLUMN); + + return colName.toString(); + } + // Otherwise, use the whole name given in the ADQL query: + else + return column.getFullColumnName(); + } + + @Override + public String translate(Concatenation concat) throws TranslationException{ + return translate((ADQLList)concat); + } + + @Override + public String translate(NegativeOperand negOp) throws TranslationException{ + return "-" + translate(negOp.getOperand()); + } + + @Override + public String translate(NumericConstant numConst) throws TranslationException{ + return numConst.getValue(); + } + + @Override + public String translate(StringConstant strConst) throws TranslationException{ + return "'" + strConst.getValue() + "'"; + } + + @Override + public String translate(WrappedOperand op) throws TranslationException{ + return "(" + translate(op.getOperand()) + ")"; + } + + @Override + public String translate(Operation op) throws TranslationException{ + return translate(op.getLeftOperand()) + op.getOperation().toADQL() + translate(op.getRightOperand()); + } + + /* ************************ */ + /* ****** CONSTRAINT ****** */ + /* ************************ */ + @Override + public String translate(ADQLConstraint cons) throws TranslationException{ + if (cons instanceof Comparison) + return translate((Comparison)cons); + else if (cons instanceof Between) + return translate((Between)cons); + else if (cons instanceof Exists) + return translate((Exists)cons); + else if (cons instanceof In) + return translate((In)cons); + else if (cons instanceof IsNull) + return translate((IsNull)cons); + else if (cons instanceof NotConstraint) + return translate((NotConstraint)cons); + else + return cons.toADQL(); + } + + @Override + public String translate(Comparison comp) throws TranslationException{ + return translate(comp.getLeftOperand()) + " " + comp.getOperator().toADQL() + " " + translate(comp.getRightOperand()); + } + + @Override + public String translate(Between comp) throws TranslationException{ + return translate(comp.getLeftOperand()) + " " + comp.getName() + " " + translate(comp.getMinOperand()) + " AND " + translate(comp.getMaxOperand()); + } + + @Override + public String translate(Exists exists) throws TranslationException{ + return "EXISTS(" + translate(exists.getSubQuery()) + ")"; + } + + @Override + public String translate(In in) throws TranslationException{ + return translate(in.getOperand()) + " " + in.getName() + " (" + (in.hasSubQuery() ? translate(in.getSubQuery()) : translate(in.getValuesList())) + ")"; + } + + @Override + public String translate(IsNull isNull) throws TranslationException{ + return translate(isNull.getColumn()) + " " + isNull.getName(); + } + + @Override + public String translate(NotConstraint notCons) throws TranslationException{ + return "NOT " + translate(notCons.getConstraint()); + } + + /* *********************** */ + /* ****** FUNCTIONS ****** */ + /* *********************** */ + @Override + public String translate(ADQLFunction fct) throws TranslationException{ + if (fct instanceof GeometryFunction) + return translate((GeometryFunction)fct); + else if (fct instanceof MathFunction) + return translate((MathFunction)fct); + else if (fct instanceof SQLFunction) + return translate((SQLFunction)fct); + else if (fct instanceof UserDefinedFunction) + return translate((UserDefinedFunction)fct); + else + return getDefaultADQLFunction(fct); + } + + /** + * Gets the default SQL output for the given ADQL function. + * + * @param fct The ADQL function to format into SQL. + * + * @return The corresponding SQL. + * + * @throws TranslationException If there is an error during the translation. + */ + protected final String getDefaultADQLFunction(ADQLFunction fct) throws TranslationException{ + String sql = fct.getName() + "("; + + for(int i = 0; i < fct.getNbParameters(); i++) + sql += ((i == 0) ? "" : ", ") + translate(fct.getParameter(i)); + + return sql + ")"; + } + + @Override + public String translate(SQLFunction fct) throws TranslationException{ + if (fct.getType() == SQLFunctionType.COUNT_ALL) + return "COUNT(" + (fct.isDistinct() ? "DISTINCT " : "") + "*)"; + else + return fct.getName() + "(" + (fct.isDistinct() ? "DISTINCT " : "") + translate(fct.getParameter(0)) + ")"; + } + + @Override + public String translate(MathFunction fct) throws TranslationException{ + return getDefaultADQLFunction(fct); + } + + @Override + public String translate(UserDefinedFunction fct) throws TranslationException{ + return fct.translate(this); + } + + /* *********************************** */ + /* ****** GEOMETRICAL FUNCTIONS ****** */ + /* *********************************** */ + @Override + public String translate(GeometryFunction fct) throws TranslationException{ + if (fct instanceof AreaFunction) + return translate((AreaFunction)fct); + else if (fct instanceof BoxFunction) + return translate((BoxFunction)fct); + else if (fct instanceof CentroidFunction) + return translate((CentroidFunction)fct); + else if (fct instanceof CircleFunction) + return translate((CircleFunction)fct); + else if (fct instanceof ContainsFunction) + return translate((ContainsFunction)fct); + else if (fct instanceof DistanceFunction) + return translate((DistanceFunction)fct); + else if (fct instanceof ExtractCoord) + return translate((ExtractCoord)fct); + else if (fct instanceof ExtractCoordSys) + return translate((ExtractCoordSys)fct); + else if (fct instanceof IntersectsFunction) + return translate((IntersectsFunction)fct); + else if (fct instanceof PointFunction) + return translate((PointFunction)fct); + else if (fct instanceof PolygonFunction) + return translate((PolygonFunction)fct); + else if (fct instanceof RegionFunction) + return translate((RegionFunction)fct); + else + return getDefaultADQLFunction(fct); + } + + @Override + public String translate(GeometryValue geomValue) throws TranslationException{ + return translate(geomValue.getValue()); + } + + /** + * Convert any type provided by a JDBC driver into a type understandable by the ADQL/TAP library. + * + * @param dbmsType Type returned by a JDBC driver. Note: this value is returned by ResultSetMetadata.getColumnType(int) and correspond to a type of java.sql.Types + * @param rawDbmsTypeName Full name of the type returned by a JDBC driver. Note: this name is returned by ResultSetMetadata.getColumnTypeName(int) ; this name may contain parameters + * @param dbmsTypeName Name of type, without the eventual parameters. Note: this name is extracted from rawDbmsTypeName. + * @param typeParams The eventual type parameters (e.g. char string length). Note: these parameters are extracted from rawDbmsTypeName. + * + * @return The corresponding ADQL/TAP type or NULL if the specified type is unknown. + */ + public abstract DBType convertTypeFromDB(final int dbmsType, final String rawDbmsTypeName, final String dbmsTypeName, final String[] typeParams); + + /** + *

Convert any type provided by the ADQL/TAP library into a type understandable by a JDBC driver.

+ * + *

Note: + * The returned DBMS type may contain some parameters between brackets. + *

+ * + * @param type The ADQL/TAP library's type to convert. + * + * @return The corresponding DBMS type or NULL if the specified type is unknown. + */ + public abstract String convertTypeToDB(final DBType type); + + /** + *

Parse the given JDBC column value as a geometry object and convert it into a {@link Region}.

+ * + *

Note: + * Generally the returned object will be used to get its STC-S expression. + *

+ * + *

Note: + * If the given column value is NULL, NULL will be returned. + *

+ * + *

Important note: + * This function is called ONLY for value of columns flagged as geometries by + * {@link #convertTypeFromDB(int, String, String, String[])}. So the value should always + * be of the expected type and format. However, if it turns out that the type is wrong + * and that the conversion is finally impossible, this function SHOULD throw a + * {@link DataReadException}. + *

+ * + * @param jdbcColValue A JDBC column value (returned by ResultSet.getObject(int)). + * + * @return The corresponding {@link Region} if the given value is a geometry. + * + * @throws ParseException If the given object is not a geometrical object + * or can not be transformed into a {@link Region} object. + */ + public abstract Region translateGeometryFromDB(final Object jdbcColValue) throws ParseException; + + /** + *

Convert the given STC region into a DB column value.

+ * + *

Note: + * This function is used only by the UPLOAD feature, to import geometries provided as STC-S expression in + * a VOTable document inside a DB column. + *

+ * + *

Note: + * If the given region is NULL, NULL will be returned. + *

+ * + * @param region The region to store in the DB. + * + * @return The corresponding DB column object. + * + * @throws ParseException If the given STC Region can not be converted into a DB object. + */ + public abstract Object translateGeometryToDB(final Region region) throws ParseException; + +} diff --git a/src/adql/translator/PgSphereTranslator.java b/src/adql/translator/PgSphereTranslator.java index efe3807..96c509c 100644 --- a/src/adql/translator/PgSphereTranslator.java +++ b/src/adql/translator/PgSphereTranslator.java @@ -16,12 +16,22 @@ package adql.translator; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ +import java.sql.SQLException; +import java.util.ArrayList; + +import org.postgresql.util.PGobject; + +import adql.db.DBType; +import adql.db.DBType.DBDatatype; +import adql.db.STCS.Region; +import adql.parser.ParseException; +import adql.query.TextPosition; import adql.query.constraint.Comparison; import adql.query.constraint.ComparisonOperator; - import adql.query.operand.function.geometry.AreaFunction; import adql.query.operand.function.geometry.BoxFunction; import adql.query.operand.function.geometry.CircleFunction; @@ -32,24 +42,25 @@ import adql.query.operand.function.geometry.IntersectsFunction; import adql.query.operand.function.geometry.PointFunction; import adql.query.operand.function.geometry.PolygonFunction; -import adql.translator.PostgreSQLTranslator; -import adql.translator.TranslationException; - /** *

Translates all ADQL objects into the SQL adaptation of Postgres+PgSphere. * Actually only the geometrical functions are translated in this class. * The other functions are managed by {@link PostgreSQLTranslator}.

* - * @author Grégory Mantelet (CDS) - * @version 01/2012 - * - * @see PostgreSQLTranslator + * @author Grégory Mantelet (CDS;ARI) + * @version 1.3 (11/2014) */ public class PgSphereTranslator extends PostgreSQLTranslator { + /** Angle between two points generated while transforming a circle into a polygon. + * This angle is computed by default to get at the end a polygon of 32 points. + * @see #circleToPolygon(double[], double) + * @since 1.3 */ + protected static double ANGLE_CIRCLE_TO_POLYGON = 2 * Math.PI / 32; + /** - * Builds a PgSphereTranslator which takes into account the case sensitivity on column names. - * It means that column names which have been written between double quotes, will be also translated between double quotes. + * Builds a PgSphereTranslator which always translates in SQL all identifiers (schema, table and column) in a case sensitive manner ; + * in other words, schema, table and column names will be surrounded by double quotes in the SQL translation. * * @see PostgreSQLTranslator#PostgreSQLTranslator() */ @@ -58,23 +69,24 @@ public class PgSphereTranslator extends PostgreSQLTranslator { } /** - * Builds a PgSphereTranslator. + * Builds a PgSphereTranslator which always translates in SQL all identifiers (schema, table and column) in the specified case sensitivity ; + * in other words, schema, table and column names will all be surrounded or not by double quotes in the SQL translation. * - * @param column true to take into account the case sensitivity of column names, false otherwise. + * @param allCaseSensitive true to translate all identifiers in a case sensitive manner (surrounded by double quotes), false for case insensitivity. * * @see PostgreSQLTranslator#PostgreSQLTranslator(boolean) */ - public PgSphereTranslator(boolean column){ - super(column); + public PgSphereTranslator(boolean allCaseSensitive){ + super(allCaseSensitive); } /** - * Builds a PgSphereTranslator. + * Builds a PgSphereTranslator which will always translate in SQL identifiers with the defined case sensitivity. * - * @param catalog true to take into account the case sensitivity of catalog names, false otherwise. - * @param schema true to take into account the case sensitivity of schema names, false otherwise. - * @param table true to take into account the case sensitivity of table names, false otherwise. - * @param column true to take into account the case sensitivity of column names, false otherwise. + * @param catalog true to translate catalog names with double quotes (case sensitive in the DBMS), false otherwise. + * @param schema true to translate schema names with double quotes (case sensitive in the DBMS), false otherwise. + * @param table true to translate table names with double quotes (case sensitive in the DBMS), false otherwise. + * @param column true to translate column names with double quotes (case sensitive in the DBMS), false otherwise. * * @see PostgreSQLTranslator#PostgreSQLTranslator(boolean, boolean, boolean, boolean) */ @@ -103,11 +115,12 @@ public class PgSphereTranslator extends PostgreSQLTranslator { public String translate(BoxFunction box) throws TranslationException{ StringBuffer str = new StringBuffer("sbox("); + str.append("spoint(").append("radians(").append(translate(box.getCoord1())).append("-(").append(translate(box.getWidth())).append("/2.0)),"); + str.append("radians(").append(translate(box.getCoord2())).append("-(").append(translate(box.getHeight())).append("/2.0))),"); + str.append("spoint(").append("radians(").append(translate(box.getCoord1())).append("+(").append(translate(box.getWidth())).append("/2.0)),"); - str.append("radians(").append(translate(box.getCoord2())).append("+(").append(translate(box.getHeight())).append("/2.0))),"); + str.append("radians(").append(translate(box.getCoord2())).append("+(").append(translate(box.getHeight())).append("/2.0))))"); - str.append("spoint(").append("radians(").append(translate(box.getCoord1())).append("-(").append(translate(box.getWidth())).append("/2.0)),"); - str.append("radians(").append(translate(box.getCoord2())).append("-(").append(translate(box.getHeight())).append("/2.0))))"); return str.toString(); } @@ -156,8 +169,8 @@ public class PgSphereTranslator extends PostgreSQLTranslator { @Override public String translate(AreaFunction areaFunction) throws TranslationException{ - StringBuffer str = new StringBuffer("degrees(area("); - str.append(translate(areaFunction.getParameter())).append("))"); + StringBuffer str = new StringBuffer("degrees(degrees(area("); + str.append(translate(areaFunction.getParameter())).append(")))"); return str.toString(); } @@ -185,4 +198,534 @@ public class PgSphereTranslator extends PostgreSQLTranslator { return super.translate(comp); } + @Override + public DBType convertTypeFromDB(final int dbmsType, final String rawDbmsTypeName, String dbmsTypeName, final String[] params){ + // If no type is provided return VARCHAR: + if (dbmsTypeName == null || dbmsTypeName.trim().length() == 0) + return new DBType(DBDatatype.VARCHAR, DBType.NO_LENGTH); + + // Put the dbmsTypeName in lower case for the following comparisons: + dbmsTypeName = dbmsTypeName.toLowerCase(); + + if (dbmsTypeName.equals("spoint")) + return new DBType(DBDatatype.POINT); + else if (dbmsTypeName.equals("scircle") || dbmsTypeName.equals("sbox") || dbmsTypeName.equals("spoly")) + return new DBType(DBDatatype.REGION); + else + return super.convertTypeFromDB(dbmsType, rawDbmsTypeName, dbmsTypeName, params); + } + + @Override + public String convertTypeToDB(final DBType type){ + if (type != null){ + if (type.type == DBDatatype.POINT) + return "spoint"; + else if (type.type == DBDatatype.REGION) + return "spoly"; + } + return super.convertTypeToDB(type); + } + + @Override + public Region translateGeometryFromDB(final Object jdbcColValue) throws ParseException{ + // A NULL value stays NULL: + if (jdbcColValue == null) + return null; + // Only a special object is expected: + else if (!(jdbcColValue instanceof PGobject)) + throw new ParseException("Incompatible type! The column value \"" + jdbcColValue.toString() + "\" was supposed to be a geometrical object."); + + PGobject pgo = (PGobject)jdbcColValue; + + // In case one or both of the fields of the given object are NULL: + if (pgo == null || pgo.getType() == null || pgo.getValue() == null || pgo.getValue().length() == 0) + return null; + + // Extract the object type and its value: + String objType = pgo.getType().toLowerCase(); + String geomStr = pgo.getValue(); + + /* Only spoint, scircle, sbox and spoly are supported ; + * these geometries are parsed and transformed in Region instances:*/ + if (objType.equals("spoint")) + return (new PgSphereGeometryParser()).parsePoint(geomStr); + else if (objType.equals("scircle")) + return (new PgSphereGeometryParser()).parseCircle(geomStr); + else if (objType.equals("sbox")) + return (new PgSphereGeometryParser()).parseBox(geomStr); + else if (objType.equals("spoly")) + return (new PgSphereGeometryParser()).parsePolygon(geomStr); + else + throw new ParseException("Unsupported PgSphere type: \"" + objType + "\"! Impossible to convert the column value \"" + geomStr + "\" into a Region."); + } + + @Override + public Object translateGeometryToDB(final Region region) throws ParseException{ + // A NULL value stays NULL: + if (region == null) + return null; + + try{ + PGobject dbRegion = new PGobject(); + StringBuffer buf; + + // Build the PgSphere expression from the given geometry in function of its type: + switch(region.type){ + + case POSITION: + dbRegion.setType("spoint"); + dbRegion.setValue("(" + region.coordinates[0][0] + "d," + region.coordinates[0][1] + "d)"); + break; + + case POLYGON: + dbRegion.setType("spoly"); + buf = new StringBuffer("{"); + for(int i = 0; i < region.coordinates.length; i++){ + if (i > 0) + buf.append(','); + buf.append('(').append(region.coordinates[i][0]).append("d,").append(region.coordinates[i][1]).append("d)"); + } + buf.append('}'); + dbRegion.setValue(buf.toString()); + break; + + case BOX: + dbRegion.setType("spoly"); + buf = new StringBuffer("{"); + // south west + buf.append('(').append(region.coordinates[0][0] - region.width / 2).append("d,").append(region.coordinates[0][1] - region.height / 2).append("d),"); + // north west + buf.append('(').append(region.coordinates[0][0] - region.width / 2).append("d,").append(region.coordinates[0][1] + region.height / 2).append("d),"); + // north east + buf.append('(').append(region.coordinates[0][0] + region.width / 2).append("d,").append(region.coordinates[0][1] + region.height / 2).append("d),"); + // south east + buf.append('(').append(region.coordinates[0][0] + region.width / 2).append("d,").append(region.coordinates[0][1] - region.height / 2).append("d)"); + buf.append('}'); + dbRegion.setValue(buf.toString()); + break; + + case CIRCLE: + dbRegion.setType("spoly"); + dbRegion.setValue(circleToPolygon(region.coordinates[0], region.radius)); + break; + + default: + throw new ParseException("Unsupported geometrical region: \"" + region.type + "\"!"); + } + return dbRegion; + }catch(SQLException e){ + /* This error could never happen! */ + return null; + } + } + + /** + *

Convert the specified circle into a polygon. + * The generated polygon is formatted using the PgSphere syntax.

+ * + *

Note: + * The center coordinates and the radius are expected in degrees. + *

+ * + * @param center Center of the circle ([0]=ra and [1]=dec). + * @param radius Radius of the circle. + * + * @return The PgSphere serialization of the corresponding polygon. + * + * @since 1.3 + */ + protected String circleToPolygon(final double[] center, final double radius){ + double angle = 0, x, y; + StringBuffer buf = new StringBuffer(); + while(angle < 2 * Math.PI){ + x = center[0] + radius * Math.cos(angle); + y = center[1] + radius * Math.sin(angle); + if (buf.length() > 0) + buf.append(','); + buf.append('(').append(x).append("d,").append(y).append("d)"); + angle += ANGLE_CIRCLE_TO_POLYGON; + } + return "{" + buf + "}"; + } + + /** + *

Let parse a geometry serialized with the PgSphere syntax.

+ * + *

+ * There is one function parseXxx(String) for each supported geometry. + * These functions always return a {@link Region} object, + * which is the object representation of an STC region. + *

+ * + *

Only the following geometries are supported:

+ *
    + *
  • spoint => Position
  • + *
  • scircle => Circle
  • + *
  • sbox => Box
  • + *
  • spoly => Polygon
  • + *
+ * + *

+ * This parser supports all the known PgSphere representations of an angle. + * However, it always returns angle (coordinates, radius, width and height) in degrees. + *

+ * + * @author Grégory Mantelet (ARI) + * @version 1.3 (11/2014) + * @since 1.3 + */ + protected static class PgSphereGeometryParser { + /** Position of the next characters to read in the PgSphere expression to parse. */ + private int pos; + /** Full PgSphere expression to parse. */ + private String expr; + /** Last read token (either a string/numeric or a separator). */ + private String token; + /** Buffer used to read tokens. */ + private StringBuffer buffer; + + private static final char OPEN_PAR = '('; + private static final char CLOSE_PAR = ')'; + private static final char COMMA = ','; + private static final char LESS_THAN = '<'; + private static final char GREATER_THAN = '>'; + private static final char OPEN_BRACE = '{'; + private static final char CLOSE_BRACE = '}'; + private static final char DEGREE = 'd'; + private static final char HOUR = 'h'; + private static final char MINUTE = 'm'; + private static final char SECOND = 's'; + + /** + * Exception sent when the end of the expression + * (EOE = End Of Expression) is reached. + * + * @author Grégory Mantelet (ARI) + * @version 1.3 (11/2014) + * @since 1.3 + */ + private static class EOEException extends ParseException { + private static final long serialVersionUID = 1L; + + /** Build a simple EOEException. */ + public EOEException(){ + super("Unexpected End Of PgSphere Expression!"); + } + } + + /** + * Build the PgSphere parser. + */ + public PgSphereGeometryParser(){} + + /** + * Prepare the parser in order to read the given PgSphere expression. + * + * @param newStcs New PgSphere expression to parse from now. + */ + private void init(final String newExpr){ + expr = (newExpr == null) ? "" : newExpr; + token = null; + buffer = new StringBuffer(); + pos = 0; + } + + /** + * Finalize the parsing. + * No more characters (except eventually some space characters) should remain in the PgSphere expression to parse. + * + * @throws ParseException If other non-space characters remains. + */ + private void end() throws ParseException{ + // Skip all spaces: + skipSpaces(); + + // If there is still some characters, they are not expected, and so throw an exception: + if (expr.length() > 0 && pos < expr.length()) + throw new ParseException("Unexpected end of PgSphere region expression: \"" + expr.substring(pos) + "\" was unexpected!", new TextPosition(1, pos, 1, expr.length())); + + // Reset the buffer, token and the PgSphere expression to parse: + buffer = null; + expr = null; + token = null; + } + + /** + * Tool function which skips all next space characters until the next meaningful characters. + */ + private void skipSpaces(){ + while(pos < expr.length() && Character.isWhitespace(expr.charAt(pos))) + pos++; + } + + /** + *

Get the next meaningful word. This word can be a numeric, any string constant or a separator. + * This function returns this token but also stores it in the class attribute {@link #token}.

+ * + *

+ * In case the end of the expression is reached before getting any meaningful character, + * an {@link EOEException} is thrown. + *

+ * + * @return The full read word/token, or NULL if the end has been reached. + */ + private String nextToken() throws EOEException{ + // Skip all spaces: + skipSpaces(); + + if (pos >= expr.length()) + throw new EOEException(); + + // Fetch all characters until word separator (a space or a open/close parenthesis): + buffer.append(expr.charAt(pos++)); + if (!isSyntaxSeparator(buffer.charAt(0))){ + while(pos < expr.length() && !isSyntaxSeparator(expr.charAt(pos))){ + // skip eventual white-spaces: + if (!Character.isWhitespace(expr.charAt(pos))) + buffer.append(expr.charAt(pos)); + pos++; + } + } + + // Save the read token and reset the buffer: + token = buffer.toString(); + buffer.delete(0, token.length()); + + return token; + } + + /** + *

Tell whether the given character is a separator defined in the syntax.

+ * + *

Here, the following characters are considered as separators/specials: + * ',', 'd', 'h', 'm', 's', '(', ')', '<', '>', '{' and '}'.

+ * + * @param c Character to test. + * + * @return true if the given character must be considered as a separator, false otherwise. + */ + private static boolean isSyntaxSeparator(final char c){ + return (c == COMMA || c == DEGREE || c == HOUR || c == MINUTE || c == SECOND || c == OPEN_PAR || c == CLOSE_PAR || c == LESS_THAN || c == GREATER_THAN || c == OPEN_BRACE || c == CLOSE_BRACE); + } + + /** + * Get the next character and ensure it is the same as the character given in parameter. + * If the read character is not matching the expected one, a {@link ParseException} is thrown. + * + * @param expected Expected character. + * + * @throws ParseException If the next character is not matching the given one. + */ + private void nextToken(final char expected) throws ParseException{ + // Skip all spaces: + skipSpaces(); + + // Test whether the end is reached: + if (pos >= expr.length()) + throw new EOEException(); + + // Fetch the next character: + char t = expr.charAt(pos++); + token = new String(new char[]{t}); + + /* Test the the fetched character with the expected one + * and throw an error if they don't match: */ + if (t != expected) + throw new ParseException("Incorrect syntax for \"" + expr + "\"! \"" + expected + "\" was expected instead of \"" + t + "\".", new TextPosition(1, pos - 1, 1, pos)); + } + + /** + * Parse the given PgSphere geometry as a point. + * + * @param pgsphereExpr The PgSphere expression to parse as a point. + * + * @return A {@link Region} implementing a STC Position region. + * + * @throws ParseException If the PgSphere syntax of the given expression is wrong or does not correspond to a point. + */ + public Region parsePoint(final String pgsphereExpr) throws ParseException{ + // Init the parser: + init(pgsphereExpr); + // Parse the expression: + double[] coord = parsePoint(); + // No more character should remain after that: + end(); + // Build the STC Position region: + return new Region(null, coord); + } + + /** + * Internal spoint parsing function. It parses the PgSphere expression stored in this parser as a point. + * + * @return The ra and dec coordinates (in degrees) of the parsed point. + * + * @throws ParseException If the PgSphere syntax of the given expression is wrong or does not correspond to a point. + * + * @see #parseAngle() + * @see #parsePoint(String) + */ + private double[] parsePoint() throws ParseException{ + nextToken(OPEN_PAR); + double x = parseAngle(); + nextToken(COMMA); + double y = parseAngle(); + nextToken(CLOSE_PAR); + return new double[]{x,y}; + } + + /** + * Parse the given PgSphere geometry as a circle. + * + * @param pgsphereExpr The PgSphere expression to parse as a circle. + * + * @return A {@link Region} implementing a STC Circle region. + * + * @throws ParseException If the PgSphere syntax of the given expression is wrong or does not correspond to a circle. + */ + public Region parseCircle(final String pgsphereExpr) throws ParseException{ + // Init the parser: + init(pgsphereExpr); + + // Parse the expression: + nextToken(LESS_THAN); + double[] center = parsePoint(); + nextToken(COMMA); + double radius = parseAngle(); + nextToken(GREATER_THAN); + + // No more character should remain after that: + end(); + + // Build the STC Circle region: + return new Region(null, center, radius); + } + + /** + * Parse the given PgSphere geometry as a box. + * + * @param pgsphereExpr The PgSphere expression to parse as a box. + * + * @return A {@link Region} implementing a STC Box region. + * + * @throws ParseException If the PgSphere syntax of the given expression is wrong or does not correspond to a box. + */ + public Region parseBox(final String pgsphereExpr) throws ParseException{ + // Init the parser: + init(pgsphereExpr); + + // Parse the expression: + nextToken(OPEN_PAR); + double[] southwest = parsePoint(); + nextToken(COMMA); + double[] northeast = parsePoint(); + nextToken(CLOSE_PAR); + + // No more character should remain after that: + end(); + + // Build the STC Box region: + double width = Math.abs(northeast[0] - southwest[0]), height = Math.abs(northeast[1] - southwest[1]); + double[] center = new double[]{northeast[0] - width / 2,northeast[1] - height / 2}; + return new Region(null, center, width, height); + } + + /** + * Parse the given PgSphere geometry as a point. + * + * @param pgsphereExpr The PgSphere expression to parse as a point. + * + * @return A {@link Region} implementing a STC Position region. + * + * @throws ParseException If the PgSphere syntax of the given expression is wrong or does not correspond to a point. + */ + public Region parsePolygon(final String pgsphereExpr) throws ParseException{ + // Init the parser: + init(pgsphereExpr); + + // Parse the expression: + nextToken(OPEN_BRACE); + ArrayList points = new ArrayList(3); + // at least 3 points are expected: + points.add(parsePoint()); + nextToken(COMMA); + points.add(parsePoint()); + nextToken(COMMA); + points.add(parsePoint()); + // but if there are more points, parse and keep them: + while(nextToken().length() == 1 && token.charAt(0) == COMMA) + points.add(parsePoint()); + // the expression must end with a } : + if (token.length() != 1 || token.charAt(0) != CLOSE_BRACE) + throw new ParseException("Incorrect syntax for \"" + expr + "\"! \"}\" was expected instead of \"" + token + "\".", new TextPosition(1, pos - token.length(), 1, pos)); + + // No more character should remain after that: + end(); + + // Build the STC Polygon region: + return new Region(null, points.toArray(new double[points.size()][2])); + } + + /** + *

Read the next tokens as an angle expression and returns the corresponding angle in degrees.

+ * + *

This function supports the 4 following syntaxes:

+ *
    + *
  • RAD: {number}
  • + *
  • DEG: {number}d
  • + *
  • DMS: {number}d {number}m {number}s
  • + *
  • HMS: {number}h {number}m {number}s
  • + *
+ * + * @return The corresponding angle in degrees. + * + * @throws ParseException If the angle syntax is wrong or not supported. + */ + private double parseAngle() throws ParseException{ + int oldPos = pos; + String number = nextToken(); + try{ + double degrees = Double.parseDouble(number); + int sign = (degrees < 0) ? -1 : 1; + degrees = Math.abs(degrees); + + oldPos = pos; + try{ + if (nextToken().length() == 1 && token.charAt(0) == HOUR) + sign *= 15; + else if (token.length() != 1 || token.charAt(0) != DEGREE){ + degrees = degrees * 180 / Math.PI; + pos -= token.length(); + return degrees * sign; + } + + oldPos = pos; + number = nextToken(); + if (nextToken().length() == 1 && token.charAt(0) == MINUTE) + degrees += Double.parseDouble(number) / 60; + else if (token.length() == 1 && token.charAt(0) == SECOND){ + degrees += Double.parseDouble(number) / 3600; + return degrees * sign; + }else{ + pos = oldPos; + return degrees * sign; + } + + oldPos = pos; + number = nextToken(); + if (nextToken().length() == 1 && token.charAt(0) == SECOND) + degrees += Double.parseDouble(number) / 3600; + else + pos = oldPos; + }catch(EOEException ex){ + pos = oldPos; + } + + return degrees * sign; + + }catch(NumberFormatException nfe){ + throw new ParseException("Incorrect numeric syntax: \"" + number + "\"!", new TextPosition(1, pos - token.length(), 1, pos)); + } + } + } + } diff --git a/src/adql/translator/PostgreSQLTranslator.java b/src/adql/translator/PostgreSQLTranslator.java index 58cdc7a..4b2d013 100644 --- a/src/adql/translator/PostgreSQLTranslator.java +++ b/src/adql/translator/PostgreSQLTranslator.java @@ -17,50 +17,16 @@ package adql.translator; * along with ADQLLibrary. If not, see . * * Copyright 2012-2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomisches Rechen Institute (ARI) + * Astronomisches Rechen Institut (ARI) */ -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; - -import adql.db.DBColumn; -import adql.db.DBTable; -import adql.db.exception.UnresolvedJoin; -import adql.query.ADQLList; -import adql.query.ADQLObject; -import adql.query.ADQLOrder; -import adql.query.ADQLQuery; -import adql.query.ClauseConstraints; -import adql.query.ClauseSelect; -import adql.query.ColumnReference; +import adql.db.DBType; +import adql.db.DBType.DBDatatype; +import adql.db.STCS.Region; +import adql.parser.ParseException; import adql.query.IdentifierField; -import adql.query.SelectAllColumns; -import adql.query.SelectItem; -import adql.query.constraint.ADQLConstraint; -import adql.query.constraint.Between; -import adql.query.constraint.Comparison; -import adql.query.constraint.ConstraintsGroup; -import adql.query.constraint.Exists; -import adql.query.constraint.In; -import adql.query.constraint.IsNull; -import adql.query.constraint.NotConstraint; -import adql.query.from.ADQLJoin; -import adql.query.from.ADQLTable; -import adql.query.from.FromContent; -import adql.query.operand.ADQLColumn; -import adql.query.operand.ADQLOperand; -import adql.query.operand.Concatenation; -import adql.query.operand.NegativeOperand; -import adql.query.operand.NumericConstant; -import adql.query.operand.Operation; import adql.query.operand.StringConstant; -import adql.query.operand.WrappedOperand; -import adql.query.operand.function.ADQLFunction; import adql.query.operand.function.MathFunction; -import adql.query.operand.function.SQLFunction; -import adql.query.operand.function.SQLFunctionType; -import adql.query.operand.function.UserDefinedFunction; import adql.query.operand.function.geometry.AreaFunction; import adql.query.operand.function.geometry.BoxFunction; import adql.query.operand.function.geometry.CentroidFunction; @@ -69,54 +35,61 @@ import adql.query.operand.function.geometry.ContainsFunction; import adql.query.operand.function.geometry.DistanceFunction; import adql.query.operand.function.geometry.ExtractCoord; import adql.query.operand.function.geometry.ExtractCoordSys; -import adql.query.operand.function.geometry.GeometryFunction; -import adql.query.operand.function.geometry.GeometryFunction.GeometryValue; import adql.query.operand.function.geometry.IntersectsFunction; import adql.query.operand.function.geometry.PointFunction; import adql.query.operand.function.geometry.PolygonFunction; import adql.query.operand.function.geometry.RegionFunction; /** - *

Translates all ADQL objects into the SQL adaptation of Postgres.

+ *

Translates all ADQL objects into an SQL interrogation query designed for PostgreSQL.

* - *

IMPORTANT: The geometrical functions are translated exactly as in ADQL. - * You will probably need to extend this translator to correctly manage the geometrical functions. - * An extension is already available for PgSphere: {@link PgSphereTranslator}.

+ *

Important: + * The geometrical functions are translated exactly as in ADQL. + * You will probably need to extend this translator to correctly manage the geometrical functions. + * An extension is already available for PgSphere: {@link PgSphereTranslator}. + *

* * @author Grégory Mantelet (CDS;ARI) - * @version 1.2 (03/2014) + * @version 1.3 (11/2014) * * @see PgSphereTranslator */ -public class PostgreSQLTranslator implements ADQLTranslator { +public class PostgreSQLTranslator extends JDBCTranslator { - protected boolean inSelect = false; + /**

Indicate the case sensitivity to apply to each SQL identifier (only SCHEMA, TABLE and COLUMN).

+ * + *

Note: + * In this implementation, this field is set by the constructor and never modified elsewhere. + * It would be better to never modify it after the construction in order to keep a certain consistency. + *

+ */ protected byte caseSensitivity = 0x00; /** - * Builds a PostgreSQLTranslator which takes into account the case sensitivity on column names. - * It means that column names which have been written between double quotes, will be also translated between double quotes. + * Builds a PostgreSQLTranslator which always translates in SQL all identifiers (schema, table and column) in a case sensitive manner ; + * in other words, schema, table and column names will be surrounded by double quotes in the SQL translation. */ public PostgreSQLTranslator(){ - this(true); + caseSensitivity = 0x0F; } /** - * Builds a PostgreSQLTranslator. + * Builds a PostgreSQLTranslator which always translates in SQL all identifiers (schema, table and column) in the specified case sensitivity ; + * in other words, schema, table and column names will all be surrounded or not by double quotes in the SQL translation. * - * @param column true to take into account the case sensitivity of column names, false otherwise. + * @param allCaseSensitive true to translate all identifiers in a case sensitive manner (surrounded by double quotes), false for case insensitivity. */ - public PostgreSQLTranslator(final boolean column){ - caseSensitivity = IdentifierField.COLUMN.setCaseSensitive(caseSensitivity, column); + public PostgreSQLTranslator(final boolean allCaseSensitive){ + caseSensitivity = allCaseSensitive ? (byte)0x0F : (byte)0x00; } /** - * Builds a PostgreSQLTranslator. + * Builds a PostgreSQLTranslator which will always translate in SQL identifiers with the defined case sensitivity. * - * @param catalog true to take into account the case sensitivity of catalog names, false otherwise. - * @param schema true to take into account the case sensitivity of schema names, false otherwise. - * @param table true to take into account the case sensitivity of table names, false otherwise. - * @param column true to take into account the case sensitivity of column names, false otherwise. + * @param catalog true to translate catalog names with double quotes (case sensitive in the DBMS), false otherwise. + * @param schema true to translate schema names with double quotes (case sensitive in the DBMS), false otherwise. + * @param table true to translate table names with double quotes (case sensitive in the DBMS), false otherwise. + * @param column true to translate column names with double quotes (case sensitive in the DBMS), false otherwise. */ public PostgreSQLTranslator(final boolean catalog, final boolean schema, final boolean table, final boolean column){ caseSensitivity = IdentifierField.CATALOG.setCaseSensitive(caseSensitivity, catalog); @@ -125,521 +98,22 @@ public class PostgreSQLTranslator implements ADQLTranslator { caseSensitivity = IdentifierField.COLUMN.setCaseSensitive(caseSensitivity, column); } - /** - * Appends the full name of the given table to the given StringBuffer. - * - * @param str The string buffer. - * @param dbTable The table whose the full name must be appended. - * - * @return The string buffer + full table name. - */ - public final StringBuffer appendFullDBName(final StringBuffer str, final DBTable dbTable){ - if (dbTable != null){ - if (dbTable.getDBCatalogName() != null) - appendIdentifier(str, dbTable.getDBCatalogName(), IdentifierField.CATALOG).append('.'); - - if (dbTable.getDBSchemaName() != null) - appendIdentifier(str, dbTable.getDBSchemaName(), IdentifierField.SCHEMA).append('.'); - - appendIdentifier(str, dbTable.getDBName(), IdentifierField.TABLE); - } - return str; - } - - /** - * Appends the given identifier in the given StringBuffer. - * - * @param str The string buffer. - * @param id The identifier to append. - * @param field The type of identifier (column, table, schema, catalog or alias ?). - * - * @return The string buffer + identifier. - */ - public final StringBuffer appendIdentifier(final StringBuffer str, final String id, final IdentifierField field){ - return appendIdentifier(str, id, field.isCaseSensitive(caseSensitivity)); - } - - /** - * Appends the given identifier to the given StringBuffer. - * - * @param str The string buffer. - * @param id The identifier to append. - * @param caseSensitive true to format the identifier so that preserving the case sensitivity, false otherwise. - * - * @return The string buffer + identifier. - */ - public static final StringBuffer appendIdentifier(final StringBuffer str, final String id, final boolean caseSensitive){ - if (caseSensitive) - return str.append('\"').append(id).append('\"'); - else - return str.append(id); - } - - @Override - @SuppressWarnings("unchecked") - public String translate(ADQLObject obj) throws TranslationException{ - if (obj instanceof ADQLQuery) - return translate((ADQLQuery)obj); - else if (obj instanceof ADQLList) - return translate((ADQLList)obj); - else if (obj instanceof SelectItem) - return translate((SelectItem)obj); - else if (obj instanceof ColumnReference) - return translate((ColumnReference)obj); - else if (obj instanceof ADQLTable) - return translate((ADQLTable)obj); - else if (obj instanceof ADQLJoin) - return translate((ADQLJoin)obj); - else if (obj instanceof ADQLOperand) - return translate((ADQLOperand)obj); - else if (obj instanceof ADQLConstraint) - return translate((ADQLConstraint)obj); - else - return obj.toADQL(); - } - - @Override - public String translate(ADQLQuery query) throws TranslationException{ - StringBuffer sql = new StringBuffer(translate(query.getSelect())); - - sql.append("\nFROM ").append(translate(query.getFrom())); - - if (!query.getWhere().isEmpty()) - sql.append('\n').append(translate(query.getWhere())); - - if (!query.getGroupBy().isEmpty()) - sql.append('\n').append(translate(query.getGroupBy())); - - if (!query.getHaving().isEmpty()) - sql.append('\n').append(translate(query.getHaving())); - - if (!query.getOrderBy().isEmpty()) - sql.append('\n').append(translate(query.getOrderBy())); - - if (query.getSelect().hasLimit()) - sql.append("\nLimit ").append(query.getSelect().getLimit()); - - return sql.toString(); - } - - /* *************************** */ - /* ****** LIST & CLAUSE ****** */ - /* *************************** */ - @Override - public String translate(ADQLList list) throws TranslationException{ - if (list instanceof ClauseSelect) - return translate((ClauseSelect)list); - else if (list instanceof ClauseConstraints) - return translate((ClauseConstraints)list); - else - return getDefaultADQLList(list); - } - - /** - * Gets the default SQL output for a list of ADQL objects. - * - * @param list List to format into SQL. - * - * @return The corresponding SQL. - * - * @throws TranslationException If there is an error during the translation. - */ - protected String getDefaultADQLList(ADQLList list) throws TranslationException{ - String sql = (list.getName() == null) ? "" : (list.getName() + " "); - - boolean oldInSelect = inSelect; - inSelect = (list.getName() != null) && list.getName().equalsIgnoreCase("select"); - - try{ - for(int i = 0; i < list.size(); i++) - sql += ((i == 0) ? "" : (" " + list.getSeparator(i) + " ")) + translate(list.get(i)); - }finally{ - inSelect = oldInSelect; - } - - return sql; - } - @Override - public String translate(ClauseSelect clause) throws TranslationException{ - String sql = null; - - for(int i = 0; i < clause.size(); i++){ - if (i == 0){ - sql = clause.getName() + (clause.distinctColumns() ? " DISTINCT" : ""); - }else - sql += " " + clause.getSeparator(i); - - sql += " " + translate(clause.get(i)); - } - - return sql; - } - - @Override - public String translate(ClauseConstraints clause) throws TranslationException{ - if (clause instanceof ConstraintsGroup) - return "(" + getDefaultADQLList(clause) + ")"; - else - return getDefaultADQLList(clause); - } - - @Override - public String translate(SelectItem item) throws TranslationException{ - if (item instanceof SelectAllColumns) - return translate((SelectAllColumns)item); - - StringBuffer translation = new StringBuffer(translate(item.getOperand())); - if (item.hasAlias()){ - translation.append(" AS "); - appendIdentifier(translation, item.getAlias(), item.isCaseSensitive()); - }else - translation.append(" AS ").append(item.getName()); - - return translation.toString(); - } - - @Override - public String translate(SelectAllColumns item) throws TranslationException{ - HashMap mapAlias = new HashMap(); - - // Fetch the full list of columns to display: - Iterable dbCols = null; - if (item.getAdqlTable() != null && item.getAdqlTable().getDBLink() != null){ - ADQLTable table = item.getAdqlTable(); - dbCols = table.getDBLink(); - if (table.hasAlias()){ - String key = appendFullDBName(new StringBuffer(), table.getDBLink()).toString(); - mapAlias.put(key, table.isCaseSensitive(IdentifierField.ALIAS) ? ("\"" + table.getAlias() + "\"") : table.getAlias()); - } - }else if (item.getQuery() != null){ - try{ - dbCols = item.getQuery().getFrom().getDBColumns(); - }catch(UnresolvedJoin pe){ - throw new TranslationException("Due to a join problem, the ADQL to SQL translation can not be completed!", pe); - } - ArrayList tables = item.getQuery().getFrom().getTables(); - for(ADQLTable table : tables){ - if (table.hasAlias()){ - String key = appendFullDBName(new StringBuffer(), table.getDBLink()).toString(); - mapAlias.put(key, table.isCaseSensitive(IdentifierField.ALIAS) ? ("\"" + table.getAlias() + "\"") : table.getAlias()); - } - } - } - - // Write the DB name of all these columns: - if (dbCols != null){ - StringBuffer cols = new StringBuffer(); - for(DBColumn col : dbCols){ - if (cols.length() > 0) - cols.append(','); - if (col.getTable() != null){ - String fullDbName = appendFullDBName(new StringBuffer(), col.getTable()).toString(); - if (mapAlias.containsKey(fullDbName)) - appendIdentifier(cols, mapAlias.get(fullDbName), false).append('.'); - else - cols.append(fullDbName).append('.'); - } - appendIdentifier(cols, col.getDBName(), IdentifierField.COLUMN); - cols.append(" AS \"").append(col.getADQLName()).append('\"'); - } - return (cols.length() > 0) ? cols.toString() : item.toADQL(); - }else{ - return item.toADQL(); - } - } - - @Override - public String translate(ColumnReference ref) throws TranslationException{ - if (ref instanceof ADQLOrder) - return translate((ADQLOrder)ref); - else - return getDefaultColumnReference(ref); - } - - /** - * Gets the default SQL output for a column reference. - * - * @param ref The column reference to format into SQL. - * - * @return The corresponding SQL. - * - * @throws TranslationException If there is an error during the translation. - */ - protected String getDefaultColumnReference(ColumnReference ref) throws TranslationException{ - if (ref.isIndex()){ - return "" + ref.getColumnIndex(); - }else{ - if (ref.getDBLink() == null){ - return (ref.isCaseSensitive() ? ("\"" + ref.getColumnName() + "\"") : ref.getColumnName()); - }else{ - DBColumn dbCol = ref.getDBLink(); - StringBuffer colName = new StringBuffer(); - // Use the table alias if any: - if (ref.getAdqlTable() != null && ref.getAdqlTable().hasAlias()) - appendIdentifier(colName, ref.getAdqlTable().getAlias(), ref.getAdqlTable().isCaseSensitive(IdentifierField.ALIAS)).append('.'); - - // Use the DBTable if any: - else if (dbCol.getTable() != null) - appendFullDBName(colName, dbCol.getTable()).append('.'); - - appendIdentifier(colName, dbCol.getDBName(), IdentifierField.COLUMN); - - return colName.toString(); - } - } - } - - @Override - public String translate(ADQLOrder order) throws TranslationException{ - return getDefaultColumnReference(order) + (order.isDescSorting() ? " DESC" : " ASC"); - } - - /* ************************** */ - /* ****** TABLE & JOIN ****** */ - /* ************************** */ - @Override - public String translate(FromContent content) throws TranslationException{ - if (content instanceof ADQLTable) - return translate((ADQLTable)content); - else if (content instanceof ADQLJoin) - return translate((ADQLJoin)content); - else - return content.toADQL(); - } - - @Override - public String translate(ADQLTable table) throws TranslationException{ - StringBuffer sql = new StringBuffer(); - - // CASE: SUB-QUERY: - if (table.isSubQuery()) - sql.append('(').append(translate(table.getSubQuery())).append(')'); - - // CASE: TABLE REFERENCE: - else{ - // Use the corresponding DB table, if known: - if (table.getDBLink() != null) - appendFullDBName(sql, table.getDBLink()); - // Otherwise, use the whole table name given in the ADQL query: - else - sql.append(table.getFullTableName()); - } - - // Add the table alias, if any: - if (table.hasAlias()){ - sql.append(" AS "); - appendIdentifier(sql, table.getAlias(), table.isCaseSensitive(IdentifierField.ALIAS)); - } - - return sql.toString(); - } - - @Override - public String translate(ADQLJoin join) throws TranslationException{ - StringBuffer sql = new StringBuffer(translate(join.getLeftTable())); - - if (join.isNatural()) - sql.append(" NATURAL"); - - sql.append(' ').append(join.getJoinType()).append(' ').append(translate(join.getRightTable())).append(' '); - - if (!join.isNatural()){ - if (join.getJoinCondition() != null) - sql.append(translate(join.getJoinCondition())); - else if (join.hasJoinedColumns()){ - StringBuffer cols = new StringBuffer(); - Iterator it = join.getJoinedColumns(); - while(it.hasNext()){ - ADQLColumn item = it.next(); - if (cols.length() > 0) - cols.append(", "); - if (item.getDBLink() == null) - appendIdentifier(cols, item.getColumnName(), item.isCaseSensitive(IdentifierField.COLUMN)); - else - appendIdentifier(cols, item.getDBLink().getDBName(), IdentifierField.COLUMN); - } - sql.append("USING (").append(cols).append(')'); - } - } - - return sql.toString(); - } - - /* ********************* */ - /* ****** OPERAND ****** */ - /* ********************* */ - @Override - public String translate(ADQLOperand op) throws TranslationException{ - if (op instanceof ADQLColumn) - return translate((ADQLColumn)op); - else if (op instanceof Concatenation) - return translate((Concatenation)op); - else if (op instanceof NegativeOperand) - return translate((NegativeOperand)op); - else if (op instanceof NumericConstant) - return translate((NumericConstant)op); - else if (op instanceof StringConstant) - return translate((StringConstant)op); - else if (op instanceof WrappedOperand) - return translate((WrappedOperand)op); - else if (op instanceof Operation) - return translate((Operation)op); - else if (op instanceof ADQLFunction) - return translate((ADQLFunction)op); - else - return op.toADQL(); - } - - @Override - public String translate(ADQLColumn column) throws TranslationException{ - // Use its DB name if known: - if (column.getDBLink() != null){ - DBColumn dbCol = column.getDBLink(); - StringBuffer colName = new StringBuffer(); - // Use the table alias if any: - if (column.getAdqlTable() != null && column.getAdqlTable().hasAlias()) - appendIdentifier(colName, column.getAdqlTable().getAlias(), column.getAdqlTable().isCaseSensitive(IdentifierField.ALIAS)).append('.'); - - // Use the DBTable if any: - else if (dbCol.getTable() != null && dbCol.getTable().getDBName() != null) - appendFullDBName(colName, dbCol.getTable()).append('.'); - - // Otherwise, use the prefix of the column given in the ADQL query: - else if (column.getTableName() != null) - colName = column.getFullColumnPrefix().append('.'); - - appendIdentifier(colName, dbCol.getDBName(), IdentifierField.COLUMN); - - return colName.toString(); - } - // Otherwise, use the whole name given in the ADQL query: - else - return column.getFullColumnName(); - } - - @Override - public String translate(Concatenation concat) throws TranslationException{ - return translate((ADQLList)concat); - } - - @Override - public String translate(NegativeOperand negOp) throws TranslationException{ - return "-" + translate(negOp.getOperand()); - } - - @Override - public String translate(NumericConstant numConst) throws TranslationException{ - return numConst.getValue(); + public boolean isCaseSensitive(final IdentifierField field){ + return field == null ? false : field.isCaseSensitive(caseSensitivity); } @Override public String translate(StringConstant strConst) throws TranslationException{ - return "'" + strConst.getValue() + "'"; - } - - @Override - public String translate(WrappedOperand op) throws TranslationException{ - return "(" + translate(op.getOperand()) + ")"; - } - - @Override - public String translate(Operation op) throws TranslationException{ - return translate(op.getLeftOperand()) + op.getOperation().toADQL() + translate(op.getRightOperand()); - } - - /* ************************ */ - /* ****** CONSTRAINT ****** */ - /* ************************ */ - @Override - public String translate(ADQLConstraint cons) throws TranslationException{ - if (cons instanceof Comparison) - return translate((Comparison)cons); - else if (cons instanceof Between) - return translate((Between)cons); - else if (cons instanceof Exists) - return translate((Exists)cons); - else if (cons instanceof In) - return translate((In)cons); - else if (cons instanceof IsNull) - return translate((IsNull)cons); - else if (cons instanceof NotConstraint) - return translate((NotConstraint)cons); - else - return cons.toADQL(); - } - - @Override - public String translate(Comparison comp) throws TranslationException{ - return translate(comp.getLeftOperand()) + " " + comp.getOperator().toADQL() + " " + translate(comp.getRightOperand()); - } - - @Override - public String translate(Between comp) throws TranslationException{ - return translate(comp.getLeftOperand()) + " BETWEEN " + translate(comp.getMinOperand()) + " AND " + translate(comp.getMaxOperand()); - } - - @Override - public String translate(Exists exists) throws TranslationException{ - return "EXISTS(" + translate(exists.getSubQuery()) + ")"; - } - - @Override - public String translate(In in) throws TranslationException{ - return translate(in.getOperand()) + " " + in.getName() + " (" + (in.hasSubQuery() ? translate(in.getSubQuery()) : translate(in.getValuesList())) + ")"; - } - - @Override - public String translate(IsNull isNull) throws TranslationException{ - return translate(isNull.getColumn()) + " IS " + (isNull.isNotNull() ? "NOT " : "") + "NULL"; - } - - @Override - public String translate(NotConstraint notCons) throws TranslationException{ - return "NOT " + translate(notCons.getConstraint()); - } - - /* *********************** */ - /* ****** FUNCTIONS ****** */ - /* *********************** */ - @Override - public String translate(ADQLFunction fct) throws TranslationException{ - if (fct instanceof GeometryFunction) - return translate((GeometryFunction)fct); - else if (fct instanceof MathFunction) - return translate((MathFunction)fct); - else if (fct instanceof SQLFunction) - return translate((SQLFunction)fct); - else if (fct instanceof UserDefinedFunction) - return translate((UserDefinedFunction)fct); + // Deal with the special escaping syntax of Postgres: + /* A string containing characters to escape must be prefixed by an E. + * Without this prefix, Potsgres does not escape the concerned characters and + * consider backslashes as normal characters. + * For instance: E'foo\tfoo2'. */ + if (strConst.getValue() != null && strConst.getValue().contains("\\")) + return "E'" + strConst.getValue() + "'"; else - return getDefaultADQLFunction(fct); - } - - /** - * Gets the default SQL output for the given ADQL function. - * - * @param fct The ADQL function to format into SQL. - * - * @return The corresponding SQL. - * - * @throws TranslationException If there is an error during the translation. - */ - protected String getDefaultADQLFunction(ADQLFunction fct) throws TranslationException{ - String sql = fct.getName() + "("; - - for(int i = 0; i < fct.getNbParameters(); i++) - sql += ((i == 0) ? "" : ", ") + translate(fct.getParameter(i)); - - return sql + ")"; - } - - @Override - public String translate(SQLFunction fct) throws TranslationException{ - if (fct.getType() == SQLFunctionType.COUNT_ALL) - return "COUNT(" + (fct.isDistinct() ? "DISTINCT " : "") + "*)"; - else - return fct.getName() + "(" + (fct.isDistinct() ? "DISTINCT " : "") + translate(fct.getParameter(0)) + ")"; + return super.translate(strConst); } @Override @@ -658,125 +132,168 @@ public class PostgreSQLTranslator implements ADQLTranslator { } } - @Override - public String translate(UserDefinedFunction fct) throws TranslationException{ - return getDefaultADQLFunction(fct); - } - - /* *********************************** */ - /* ****** GEOMETRICAL FUNCTIONS ****** */ - /* *********************************** */ - @Override - public String translate(GeometryFunction fct) throws TranslationException{ - if (fct instanceof AreaFunction) - return translate((AreaFunction)fct); - else if (fct instanceof BoxFunction) - return translate((BoxFunction)fct); - else if (fct instanceof CentroidFunction) - return translate((CentroidFunction)fct); - else if (fct instanceof CircleFunction) - return translate((CircleFunction)fct); - else if (fct instanceof ContainsFunction) - return translate((ContainsFunction)fct); - else if (fct instanceof DistanceFunction) - return translate((DistanceFunction)fct); - else if (fct instanceof ExtractCoord) - return translate((ExtractCoord)fct); - else if (fct instanceof ExtractCoordSys) - return translate((ExtractCoordSys)fct); - else if (fct instanceof IntersectsFunction) - return translate((IntersectsFunction)fct); - else if (fct instanceof PointFunction) - return translate((PointFunction)fct); - else if (fct instanceof PolygonFunction) - return translate((PolygonFunction)fct); - else if (fct instanceof RegionFunction) - return translate((RegionFunction)fct); - else - return getDefaultGeometryFunction(fct); - } - - /** - *

Gets the default SQL output for the given geometrical function.

- * - *

Note: By default, only the ADQL serialization is returned.

- * - * @param fct The geometrical function to translate. - * - * @return The corresponding SQL. - * - * @throws TranslationException If there is an error during the translation. - */ - protected String getDefaultGeometryFunction(GeometryFunction fct) throws TranslationException{ - if (inSelect) - return "'" + fct.toADQL().replaceAll("'", "''") + "'"; - else - return getDefaultADQLFunction(fct); - } - - @Override - public String translate(GeometryValue geomValue) throws TranslationException{ - return translate(geomValue.getValue()); - } - @Override public String translate(ExtractCoord extractCoord) throws TranslationException{ - return getDefaultGeometryFunction(extractCoord); + return getDefaultADQLFunction(extractCoord); } @Override public String translate(ExtractCoordSys extractCoordSys) throws TranslationException{ - return getDefaultGeometryFunction(extractCoordSys); + return getDefaultADQLFunction(extractCoordSys); } @Override public String translate(AreaFunction areaFunction) throws TranslationException{ - return getDefaultGeometryFunction(areaFunction); + return getDefaultADQLFunction(areaFunction); } @Override public String translate(CentroidFunction centroidFunction) throws TranslationException{ - return getDefaultGeometryFunction(centroidFunction); + return getDefaultADQLFunction(centroidFunction); } @Override public String translate(DistanceFunction fct) throws TranslationException{ - return getDefaultGeometryFunction(fct); + return getDefaultADQLFunction(fct); } @Override public String translate(ContainsFunction fct) throws TranslationException{ - return getDefaultGeometryFunction(fct); + return getDefaultADQLFunction(fct); } @Override public String translate(IntersectsFunction fct) throws TranslationException{ - return getDefaultGeometryFunction(fct); + return getDefaultADQLFunction(fct); } @Override public String translate(BoxFunction box) throws TranslationException{ - return getDefaultGeometryFunction(box); + return getDefaultADQLFunction(box); } @Override public String translate(CircleFunction circle) throws TranslationException{ - return getDefaultGeometryFunction(circle); + return getDefaultADQLFunction(circle); } @Override public String translate(PointFunction point) throws TranslationException{ - return getDefaultGeometryFunction(point); + return getDefaultADQLFunction(point); } @Override public String translate(PolygonFunction polygon) throws TranslationException{ - return getDefaultGeometryFunction(polygon); + return getDefaultADQLFunction(polygon); } @Override public String translate(RegionFunction region) throws TranslationException{ - return getDefaultGeometryFunction(region); + return getDefaultADQLFunction(region); + } + + @Override + public DBType convertTypeFromDB(final int dbmsType, final String rawDbmsTypeName, String dbmsTypeName, final String[] params){ + // If no type is provided return VARCHAR: + if (dbmsTypeName == null || dbmsTypeName.trim().length() == 0) + return new DBType(DBDatatype.VARCHAR, DBType.NO_LENGTH); + + // Put the dbmsTypeName in lower case for the following comparisons: + dbmsTypeName = dbmsTypeName.toLowerCase(); + + // Extract the length parameter (always the first one): + int lengthParam = DBType.NO_LENGTH; + if (params != null && params.length > 0){ + try{ + lengthParam = Integer.parseInt(params[0]); + }catch(NumberFormatException nfe){} + } + + // SMALLINT + if (dbmsTypeName.equals("smallint") || dbmsTypeName.equals("int2") || dbmsTypeName.equals("smallserial") || dbmsTypeName.equals("serial2") || dbmsTypeName.equals("boolean") || dbmsTypeName.equals("bool")) + return new DBType(DBDatatype.SMALLINT); + // INTEGER + else if (dbmsTypeName.equals("integer") || dbmsTypeName.equals("int") || dbmsTypeName.equals("int4") || dbmsTypeName.equals("serial") || dbmsTypeName.equals("serial4")) + return new DBType(DBDatatype.INTEGER); + // BIGINT + else if (dbmsTypeName.equals("bigint") || dbmsTypeName.equals("int8") || dbmsTypeName.equals("bigserial") || dbmsTypeName.equals("bigserial8")) + return new DBType(DBDatatype.BIGINT); + // REAL + else if (dbmsTypeName.equals("real") || dbmsTypeName.equals("float4")) + return new DBType(DBDatatype.REAL); + // DOUBLE + else if (dbmsTypeName.equals("double precision") || dbmsTypeName.equals("float8")) + return new DBType(DBDatatype.DOUBLE); + // BINARY + else if (dbmsTypeName.equals("bit")) + return new DBType(DBDatatype.BINARY, lengthParam); + // VARBINARY + else if (dbmsTypeName.equals("bit varying") || dbmsTypeName.equals("varbit")) + return new DBType(DBDatatype.VARBINARY, lengthParam); + // CHAR + else if (dbmsTypeName.equals("char") || dbmsTypeName.equals("character")) + return new DBType(DBDatatype.CHAR, lengthParam); + // VARCHAR + else if (dbmsTypeName.equals("varchar") || dbmsTypeName.equals("character varying")) + return new DBType(DBDatatype.VARCHAR, lengthParam); + // BLOB + else if (dbmsTypeName.equals("bytea")) + return new DBType(DBDatatype.BLOB); + // CLOB + else if (dbmsTypeName.equals("text")) + return new DBType(DBDatatype.CLOB); + // TIMESTAMP + else if (dbmsTypeName.equals("timestamp") || dbmsTypeName.equals("timestamptz") || dbmsTypeName.equals("time") || dbmsTypeName.equals("timetz") || dbmsTypeName.equals("date")) + return new DBType(DBDatatype.TIMESTAMP); + // Default: + else + return new DBType(DBDatatype.VARCHAR, DBType.NO_LENGTH); + } + + @Override + public String convertTypeToDB(final DBType type){ + if (type == null) + return "VARCHAR"; + + switch(type.type){ + + case SMALLINT: + case INTEGER: + case REAL: + case BIGINT: + case CHAR: + case VARCHAR: + case TIMESTAMP: + return type.type.toString(); + + case DOUBLE: + return "DOUBLE PRECISION"; + + case BINARY: + case VARBINARY: + return "bytea"; + + case BLOB: + return "bytea"; + + case CLOB: + return "TEXT"; + + case POINT: + case REGION: + default: + return "VARCHAR"; + } + } + + @Override + public Region translateGeometryFromDB(final Object jdbcColValue) throws ParseException{ + throw new ParseException("Unsupported geometrical value! The value \"" + jdbcColValue + "\" can not be parsed as a region."); + } + + @Override + public Object translateGeometryToDB(final Region region) throws ParseException{ + throw new ParseException("Geometries can not be uploaded in the database in this implementation!"); } } diff --git a/src/cds/utils/TextualSearchList.java b/src/cds/utils/TextualSearchList.java index dfbff86..3dd9888 100644 --- a/src/cds/utils/TextualSearchList.java +++ b/src/cds/utils/TextualSearchList.java @@ -17,7 +17,7 @@ package cds.utils; * along with ADQLLibrary. If not, see . * * Copyright 2012-2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomisches Rechen Institute (ARI) + * Astronomisches Rechen Institut (ARI) */ import java.util.ArrayList; diff --git a/src/org/json/Json4Uws.java b/src/org/json/Json4Uws.java index bfa25e3..9e84863 100644 --- a/src/org/json/Json4Uws.java +++ b/src/org/json/Json4Uws.java @@ -16,26 +16,26 @@ package org.json; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.util.Iterator; +import uws.ISO8601Format; import uws.job.ErrorSummary; import uws.job.JobList; import uws.job.Result; import uws.job.UWSJob; - import uws.job.user.JobOwner; - import uws.service.UWS; import uws.service.UWSUrl; /** * Useful conversion functions from UWS to JSON. * - * @author Grégory Mantelet (CDS) - * @version 05/2012 + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (12/2014) */ public final class Json4Uws { @@ -130,11 +130,11 @@ public final class Json4Uws { json.put(UWSJob.PARAM_OWNER, job.getOwner().getPseudo()); json.put(UWSJob.PARAM_QUOTE, job.getQuote()); if (job.getStartTime() != null) - json.put(UWSJob.PARAM_START_TIME, UWSJob.dateFormat.format(job.getStartTime())); + json.put(UWSJob.PARAM_START_TIME, ISO8601Format.format(job.getStartTime())); if (job.getEndTime() != null) - json.put(UWSJob.PARAM_END_TIME, UWSJob.dateFormat.format(job.getEndTime())); + json.put(UWSJob.PARAM_END_TIME, ISO8601Format.format(job.getEndTime())); if (job.getDestructionTime() != null) - json.put(UWSJob.PARAM_DESTRUCTION_TIME, UWSJob.dateFormat.format(job.getDestructionTime())); + json.put(UWSJob.PARAM_DESTRUCTION_TIME, ISO8601Format.format(job.getDestructionTime())); json.put(UWSJob.PARAM_EXECUTION_DURATION, job.getExecutionDuration()); json.put(UWSJob.PARAM_PARAMETERS, getJobParamsJson(job)); json.put(UWSJob.PARAM_RESULTS, getJobResultsJson(job)); @@ -153,8 +153,23 @@ public final class Json4Uws { public final static JSONObject getJobParamsJson(final UWSJob job) throws JSONException{ JSONObject json = new JSONObject(); if (job != null){ - for(String name : job.getAdditionalParameters()) - json.put(name, job.getAdditionalParameterValue(name)); + Object val; + for(String name : job.getAdditionalParameters()){ + // get the raw value: + val = job.getAdditionalParameterValue(name); + // if an array, build a JSON array of strings: + if (val != null && val.getClass().isArray()){ + JSONArray array = new JSONArray(); + for(Object o : (Object[])val){ + if (o != null) + array.put(o.toString()); + } + json.put(name, array); + } + // otherwise, just put the value: + else + json.put(name, val); + } } return json; } diff --git a/src/tap/ADQLExecutor.java b/src/tap/ADQLExecutor.java index 6b70b0c..27ec125 100644 --- a/src/tap/ADQLExecutor.java +++ b/src/tap/ADQLExecutor.java @@ -16,141 +16,203 @@ package tap; * You should have received a copy of the GNU Lesser General Public License * along with TAPLibrary. If not, see . * - * Copyright 2012-2013 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomisches Rechen Institute (ARI) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.io.IOException; import java.io.OutputStream; -import java.sql.SQLException; import javax.servlet.http.HttpServletResponse; +import tap.data.DataReadException; +import tap.data.TableIterator; import tap.db.DBConnection; import tap.db.DBException; import tap.formatter.OutputFormat; import tap.log.TAPLog; import tap.metadata.TAPSchema; import tap.metadata.TAPTable; +import tap.parameters.DALIUpload; import tap.parameters.TAPParameters; -import tap.upload.TableLoader; +import tap.upload.Uploader; import uws.UWSException; +import uws.UWSToolBox; import uws.job.JobThread; import uws.job.Result; +import uws.service.log.UWSLog.LogLevel; import adql.parser.ADQLParser; import adql.parser.ADQLQueryFactory; import adql.parser.ParseException; -import adql.parser.QueryChecker; import adql.query.ADQLQuery; -import adql.translator.ADQLTranslator; -import adql.translator.TranslationException; /** + *

Let process completely an ADQL query.

* + *

Thus, this class aims to apply the following actions (in the given order):

+ *
    + *
  1. Upload the user tables, if any
  2. + *
  3. Parse the ADQL query (and so, transform it in an object tree)
  4. + *
  5. Execute it in the "database"
  6. + *
  7. Format and write the result
  8. + *
  9. Drop all uploaded tables from the "database"
  10. + *
* - * @author Grégory Mantelet (CDS;ARI) - gmantele@ari.uni-heidelberg.de - * @version 1.1 (12/2013) + *

Job execution mode

* - * @param + *

+ * This executor is able to process queries coming from a synchronous job (the result must be written directly in the HTTP response) + * and from an asynchronous job (the result must be written, generally, in a file). Two start(...) functions let deal with + * the differences between the two job execution modes: {@link #start(AsyncThread)} for asynchronous jobs + * and {@link #start(Thread, String, TAPParameters, HttpServletResponse)} for synchronous jobs. + *

+ * + *

Input/Output formats

+ * + *

Uploaded tables must be provided in VOTable format.

+ * + *

+ * Query results must be formatted in the format specified by the user in the job parameters. A corresponding formatter ({@link OutputFormat}) + * is asked to the description of the TAP service ({@link ServiceConnection}). If none can be found, VOTable will be chosen by default. + *

+ * + *

Executor customization

+ * + *

It is totally possible to customize some parts of the ADQL query processing. However, the main algorithm must remain the same and is implemented + * by {@link #start()}. This function is final, like {@link #start(AsyncThread)} and {@link #start(Thread, String, TAPParameters, HttpServletResponse)}, + * which are just preparing the execution for {@link #start()} in function of the job execution mode (asynchronous or synchronous). + *

+ * + *

Note: + * {@link #start()} is using the Template Method Design Pattern: it defines the skeleton/algorithm of the processing, and defers some steps + * to other functions. + *

+ * + *

+ * So, you are able to customize almost all individual steps of the ADQL query processing: {@link #parseADQL()}, {@link #executeADQL(ADQLQuery)} and + * {@link #writeResult(TableIterator, OutputFormat, OutputStream)}. + *

+ * + *

Note: + * Note that the formatting of the result is done by an OutputFormat and that the executor is just calling the appropriate function of the formatter. + *

+ * + *

+ * There is no way in this executor to customize the upload. However, it does not mean it can not be customized. + * Indeed you can do it easily by extending {@link Uploader} and by providing the new class inside your {@link TAPFactory} implementation + * (see {@link TAPFactory#createUploader(DBConnection)}). + *

+ * + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (04/2015) */ -public class ADQLExecutor< R > { +public class ADQLExecutor { - protected final ServiceConnection service; + /** Description of the current TAP service. */ + protected final ServiceConnection service; + /** The logger to use. */ protected final TAPLog logger; + /** The thread which is using this executor. */ protected Thread thread; + /** List of all TAP parameters needed for the query execution (and particularly the ADQL query itself). */ protected TAPParameters tapParams; + /** Description of the ADQL schema containing all the tables uploaded by the user for this specific query execution. + * Note: This attribute is NULL before calling one of the start(...) function. It MAY be NULL also after if no table has been uploaded. */ + protected TAPSchema uploadSchema = null; + + /** The HTTP response in which the query execution must be written. This attribute is NULL if the execution is asynchronous. */ protected HttpServletResponse response; + /** The execution report to fill gradually while the processing of the query. + * Note: This attribute is NULL before calling one of the start(...) function, but it will never be after this call. */ protected TAPExecutionReport report; - private DBConnection dbConn = null; - protected TAPSchema uploadSchema = null; - - public ADQLExecutor(final ServiceConnection service){ + /** Connection to the "database". + * Note: This attribute is NULL before and after the query processing (= call of a start(...) function). */ + private DBConnection dbConn = null; + /** ID of the current query processing step (uploading, parsing, execution, writing result, ...). + * Note: This attribute is NULL before and after the query processing (= call of a start(...) function). */ + private ExecutionProgression progression = null; + /** Date/Time at which the current query processing step has started. */ + private long startStep = -1; + + /** + * Build an {@link ADQLExecutor}. + * + * @param service The description of the TAP service. + */ + public ADQLExecutor(final ServiceConnection service){ this.service = service; this.logger = service.getLogger(); } + /** + * Get the logger used by this executor. + * + * @return The used logger. + */ public final TAPLog getLogger(){ return logger; } + /** + *

Get the report of the query execution. It helps indicating the execution progression and the duration of each step.

+ * + *

Note: + * Before starting the execution (= before the call of a "start(...)" function), this function will return NULL. + * It is set when the query processing starts and remains not NULL after that (even when the execution is finished). + *

+ * + * @return The execution report. + */ public final TAPExecutionReport getExecReport(){ return report; } - public boolean hasUploadedTables(){ - return (uploadSchema != null) && (uploadSchema.getNbTables() > 0); - } - - protected final DBConnection getDBConnection() throws TAPException{ - return (dbConn != null) ? dbConn : (dbConn = service.getFactory().createDBConnection((report != null) ? report.jobID : null)); - } - - public final void closeDBConnection() throws TAPException{ - if (dbConn != null){ - dbConn.close(); - dbConn = null; - } - } - - private final void uploadTables() throws TAPException{ - TableLoader[] tables = tapParams.getTableLoaders(); - if (tables.length > 0){ - logger.info("JOB " + report.jobID + "\tLoading uploaded tables (" + tables.length + ")..."); - long start = System.currentTimeMillis(); - try{ - /* TODO Problem with the DBConnection! One is created here for the Uploader (and dbConn is set) and closed by its uploadTables function (but dbConn is not set to null). - * Ideally, the connection should not be close, or at least dbConn should be set to null just after. */ - uploadSchema = service.getFactory().createUploader(getDBConnection()).upload(tables); - }finally{ - TAPParameters.deleteUploadedTables(tables); - report.setDuration(ExecutionProgression.UPLOADING, System.currentTimeMillis() - start); - } - } - - } - - private final R executeADQL() throws ParseException, InterruptedException, TranslationException, SQLException, TAPException, UWSException{ - long start; - - tapParams.set(TAPJob.PARAM_PROGRESSION, ExecutionProgression.PARSING); - start = System.currentTimeMillis(); - ADQLQuery adql = parseADQL(); - report.setDuration(ExecutionProgression.PARSING, System.currentTimeMillis() - start); - - if (thread.isInterrupted()) - throw new InterruptedException(); - - report.resultingColumns = adql.getResultingColumns(); - - final int limit = adql.getSelect().getLimit(); - final Integer maxRec = tapParams.getMaxRec(); - if (maxRec != null && maxRec > -1){ - if (limit <= -1 || limit > maxRec) - adql.getSelect().setLimit(maxRec + 1); - } - - tapParams.set(TAPJob.PARAM_PROGRESSION, ExecutionProgression.TRANSLATING); - start = System.currentTimeMillis(); - String sqlQuery = translateADQL(adql); - report.setDuration(ExecutionProgression.TRANSLATING, System.currentTimeMillis() - start); - report.sqlTranslation = sqlQuery; - - if (thread.isInterrupted()) - throw new InterruptedException(); + /** + *

Get the object to use in order to write the query result in the appropriate format + * (either the asked one, or else VOTable).

+ * + * @return The appropriate result formatter to use. Can not be NULL! + * + * @throws TAPException If no format corresponds to the asked one and if no default format (for VOTable) can be found. + * + * @see ServiceConnection#getOutputFormat(String) + */ + protected OutputFormat getFormatter() throws TAPException{ + // Search for the corresponding formatter: + String format = tapParams.getFormat(); + OutputFormat formatter = service.getOutputFormat((format == null) ? "votable" : format); + if (format != null && formatter == null) + formatter = service.getOutputFormat("votable"); - tapParams.set(TAPJob.PARAM_PROGRESSION, ExecutionProgression.EXECUTING_SQL); - start = System.currentTimeMillis(); - R result = executeQuery(sqlQuery, adql); - report.setDuration(ExecutionProgression.EXECUTING_SQL, System.currentTimeMillis() - start); + // Format the result: + if (formatter == null) + throw new TAPException("Impossible to format the query result: no formatter has been found for the given MIME type \"" + format + "\" and for the default MIME type \"votable\" (short form) !"); - return result; + return formatter; } - public final TAPExecutionReport start(final AsyncThread thread) throws TAPException, UWSException, InterruptedException, ParseException, TranslationException, SQLException{ + /** + *

Start the asynchronous processing of the ADQL query.

+ * + *

+ * This function initialize the execution report, get the execution parameters (including the query to process) + * and then call {@link #start()}. + *

+ * + * @param thread The asynchronous thread which asks the query processing. + * + * @return The resulting execution report. + * + * @throws UWSException If any error occurs while executing the ADQL query. + * @throws InterruptedException If the job has been interrupted (by the user or a time-out). + * + * @see #start() + */ + public final TAPExecutionReport start(final AsyncThread thread) throws UWSException, InterruptedException{ if (this.thread != null || this.report != null) - throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, "This ADQLExecutor has already been executed !"); + throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, "This ADQLExecutor has already been executed!"); this.thread = thread; @@ -159,164 +221,480 @@ public class ADQLExecutor< R > { this.report = new TAPExecutionReport(tapJob.getJobId(), false, tapParams); this.response = null; - return start(); + try{ + return start(); + }catch(IOException ioe){ + throw new UWSException(ioe); + }catch(TAPException te){ + throw new UWSException(te.getHttpErrorCode(), te); + } + } + + /** + *

Create the database connection required for the ADQL execution.

+ * + *

Note: This function has no effect if the DB connection already exists.

+ * + * @param jobID ID of the job which will be executed by this {@link ADQLExecutor}. + * This ID will be the database connection ID. + * + * @throws TAPException If the DB connection creation fails. + * + * @see TAPFactory#getConnection(String) + * + * @since 2.0 + */ + public final void initDBConnection(final String jobID) throws TAPException{ + if (dbConn == null) + dbConn = service.getFactory().getConnection(jobID); } - public final TAPExecutionReport start(final Thread thread, final String jobId, final TAPParameters params, final HttpServletResponse response) throws TAPException, UWSException, InterruptedException, ParseException, TranslationException, SQLException{ + /** + *

Start the synchronous processing of the ADQL query.

+ * + *

This function initialize the execution report and then call {@link #start()}.

+ * + * @param thread The synchronous thread which asks the query processing. + * @param jobId ID of the corresponding job. + * @param params All execution parameters (including the query to process). + * @param response Object in which the result or the error must be written. + * + * @return The resulting execution report. + * + * @throws TAPException If any error occurs while executing the ADQL query. + * @throws IOException If any error occurs while writing the result in the given {@link HttpServletResponse}. + * @throws InterruptedException If the job has been interrupted (by the user or a time-out). + * + * @see #start() + */ + public final TAPExecutionReport start(final Thread thread, final String jobId, final TAPParameters params, final HttpServletResponse response) throws TAPException, IOException, InterruptedException{ if (this.thread != null || this.report != null) - throw new TAPException("This ADQLExecutor has already been executed !"); + throw new TAPException("This ADQLExecutor has already been executed!"); this.thread = thread; this.tapParams = params; this.report = new TAPExecutionReport(jobId, true, tapParams); this.response = response; - return start(); + try{ + return start(); + }catch(UWSException ue){ + throw new TAPException(ue, ue.getHttpErrorCode()); + } } - protected final TAPExecutionReport start() throws TAPException, UWSException, InterruptedException, ParseException, TranslationException, SQLException{ + /** + *

Process the ADQL query.

+ * + *

This function calls the following function (in the same order):

+ *
    + *
  1. {@link TAPFactory#getConnection(String)}
  2. + *
  3. {@link #uploadTables()}
  4. + *
  5. {@link #parseADQL()}
  6. + *
  7. {@link #executeADQL(ADQLQuery)}
  8. + *
  9. {@link #writeResult(TableIterator)}
  10. + *
  11. {@link #dropUploadedTables()}
  12. + *
  13. {@link TAPFactory#freeConnection(DBConnection)}
  14. + *
+ * + *

+ * The execution report is updated gradually. Besides a job parameter - progression - is set at each step of the process in order to + * notify the user of the progression of the query execution. This parameter is removed at the end of the execution if it is successful. + *

+ * + *

The "interrupted" flag of the associated thread is often tested so that stopping the execution as soon as possible.

+ * + * @return The updated execution report. + * + * @throws TAPException If any error occurs while executing the ADQL query. + * @throws UWSException If any error occurs while executing the ADQL query. + * @throws IOException If an error happens while writing the result in the specified {@link HttpServletResponse}. + * That kind of error can be thrown only in synchronous mode. + * In asynchronous, the error is stored as job error report and is never propagated. + * @throws InterruptedException If the job has been interrupted (by the user or a time-out). + */ + protected final TAPExecutionReport start() throws TAPException, UWSException, IOException, InterruptedException{ + logger.logTAP(LogLevel.INFO, report, "START_EXEC", (report.synchronous ? "Synchronous" : "Asynchronous") + " execution of an ADQL query STARTED.", null); + + // Save the start time (for reporting usage): long start = System.currentTimeMillis(); + + TableIterator queryResult = null; + try{ - // Upload tables if needed: - if (tapParams != null && tapParams.getTableLoaders() != null && tapParams.getTableLoaders().length > 0){ - tapParams.set(TAPJob.PARAM_PROGRESSION, ExecutionProgression.UPLOADING); + // Get a "database" connection: + initDBConnection(report.jobID); + + // 1. UPLOAD TABLES, if there is any: + if (tapParams.getUploadedTables() != null && tapParams.getUploadedTables().length > 0){ + startStep(ExecutionProgression.UPLOADING); uploadTables(); + endStep(); } if (thread.isInterrupted()) throw new InterruptedException(); - // Parse, translate in SQL and execute the ADQL query: - R queryResult = executeADQL(); - if (queryResult == null || thread.isInterrupted()) + // 2. PARSE THE ADQL QUERY: + startStep(ExecutionProgression.PARSING); + // Parse the query: + ADQLQuery adqlQuery = null; + try{ + adqlQuery = parseADQL(); + }catch(ParseException pe){ + if (report.synchronous) + throw new TAPException("Incorrect ADQL query: " + pe.getMessage(), pe, UWSException.BAD_REQUEST, tapParams.getQuery(), progression); + else + throw new UWSException(UWSException.BAD_REQUEST, pe, "Incorrect ADQL query: " + pe.getMessage()); + } + // List all resulting columns (it will be useful later to format the result): + report.resultingColumns = adqlQuery.getResultingColumns(); + endStep(); + + if (thread.isInterrupted()) throw new InterruptedException(); - // Write the result: - tapParams.set(TAPJob.PARAM_PROGRESSION, ExecutionProgression.WRITING_RESULT); - writeResult(queryResult); + // 3. EXECUTE THE ADQL QUERY: + startStep(ExecutionProgression.EXECUTING_ADQL); + queryResult = executeADQL(adqlQuery); + endStep(); - logger.info("JOB " + report.jobID + " COMPLETED"); - tapParams.set(TAPJob.PARAM_PROGRESSION, ExecutionProgression.FINISHED); + if (thread.isInterrupted()) + throw new InterruptedException(); + // 4. WRITE RESULT: + startStep(ExecutionProgression.WRITING_RESULT); + writeResult(queryResult); + endStep(); + + // Report the COMPLETED status: + tapParams.remove(TAPJob.PARAM_PROGRESSION); report.success = true; + // Set the total duration in the report: + report.setTotalDuration(System.currentTimeMillis() - start); + + // Log and report the end of this execution: + logger.logTAP(LogLevel.INFO, report, "END_EXEC", "ADQL query execution finished.", null); + return report; - }catch(NullPointerException npe){ - npe.printStackTrace(); - throw npe; }finally{ + // Close the result if any: + if (queryResult != null){ + try{ + queryResult.close(); + }catch(DataReadException dre){ + logger.logTAP(LogLevel.WARNING, report, "END_EXEC", "Can not close the database query result!", dre); + } + } + + // Drop all the uploaded tables (they are not supposed to exist after the query execution): try{ dropUploadedTables(); }catch(TAPException e){ - logger.error("JOB " + report.jobID + "\tCan not drop uploaded tables !", e); + logger.logTAP(LogLevel.WARNING, report, "END_EXEC", "Can not drop the uploaded tables from the database!", e); } - try{ - closeDBConnection(); - }catch(TAPException e){ - logger.error("JOB " + report.jobID + "\tCan not close the DB connection !", e); + + // Free the connection (so that giving it back to a pool, if any, otherwise, just free resources): + if (dbConn != null){ + service.getFactory().freeConnection(dbConn); + dbConn = null; } - report.setTotalDuration(System.currentTimeMillis() - start); - logger.queryFinished(report); } } - protected ADQLQuery parseADQL() throws ParseException, InterruptedException, TAPException{ - ADQLQueryFactory queryFactory = service.getFactory().createQueryFactory(); - QueryChecker queryChecker = service.getFactory().createQueryChecker(uploadSchema); - ADQLParser parser; - if (queryFactory == null) - parser = new ADQLParser(queryChecker); - else - parser = new ADQLParser(queryChecker, queryFactory); - parser.setCoordinateSystems(service.getCoordinateSystems()); - parser.setDebug(false); - //logger.info("Job "+report.jobID+" - 1/5 Parsing ADQL...."); - return parser.parseQuery(tapParams.getQuery()); + /** + *

Memorize the time at which the step starts, the step ID and update the job parameter "progression" + * (to notify the user about the progression of the query processing).

+ * + *

Note: + * If for some reason the job parameter "progression" can not be updated, no error will be thrown. A WARNING message + * will be just written in the log. + *

+ * + *

Note: + * This function is designed to work with {@link #endStep()}, which must be called after it, when the step is finished (successfully or not). + *

+ * + * @param progression ID of the starting step. + * + * @see #endStep() + */ + private void startStep(final ExecutionProgression progression){ + // Save the start time (for report usage): + startStep = System.currentTimeMillis(); + // Memorize the current step: + this.progression = progression; + // Update the job parameter "progression", to notify the user about the progression of the query processing: + try{ + tapParams.set(TAPJob.PARAM_PROGRESSION, this.progression); + }catch(UWSException ue){ + // should not happen, but just in case... + logger.logTAP(LogLevel.WARNING, report, "START_STEP", "Can not set/update the informative job parameter \"" + TAPJob.PARAM_PROGRESSION + "\" (this parameter would be just for notification purpose about the execution progression)!", ue); + } } - protected String translateADQL(ADQLQuery query) throws TranslationException, InterruptedException, TAPException{ - ADQLTranslator translator = service.getFactory().createADQLTranslator(); - //logger.info("Job "+report.jobID+" - 2/5 Translating ADQL..."); - return translator.translate(query); + /** + *

Set the duration of the current step in the execution report.

+ * + *

Note: + * The start time and the ID of the step are then forgotten. + *

+ * + *

Note: + * This function is designed to work with {@link #startStep(ExecutionProgression)}, which must be called before it, when the step is starting. + * It marks the end of a step. + *

+ * + * @see #startStep(ExecutionProgression) + */ + private void endStep(){ + if (progression != null){ + // Set the duration of this step in the execution report: + report.setDuration(progression, System.currentTimeMillis() - startStep); + // No start time: + startStep = -1; + // No step for the moment: + progression = null; + } } - protected R executeQuery(String sql, ADQLQuery adql) throws SQLException, InterruptedException, TAPException{ - //logger.info("Job "+report.jobID+" - 3/5 Creating DBConnection...."); - DBConnection dbConn = getDBConnection(); - //logger.info("Job "+report.jobID+" - 4/5 Executing query...\n"+sql); - final long startTime = System.currentTimeMillis(); - R result = dbConn.executeQuery(sql, adql); - if (result == null) - logger.info("JOB " + report.jobID + " - QUERY ABORTED AFTER " + (System.currentTimeMillis() - startTime) + " MS !"); - else - logger.info("JOB " + report.jobID + " - QUERY SUCCESFULLY EXECUTED IN " + (System.currentTimeMillis() - startTime) + " MS !"); - return result; + /** + *

Create in the "database" all tables uploaded by the user (only for this specific query execution).

+ * + *

Note: + * Obviously, nothing is done if no table has been uploaded. + *

+ * + * @throws TAPException If any error occurs while reading the uploaded table + * or while importing them in the database. + */ + private final void uploadTables() throws TAPException{ + // Fetch the tables to upload: + DALIUpload[] tables = tapParams.getUploadedTables(); + + // Upload them, if needed: + if (tables.length > 0){ + logger.logTAP(LogLevel.INFO, report, "UPLOADING", "Loading uploaded tables (" + tables.length + ")", null); + uploadSchema = service.getFactory().createUploader(dbConn).upload(tables); + } } - protected OutputFormat getFormatter() throws TAPException{ - // Search for the corresponding formatter: - String format = tapParams.getFormat(); - OutputFormat formatter = service.getOutputFormat((format == null) ? "votable" : format); - if (format != null && formatter == null) - formatter = service.getOutputFormat("votable"); + /** + *

Parse the ADQL query provided in the parameters by the user.

+ * + *

The query factory and the query checker are got from the TAP factory.

+ * + *

+ * The configuration of this TAP service list all allowed coordinate systems. These are got here and provided to the query checker + * in order to ensure the coordinate systems used in the query are in this list. + *

+ * + *

+ * The row limit specified in the ADQL query (with TOP) is checked and adjusted (if needed). Indeed, this limit + * can not exceed MAXREC given in parameter and the maximum value specified in the configuration of this TAP service. + * In the case no row limit is specified in the query or the given value is greater than MAXREC, (MAXREC+1) is used by default. + * The "+1" aims to detect overflows. + *

+ * + * @return The object representation of the ADQL query. + * + * @throws ParseException If the given ADQL query can not be parsed or if the construction of the object representation has failed. + * @throws InterruptedException If the thread has been interrupted. + * @throws TAPException If the TAP factory is unable to create the ADQL factory or the query checker. + */ + protected ADQLQuery parseADQL() throws ParseException, InterruptedException, TAPException{ + // Log the start of the parsing: + logger.logTAP(LogLevel.INFO, report, "PARSING", "Parsing ADQL: " + tapParams.getQuery().replaceAll("(\t|\r?\n)+", " "), null); + + // Create the ADQL parser: + ADQLParser parser = service.getFactory().createADQLParser(); + if (parser == null){ + logger.logTAP(LogLevel.WARNING, null, "PARSING", "No ADQL parser returned by the TAPFactory! The default implementation is used instead.", null); + parser = new ADQLParser(); + } - // Format the result: - if (formatter == null) - throw new TAPException("Impossible to format the query result: no formatter has been found for the given MIME type \"" + format + "\" and for the default MIME type \"votable\" (short form) !"); + // Set the ADQL factory: + if (parser.getQueryFactory() == null || parser.getQueryFactory().getClass() == ADQLQueryFactory.class) + parser.setQueryFactory(service.getFactory().createQueryFactory()); - return formatter; + // Set the query checker: + if (parser.getQueryChecker() == null) + parser.setQueryChecker(service.getFactory().createQueryChecker(uploadSchema)); + + // Parse the ADQL query: + ADQLQuery query = parser.parseQuery(tapParams.getQuery()); + + // Set or check the row limit: + final int limit = query.getSelect().getLimit(); + final Integer maxRec = tapParams.getMaxRec(); + if (maxRec != null && maxRec > -1){ + if (limit <= -1 || limit > maxRec) + query.getSelect().setLimit(maxRec + 1); + } + + return query; } - protected final void writeResult(R queryResult) throws InterruptedException, TAPException, UWSException{ - OutputFormat formatter = getFormatter(); + /** + *

Execute in "database" the given object representation of an ADQL query.

+ * + *

By default, this function is just calling {@link DBConnection#executeQuery(ADQLQuery)} and then it returns the value returned by this call.

+ * + *

Note: + * An INFO message is logged at the end of the query execution in order to report the result status (success or error) + * and the execution duration. + *

+ * + * @param adql The object representation of the ADQL query to execute. + * + * @return The result of the query, + * or NULL if the query execution has failed. + * + * @throws InterruptedException If the thread has been interrupted. + * @throws TAPException If the {@link DBConnection} has failed to deal with the given ADQL query. + * + * @see DBConnection#executeQuery(ADQLQuery) + */ + protected TableIterator executeADQL(final ADQLQuery adql) throws InterruptedException, TAPException{ + // Log the start of execution: + logger.logTAP(LogLevel.INFO, report, "START_DB_EXECUTION", "ADQL query: " + adql.toADQL().replaceAll("(\t|\r?\n)+", " "), null); + + // Set the fetch size, if any: + if (service.getFetchSize() != null && service.getFetchSize().length >= 1){ + if (report.synchronous && service.getFetchSize().length >= 2) + dbConn.setFetchSize(service.getFetchSize()[1]); + else + dbConn.setFetchSize(service.getFetchSize()[0]); + } + + // Execute the ADQL query: + TableIterator result = dbConn.executeQuery(adql); - // Synchronous case: + // Log the success or failure: + if (result == null) + logger.logTAP(LogLevel.INFO, report, "END_DB_EXECUTION", "Query execution aborted after " + (System.currentTimeMillis() - startStep) + "ms!", null); + else + logger.logTAP(LogLevel.INFO, report, "END_DB_EXECUTION", "Query successfully executed in " + (System.currentTimeMillis() - startStep) + "ms!", null); + + return result; + } + + /** + *

Write the given query result into the appropriate format in the appropriate output + * (HTTP response for a synchronous execution, otherwise a file or any output provided by UWS).

+ * + *

This function prepare the output in function of the execution type (synchronous or asynchronous). + * Once prepared, the result, the output and the formatter to use are given to {@link #writeResult(TableIterator, OutputFormat, OutputStream)} + * which will really process the result formatting and writing. + *

+ * + * @param queryResult The result of the query execution in database. + * + * @throws InterruptedException If the thread has been interrupted. + * @throws IOException If an error happens while writing the result in the {@link HttpServletResponse}. + * That kind of error can be thrown only in synchronous mode. + * In asynchronous, the error is stored as job error report and is never propagated. + * @throws TAPException If an error occurs while getting the appropriate formatter or while formatting or writing (synchronous execution) the result. + * @throws UWSException If an error occurs while getting the output stream or while writing (asynchronous execution) the result. + * + * @see #writeResult(TableIterator, OutputFormat, OutputStream) + */ + protected final void writeResult(final TableIterator queryResult) throws InterruptedException, IOException, TAPException, UWSException{ + // Log the start of the writing: + logger.logTAP(LogLevel.INFO, report, "WRITING_RESULT", "Writing the query result", null); + + // Get the appropriate result formatter: + OutputFormat formatter = getFormatter(); + + // CASE SYNCHRONOUS: if (response != null){ - long start = System.currentTimeMillis(); - try{ - response.setContentType(formatter.getMimeType()); - writeResult(queryResult, formatter, response.getOutputStream()); - }catch(IOException ioe){ - throw new TAPException("Impossible to get the output stream of the HTTP request to write the result of the job " + report.jobID + " !", ioe); - }finally{ - report.setDuration(ExecutionProgression.WRITING_RESULT, System.currentTimeMillis() - start); - } + long start = -1; + + // Set the HTTP content type to the MIME type of the result format: + response.setContentType(formatter.getMimeType()); + + // Set the character encoding: + response.setCharacterEncoding(UWSToolBox.DEFAULT_CHAR_ENCODING); + + // Write the formatted result in the HTTP response output: + start = System.currentTimeMillis(); + writeResult(queryResult, formatter, response.getOutputStream()); - }// Asynchronous case: + logger.logTAP(LogLevel.INFO, report, "RESULT_WRITTEN", "Result formatted (in " + formatter.getMimeType() + " ; " + (report.nbRows < 0 ? "?" : report.nbRows) + " rows ; " + ((report.resultingColumns == null) ? "?" : report.resultingColumns.length) + " columns) in " + ((start <= 0) ? "?" : (System.currentTimeMillis() - start)) + "ms!", null); + } + // CASE ASYNCHRONOUS: else{ - long start = System.currentTimeMillis(); + long start = -1, end = -1; try{ + // Create a UWS Result object to store the result + // (the result will be stored in a file and this object is the association between the job and the result file): JobThread jobThread = (JobThread)thread; Result result = jobThread.createResult(); + + // Set the MIME type of the result format in the result description: result.setMimeType(formatter.getMimeType()); + + // Write the formatted result in the file output: + start = System.currentTimeMillis(); writeResult(queryResult, formatter, jobThread.getResultOutput(result)); + end = System.currentTimeMillis(); + + // Set the size (in bytes) of the result in the result description: result.setSize(jobThread.getResultSize(result)); + + // Add the result description and link in the job description: jobThread.publishResult(result); + + logger.logTAP(LogLevel.INFO, report, "RESULT_WRITTEN", "Result formatted (in " + formatter.getMimeType() + " ; " + (report.nbRows < 0 ? "?" : report.nbRows) + " rows ; " + ((report.resultingColumns == null) ? "?" : report.resultingColumns.length) + " columns) in " + ((start <= 0 || end <= 0) ? "?" : (end - start)) + "ms!", null); + }catch(IOException ioe){ - throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, ioe, "Impossible to get the output stream of the result file to write the result of the job " + report.jobID + " !"); - }finally{ - report.setDuration(ExecutionProgression.WRITING_RESULT, System.currentTimeMillis() - start); + throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, ioe, "Impossible to write in the file into the result of the job " + report.jobID + " must be written!"); } } } - protected void writeResult(R queryResult, OutputFormat formatter, OutputStream output) throws InterruptedException, TAPException{ - //logger.info("Job "+report.jobID+" - 5/5 Writing result file..."); + /** + *

Format and write the given result in the given output with the given formatter.

+ * + *

By default, this function is just calling {@link OutputFormat#writeResult(TableIterator, OutputStream, TAPExecutionReport, Thread)}.

+ * + *

Note: + * {@link OutputFormat#writeResult(TableIterator, OutputStream, TAPExecutionReport, Thread)} is often testing the "interrupted" flag of the + * thread in order to stop as fast as possible if the user has cancelled the job or if the thread has been interrupted for another reason. + *

+ * + * @param queryResult Query result to format and to output. + * @param formatter The object able to write the result in the appropriate format. + * @param output The stream in which the result must be written. + * + * @throws InterruptedException If the thread has been interrupted. + * @throws IOException If there is an error while writing the result in the given stream. + * @throws TAPException If there is an error while formatting the result. + */ + protected void writeResult(TableIterator queryResult, OutputFormat formatter, OutputStream output) throws InterruptedException, IOException, TAPException{ formatter.writeResult(queryResult, output, report, thread); } + /** + *

Drop all tables uploaded by the user from the database.

+ * + *

Note: + * By default, if an error occurs while dropping a table from the database, the error will just be logged ; it won't be thrown/propagated. + *

+ * + * @throws TAPException If a grave error occurs. By default, no exception is thrown ; they are just logged. + */ protected void dropUploadedTables() throws TAPException{ if (uploadSchema != null){ // Drop all uploaded tables: - DBConnection dbConn = getDBConnection(); for(TAPTable t : uploadSchema){ try{ - dbConn.dropTable(t); + dbConn.dropUploadedTable(t); }catch(DBException dbe){ - logger.error("JOB " + report.jobID + "\tCan not drop the table \"" + t.getDBName() + "\" (in adql \"" + t.getADQLName() + "\") from the database !", dbe); + logger.logTAP(LogLevel.ERROR, report, "DROP_UPLOAD", "Can not drop the uploaded table \"" + t.getDBName() + "\" (in adql \"" + t.getADQLName() + "\") from the database!", dbe); } } - closeDBConnection(); } } diff --git a/src/tap/AbstractTAPFactory.java b/src/tap/AbstractTAPFactory.java index 91bad42..624b386 100644 --- a/src/tap/AbstractTAPFactory.java +++ b/src/tap/AbstractTAPFactory.java @@ -16,147 +16,304 @@ package tap; * You should have received a copy of the GNU Lesser General Public License * along with TAPLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.util.ArrayList; +import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import tap.db.DBConnection; +import tap.error.DefaultTAPErrorWriter; import tap.metadata.TAPMetadata; import tap.metadata.TAPSchema; import tap.metadata.TAPTable; import tap.parameters.TAPParameters; - import tap.upload.Uploader; - import uws.UWSException; - import uws.job.ErrorSummary; -import uws.job.JobThread; import uws.job.Result; -import uws.job.UWSJob; - -import uws.job.parameters.UWSParameters; import uws.job.user.JobOwner; - -import uws.service.AbstractUWSFactory; import uws.service.UWSService; import uws.service.backup.UWSBackupManager; +import uws.service.error.ServiceErrorWriter; import adql.db.DBChecker; -import adql.db.DBTable; - +import adql.parser.ADQLParser; import adql.parser.ADQLQueryFactory; +import adql.parser.ParseException; import adql.parser.QueryChecker; +import adql.query.ADQLQuery; -public abstract class AbstractTAPFactory< R > extends AbstractUWSFactory implements TAPFactory { +/** + * Default implementation of most of the {@link TAPFactory} function. + * Only the functions related with the database connection stay abstract. + * + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (04/2015) + */ +public abstract class AbstractTAPFactory extends TAPFactory { - protected final ServiceConnection service; + /** The error writer to use when any error occurs while executing a resource or to format an error occurring while executing an asynchronous job. */ + protected final ServiceErrorWriter errorWriter; - protected AbstractTAPFactory(ServiceConnection service) throws NullPointerException{ - if (service == null) - throw new NullPointerException("Can not create a TAPFactory without a ServiceConnection instance !"); + /** + * Build a basic TAPFactory. + * Nothing is done except setting the service connection. + * + * @param service Configuration of the TAP service. MUST NOT be NULL + * + * @throws NullPointerException If the given {@link ServiceConnection} is NULL. + * + * @see AbstractTAPFactory#AbstractTAPFactory(ServiceConnection, ServiceErrorWriter) + */ + protected AbstractTAPFactory(ServiceConnection service) throws NullPointerException{ + this(service, new DefaultTAPErrorWriter(service)); + } - this.service = service; + /** + *

Build a basic TAPFactory. + * Nothing is done except setting the service connection and the given error writer.

+ * + *

Then the error writer will be used when creating a UWS service and a job thread.

+ * + * @param service Configuration of the TAP service. MUST NOT be NULL + * @param errorWriter Object to use to format and write the errors for the user. + * + * @throws NullPointerException If the given {@link ServiceConnection} is NULL. + * + * @see TAPFactory#TAPFactory(ServiceConnection) + */ + protected AbstractTAPFactory(final ServiceConnection service, final ServiceErrorWriter errorWriter) throws NullPointerException{ + super(service); + this.errorWriter = errorWriter; } @Override - public UWSService createUWS() throws TAPException, UWSException{ - return new UWSService(this.service.getFactory(), this.service.getFileManager(), this.service.getLogger()); + public final ServiceErrorWriter getErrorWriter(){ + return errorWriter; } + /* *************** */ + /* ADQL MANAGEMENT */ + /* *************** */ + + /** + *

Note: + * Unless the standard implementation - {@link ADQLExecutor} - does not fit exactly your needs, + * it should not be necessary to extend this class and to extend this function (implemented here by default). + *

+ */ @Override - public UWSBackupManager createUWSBackupManager(final UWSService uws) throws TAPException, UWSException{ - return null; + public ADQLExecutor createADQLExecutor() throws TAPException{ + return new ADQLExecutor(service); } + /** + *

Note: + * This function should be extended if you want to customize the ADQL grammar. + *

+ */ @Override - public UWSJob createJob(HttpServletRequest request, JobOwner owner) throws UWSException{ - if (!service.isAvailable()) - throw new UWSException(HttpServletResponse.SC_SERVICE_UNAVAILABLE, service.getAvailability()); + public ADQLParser createADQLParser() throws TAPException{ + return new ADQLParser(); + } - try{ - TAPParameters tapParams = (TAPParameters)createUWSParameters(request); - return new TAPJob(owner, tapParams); - }catch(TAPException te){ - throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, te, "Can not create a TAP asynchronous job !"); - } + /** + *

Note: + * This function should be extended if you have customized the creation of any + * {@link ADQLQuery} part ; it could be the addition of one or several user defined function + * or the modification of any ADQL function or clause specific to your implementation. + *

+ */ + @Override + public ADQLQueryFactory createQueryFactory() throws TAPException{ + return new ADQLQueryFactory(); } + /** + *

This implementation gathers all tables published in this TAP service and those uploaded + * by the user. Then it calls {@link #createQueryChecker(Collection)} with this list in order + * to create a query checked. + *

+ * + *

Note: + * This function can not be overridded, but {@link #createQueryChecker(Collection)} can be. + *

+ */ @Override - public UWSJob createJob(String jobId, JobOwner owner, final UWSParameters params, long quote, long startTime, long endTime, List results, ErrorSummary error) throws UWSException{ - if (!service.isAvailable()) - throw new UWSException(HttpServletResponse.SC_SERVICE_UNAVAILABLE, service.getAvailability()); + public final QueryChecker createQueryChecker(final TAPSchema uploadSchema) throws TAPException{ + // Get all tables published in this TAP service: + TAPMetadata meta = service.getTAPMetadata(); + + // Build a list in order to gather all these with the uploaded ones: + ArrayList tables = new ArrayList(meta.getNbTables()); + + // Add all tables published in TAP: + Iterator it = meta.getTables(); + while(it.hasNext()) + tables.add(it.next()); + + // Add all tables uploaded by the user: + if (uploadSchema != null){ + for(TAPTable table : uploadSchema) + tables.add(table); + } + + // Finally, create the query checker: + return createQueryChecker(tables); + } + + /** + *

Create an object able to check the consistency between the ADQL query and the database. + * That's to say, it checks whether the tables and columns used in the query really exist + * in the database.

+ * + *

Note: + * This implementation just create a {@link DBChecker} instance with the list given in parameter. + *

+ * + * @param tables List of all available tables (and indirectly, columns). + * + * @return A new ADQL query checker. + * + * @throws TAPException If any error occurs while creating the query checker. + */ + protected QueryChecker createQueryChecker(final Collection tables) throws TAPException{ try{ - return new TAPJob(jobId, owner, (TAPParameters)params, quote, startTime, endTime, results, error); - }catch(TAPException te){ - throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, te, "Can not create a TAP asynchronous job !"); + return new DBChecker(tables, service.getUDFs(), service.getGeometries(), service.getCoordinateSystems()); + }catch(ParseException e){ + throw new TAPException("Unable to build a DBChecker instance! " + e.getMessage(), e, UWSException.INTERNAL_SERVER_ERROR); } } + /* ****** */ + /* UPLOAD */ + /* ****** */ + + /** + *

This implementation just create an {@link Uploader} instance with the given database connection.

+ * + *

Note: + * This function should be overrided if you need to change the DB name of the TAP_UPLOAD schema. + * Indeed, by overriding this function you can specify a given TAPSchema to use as TAP_UPLOAD schema + * in the constructor of {@link Uploader}. But do not forget that this {@link TAPSchema} instance MUST have + * an ADQL name equals to "TAP_UPLOAD", otherwise, a TAPException will be thrown. + *

+ */ @Override - public final JobThread createJobThread(final UWSJob job) throws UWSException{ + public Uploader createUploader(final DBConnection dbConn) throws TAPException{ + return new Uploader(service, dbConn); + } + + /* ************** */ + /* UWS MANAGEMENT */ + /* ************** */ + + /** + *

This implementation just create a {@link UWSService} instance.

+ * + *

Note: + * This implementation is largely enough for a TAP service. It is not recommended to override + * this function. + *

+ */ + @Override + public UWSService createUWS() throws TAPException{ try{ - return new AsyncThread((TAPJob)job, createADQLExecutor()); - }catch(TAPException te){ - throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, te, "Impossible to create an AsyncThread !"); + UWSService uws = new UWSService(this, this.service.getFileManager(), this.service.getLogger()); + uws.setName("TAP/async"); + uws.setErrorWriter(errorWriter); + return uws; + }catch(UWSException ue){ + throw new TAPException("Can not create a UWS service (asynchronous resource of TAP)!", ue, UWSException.INTERNAL_SERVER_ERROR); } } - public ADQLExecutor createADQLExecutor() throws TAPException{ - return new ADQLExecutor(service); + /** + *

This implementation does not provided a backup manager. + * It means that no asynchronous job will be restored and backuped.

+ * + *

You must override this function if you want enable the backup feature.

+ */ + @Override + public UWSBackupManager createUWSBackupManager(final UWSService uws) throws TAPException{ + return null; } /** - * Extracts the parameters from the given request (multipart or not). - * This function is used only to set UWS parameters, not to create a TAP query (for that, see {@link TAPParameters}). + *

This implementation provides a basic {@link TAPJob} instance.

* - * @see uws.service.AbstractUWSFactory#extractParameters(javax.servlet.http.HttpServletRequest, uws.service.UWS) + *

+ * If you need to add or modify the behavior of some functions of a {@link TAPJob}, + * you must override this function and return your own extension of {@link TAPJob}. + *

*/ @Override - public UWSParameters createUWSParameters(HttpServletRequest request) throws UWSException{ + protected TAPJob createTAPJob(final HttpServletRequest request, final JobOwner owner) throws UWSException{ try{ - return new TAPParameters(request, service, getExpectedAdditionalParameters(), getInputParamControllers()); + TAPParameters tapParams = createTAPParameters(request); + return new TAPJob(owner, tapParams); }catch(TAPException te){ - throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, te); + if (te.getCause() != null && te.getCause() instanceof UWSException) + throw (UWSException)te.getCause(); + else + throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, te, "Can not create a TAP asynchronous job!"); } } + /** + *

This implementation provides a basic {@link TAPJob} instance.

+ * + *

+ * If you need to add or modify the behavior of some functions of a {@link TAPJob}, + * you must override this function and return your own extension of {@link TAPJob}. + *

+ */ @Override - public UWSParameters createUWSParameters(Map params) throws UWSException{ + protected TAPJob createTAPJob(final String jobId, final JobOwner owner, final TAPParameters params, final long quote, final long startTime, final long endTime, final List results, final ErrorSummary error) throws UWSException{ try{ - return new TAPParameters(service, params, getExpectedAdditionalParameters(), getInputParamControllers()); + return new TAPJob(jobId, owner, params, quote, startTime, endTime, results, error); }catch(TAPException te){ - throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, te); + if (te.getCause() != null && te.getCause() instanceof UWSException) + throw (UWSException)te.getCause(); + else + throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, te, "Can not create a TAP asynchronous job !"); } } + /** + *

This implementation extracts standard TAP parameters from the given request.

+ * + *

+ * Non-standard TAP parameters are added in a map inside the returned {@link TAPParameters} object + * and are accessible with {@link TAPParameters#get(String)} and {@link TAPParameters#getAdditionalParameters()}. + * However, if you want to manage them in another way, you must extend {@link TAPParameters} and override + * this function in order to return an instance of your extension. + *

+ */ @Override - public ADQLQueryFactory createQueryFactory() throws TAPException{ - return new ADQLQueryFactory(); + public TAPParameters createTAPParameters(final HttpServletRequest request) throws TAPException{ + return new TAPParameters(request, service); } + /** + *

This implementation extracts standard TAP parameters from the given request.

+ * + *

+ * Non-standard TAP parameters are added in a map inside the returned {@link TAPParameters} object + * and are accessible with {@link TAPParameters#get(String)} and {@link TAPParameters#getAdditionalParameters()}. + * However, if you want to manage them in another way, you must extend {@link TAPParameters} and override + * this function in order to return an instance of your extension. + *

+ */ @Override - public QueryChecker createQueryChecker(TAPSchema uploadSchema) throws TAPException{ - TAPMetadata meta = service.getTAPMetadata(); - ArrayList tables = new ArrayList(meta.getNbTables()); - Iterator it = meta.getTables(); - while(it.hasNext()) - tables.add(it.next()); - if (uploadSchema != null){ - for(TAPTable table : uploadSchema) - tables.add(table); - } - return new DBChecker(tables); - } - - public Uploader createUploader(final DBConnection dbConn) throws TAPException{ - return new Uploader(service, dbConn); + public TAPParameters createTAPParameters(final Map params) throws TAPException{ + return new TAPParameters(service, params); } } diff --git a/src/tap/AsyncThread.java b/src/tap/AsyncThread.java index 4581c09..b97131a 100644 --- a/src/tap/AsyncThread.java +++ b/src/tap/AsyncThread.java @@ -16,35 +16,63 @@ package tap; * You should have received a copy of the GNU Lesser General Public License * along with TAPLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ -import adql.parser.ParseException; -import adql.translator.TranslationException; import uws.UWSException; - import uws.job.JobThread; +import uws.service.error.ServiceErrorWriter; -public class AsyncThread< R > extends JobThread { +/** + * Thread in charge of a TAP job execution. + * + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (02/2015) + */ +public class AsyncThread extends JobThread { - protected final ADQLExecutor executor; + /** The only object which knows how to execute an ADQL query. */ + protected final ADQLExecutor executor; - public AsyncThread(TAPJob j, ADQLExecutor executor) throws UWSException{ - super(j, "Execute the ADQL query of the TAP request " + j.getJobId()); + /** + * Build a TAP asynchronous job execution. + * + * @param j Description of the job to execute. + * @param executor The object to use for the ADQL execution itself. + * @param errorWriter The object to use to format and to write an execution error for the user. + * + * @throws NullPointerException If the job parameter or the {@link ADQLExecutor} is missing. + */ + public AsyncThread(final TAPJob j, final ADQLExecutor executor, final ServiceErrorWriter errorWriter) throws NullPointerException{ + super(j, "Execute the ADQL query of the TAP request " + j.getJobId(), errorWriter); + if (executor == null) + throw new NullPointerException("Missing ADQLExecutor! Can not create an instance of AsyncThread without."); this.executor = executor; } - @Override - public void interrupt(){ - if (isAlive()){ - try{ - executor.closeDBConnection(); - }catch(TAPException e){ - if (job != null && job.getLogger() != null) - job.getLogger().error("Can not close the DBConnection for the executing job \"" + job.getJobId() + "\" ! => the job will be probably not totally aborted.", e); - } + /** + *

Check whether this thread is able to start right now.

+ * + *

+ * Basically, this function asks to the {@link ADQLExecutor} to get a database connection. If no DB connection is available, + * then this thread can not start and this function return FALSE. In all the other cases, TRUE is returned. + *

+ * + *
+ * + * @return true if this thread can start right now, false otherwise. + * + * @since 2.0 + */ + public final boolean isReadyForExecution(){ + try{ + executor.initDBConnection(job.getJobId()); + return true; + }catch(TAPException te){ + return false; } - super.interrupt(); } @Override @@ -55,12 +83,6 @@ public class AsyncThread< R > extends JobThread { throw ie; }catch(UWSException ue){ throw ue; - }catch(TAPException te){ - throw new UWSException(te.getHttpErrorCode(), te, te.getMessage()); - }catch(ParseException pe){ - throw new UWSException(UWSException.BAD_REQUEST, pe, pe.getMessage()); - }catch(TranslationException te){ - throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, te, te.getMessage()); }catch(Exception ex){ throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, ex, "Error while processing the ADQL query of the job " + job.getJobId() + " !"); }finally{ @@ -68,6 +90,11 @@ public class AsyncThread< R > extends JobThread { } } + /** + * Get the description of the job that this thread is executing. + * + * @return The executed job. + */ public final TAPJob getTAPJob(){ return (TAPJob)job; } diff --git a/src/tap/ExecutionProgression.java b/src/tap/ExecutionProgression.java index 618d210..4086ccd 100644 --- a/src/tap/ExecutionProgression.java +++ b/src/tap/ExecutionProgression.java @@ -16,9 +16,16 @@ package tap; * You should have received a copy of the GNU Lesser General Public License * along with TAPLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ +/** + * Let describe the current status of a job execution. + * + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (07/2014) + */ public enum ExecutionProgression{ - PENDING, UPLOADING, PARSING, TRANSLATING, EXECUTING_SQL, WRITING_RESULT, FINISHED; + PENDING, UPLOADING, PARSING, EXECUTING_ADQL, WRITING_RESULT, FINISHED; } diff --git a/src/tap/ServiceConnection.java b/src/tap/ServiceConnection.java index 40e3745..dfa9473 100644 --- a/src/tap/ServiceConnection.java +++ b/src/tap/ServiceConnection.java @@ -16,66 +16,679 @@ package tap; * You should have received a copy of the GNU Lesser General Public License * along with TAPLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.util.Collection; import java.util.Iterator; -import tap.file.TAPFileManager; - +import tap.db.DBConnection; import tap.formatter.OutputFormat; - +import tap.log.DefaultTAPLog; import tap.log.TAPLog; - import tap.metadata.TAPMetadata; - import uws.service.UserIdentifier; +import uws.service.file.LocalUWSFileManager; +import uws.service.file.UWSFileManager; +import adql.db.FunctionDef; -public interface ServiceConnection< R > { +/** + *

Description and parameters list of a TAP service.

+ * + *

+ * Through this object, it is possible to configure the different limits and formats, + * but also to list all available tables and columns, to declare geometry features as all allowed user defined functions + * and to say where log and other kinds of files must be stored. + *

+ * + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (03/2015) + */ +public interface ServiceConnection { + /** + * List of possible limit units. + * + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (01/2015) + */ public static enum LimitUnit{ - rows, bytes; + rows("row"), bytes("byte"), kilobytes("kilobyte"), megabytes("megabyte"), gigabytes("gigabyte"); + + private final String str; + + private LimitUnit(final String str){ + this.str = str; + } + + /** + * Tells whether the given unit has the same type (bytes or rows). + * + * @param anotherUnit A unit. + * + * @return true if the given unit has the same type, false otherwise. + * + * @since 1.1 + */ + public boolean isCompatibleWith(final LimitUnit anotherUnit){ + if (this == rows) + return anotherUnit == rows; + else + return anotherUnit != rows; + } + + /** + * Gets the factor to convert into bytes the value expressed in this unit. + * Note: if this unit is not a factor of bytes, 1 is returned (so that the factor does not affect the value). + * + * @return The factor need to convert a value expressed in this unit into bytes, or 1 if not a bytes derived unit. + * + * @since 1.1 + */ + public long bytesFactor(){ + switch(this){ + case bytes: + return 1; + case kilobytes: + return 1000; + case megabytes: + return 1000000; + case gigabytes: + return 1000000000l; + default: + return 1; + } + } + + /** + * Compares the 2 given values (each one expressed in the given unit). + * Conversions are done internally in order to make a correct comparison between the 2 limits. + * + * @param leftLimit Value/Limit of the comparison left part. + * @param leftUnit Unit of the comparison left part value. + * @param rightLimit Value/Limit of the comparison right part. + * @param rightUnit Unit of the comparison right part value. + * + * @return the value 0 if x == y; a value less than 0 if x < y; and a value greater than 0 if x > y + * + * @throws TAPException If the two given units are not compatible. + * + * @see tap.ServiceConnection.LimitUnit#isCompatibleWith(tap.ServiceConnection.LimitUnit) + * @see #bytesFactor() + * @see Integer#compare(int, int) + * @see Long#compare(long, long) + * + * @since 1.1 + */ + public static int compare(final int leftLimit, final LimitUnit leftUnit, final int rightLimit, final LimitUnit rightUnit) throws TAPException{ + if (!leftUnit.isCompatibleWith(rightUnit)) + throw new TAPException("Limit units (" + leftUnit + " and " + rightUnit + ") are not compatible!"); + + if (leftUnit == rows || leftUnit == rightUnit) + return compare(leftLimit, rightLimit); + else + return compare(leftLimit * leftUnit.bytesFactor(), rightLimit * rightUnit.bytesFactor()); + } + + /** + *

(Strict copy of Integer.compare(int,int) of Java 1.7)

+ *

+ * Compares two {@code int} values numerically. + * The value returned is identical to what would be returned by: + *

+ *
+		 *    Integer.valueOf(x).compareTo(Integer.valueOf(y))
+		 * 
+ * + * @param x the first {@code int} to compare + * @param y the second {@code int} to compare + * @return the value {@code 0} if {@code x == y}; + * a value less than {@code 0} if {@code x < y}; and + * a value greater than {@code 0} if {@code x > y} + * + * @since 1.1 + */ + private static int compare(int x, int y){ + return (x < y) ? -1 : ((x == y) ? 0 : 1); + } + + /** + *

(Strict copy of Integer.compare(long,long) of Java 1.7)

+ *

+ * Compares two {@code long} values numerically. + * The value returned is identical to what would be returned by: + *

+ *
+		 *    Long.valueOf(x).compareTo(Long.valueOf(y))
+		 * 
+ * + * @param x the first {@code long} to compare + * @param y the second {@code long} to compare + * @return the value {@code 0} if {@code x == y}; + * a value less than {@code 0} if {@code x < y}; and + * a value greater than {@code 0} if {@code x > y} + * + * @since 1.1 + */ + public static int compare(long x, long y){ + return (x < y) ? -1 : ((x == y) ? 0 : 1); + } + + @Override + public String toString(){ + return str; + } } + /** + * [OPTIONAL] + *

Name of the service provider ; it can be an organization as an individual person.

+ * + *

There is no restriction on the syntax or on the label to use ; this information is totally free

+ * + *

It will be used as additional information (INFO tag) in any VOTable and HTML output.

+ * + * @return The TAP service provider or NULL to leave this field blank. + */ public String getProviderName(); + /** + * [OPTIONAL] + *

Description of the service provider.

+ * + *

It will be used as additional information (INFO tag) in any VOTable output.

+ * + * @return The TAP service description or NULL to leave this field blank. + */ public String getProviderDescription(); + /** + * [MANDATORY] + *

This function tells whether the TAP service is available + * (that's to say, "able to execute requests" ; resources like /availability, /capabilities and /tables may still work).

+ * + *

+ * A message explaining the current state of the TAP service could be provided thanks to {@link #getAvailability()}. + *

+ * + * @return true to enable all TAP resources, false to disable all of them (except /availability). + */ public boolean isAvailable(); + /** + * [OPTIONAL] + *

Get an explanation about the current TAP service state (working or not). + * This message aims to provide more details to the users about the availability of this service, + * or more particularly about its unavailability.

+ * + * @return Explanation about the TAP service state. + */ public String getAvailability(); + /** + * [MANDATORY] + *

This function sets the state of the whole TAP service. + * If true, all TAP resources will be able to execute resources. + * If false, /sync and /async won't answer any more to requests and a HTTP-503 (Service unavailable) + * error will be returned. + *

+ * + * @param isAvailable true to enable all resources, false to forbid /sync and /async (all other resources will still be available). + * @param message A message describing the current state of the service. If NULL, a default message may be set by the library. + * + * @since 2.0 + */ + public void setAvailable(final boolean isAvailable, final String message); + + /** + * [OPTIONAL] + *

Get the limit of the retention period (in seconds).

+ * + *

+ * It is the maximum period while an asynchronous job can leave in the jobs list + * and so can stay on the server. + *

+ * + *

Important notes:

+ *
    + *
  • Exactly 2 values or a NULL object is expected here.
  • + *
  • If NULL, the retention period is not limited and jobs will + * theoretically stay infinitely on the server.
  • + *
  • If not NULL, the 2 values must correspond to the default retention period + * and the maximum retention period.
  • + *
  • The default value is used to set the retention period when a job is created with no user defined retention period.
  • + *
  • The maximum value is used to limit the retention period when specified by the user while creating a job.
  • + *
  • The default value MUST be less or equals the maximum value.
  • + *
  • Both values must be positive. If a negative value is given it will be interpreted as "no limit".
  • + *
+ * + * @return NULL if no limit must be set, or a two-items array ([0]: default value, [1]: maximum value). + */ public int[] getRetentionPeriod(); + /** + * [OPTIONAL] + *

Get the limit of the job execution duration (in milliseconds).

+ * + *

+ * It is the duration of a running job (including the query execution). + * This duration is used for synchronous AND asynchronous jobs. + *

+ * + *

Important notes:

+ *
    + *
  • Exactly 2 values or a NULL object is expected here.
  • + *
  • If NULL, the execution duration is not limited and jobs could + * theoretically run infinitely.
  • + *
  • If not NULL, the 2 values must correspond to the default execution duration + * and the maximum execution duration.
  • + *
  • The default value is used to set the execution duration when a job is created with no user defined execution duration.
  • + *
  • The maximum value is used to limit the execution duration when specified by the user while creating a job.
  • + *
  • The default value MUST be less or equals the maximum value.
  • + *
  • Both values must be positive. If a negative value is given it will be interpreted as "no limit".
  • + *
+ * + * @return NULL if no limit must be set, or a two-items array ([0]: default value, [1]: maximum value). + */ public int[] getExecutionDuration(); + /** + * [OPTIONAL] + *

Get the limit of the job execution result.

+ * + *

+ * This value will limit the size of the query results, either in rows or in bytes. + * The type of limit is defined by the function {@link #getOutputLimitType()}. + *

+ * + *

Important notes:

+ *
    + *
  • Exactly 2 values or a NULL object is expected here.
  • + *
  • If NULL, the output limit is not limited and jobs could theoretically + * return very big files.
  • + *
  • If not NULL, the 2 values must correspond to the default output limit + * and the maximum output limit.
  • + *
  • The default value is used to set the output limit when a job is created with no user defined output limit.
  • + *
  • The maximum value is used to limit the output limit when specified by the user while creating a job.
  • + *
  • The structure of the object returned by this function MUST be the same as the object returned by {@link #getOutputLimitType()}. + * Particularly, the type given by the N-th item of {@link #getOutputLimitType()} must correspond to the N-th limit returned by this function.
  • + *
  • The default value MUST be less or equals the maximum value.
  • + *
  • Both values must be positive. If a negative value is given it will be interpreted as "no limit".
  • + *
+ * + *

Important note: + * Currently, the default implementations of the library is only able to deal with output limits in ROWS.
+ * Anyway, in order to save performances, it is strongly recommended to use ROWS limit rather than in bytes. Indeed, the rows limit can be taken + * into account at the effective execution of the query (so before getting the result), on the contrary of the bytes limit which + * will be applied on the query result. + *

+ * + * @return NULL if no limit must be set, or a two-items array ([0]: default value, [1]: maximum value). + * + * @see #getOutputLimitType() + */ public int[] getOutputLimit(); + /** + * [OPTIONAL] + *

Get the type of each output limit set by this service connection (and accessible with {@link #getOutputLimit()}).

+ * + *

Important notes:

+ *
    + *
  • Exactly 2 values or a NULL object is expected here.
  • + *
  • If NULL, the output limit will be considered as expressed in ROWS.
  • + *
  • The structure of the object returned by this function MUST be the same as the object returned by {@link #getOutputLimit()}. + * Particularly, the type given by the N-th item of this function must correspond to the N-th limit returned by {@link #getOutputLimit()}.
  • + *
+ * + *

Important note: + * Currently, the default implementations of the library is only able to deal with output limits in ROWS.
+ * Anyway, in order to save performances, it is strongly recommended to use ROWS limit rather than in bytes. Indeed, the rows limit can be taken + * into account at the effective execution of the query (so before getting the result), on the contrary of the bytes limit which + * will be applied on the query result. + *

+ * + * @return NULL if limits should be expressed in ROWS, or a two-items array ([0]: type of getOutputLimit()[0], [1]: type of getOutputLimit()[1]). + * + * @see #getOutputLimit() + */ public LimitUnit[] getOutputLimitType(); + /** + * [OPTIONAL] + *

Get the object to use in order to identify users at the origin of requests.

+ * + * @return NULL if no user identification should be done, a {@link UserIdentifier} instance otherwise. + */ public UserIdentifier getUserIdentifier(); + /** + * [MANDATORY] + *

This function let enable or disable the upload capability of this TAP service.

+ * + *

Note: + * If the upload is disabled, the request is aborted and an HTTP-400 error is thrown each time some tables are uploaded. + *

+ * + * @return true to enable the upload capability, false to disable it. + */ public boolean uploadEnabled(); + /** + * [OPTIONAL] + *

Get the maximum size of EACH uploaded table.

+ * + *

+ * This value is expressed either in rows or in bytes. + * The unit limit is defined by the function {@link #getUploadLimitType()}. + *

+ * + *

Important notes:

+ *
    + *
  • Exactly 2 values or a NULL object is expected here.
  • + *
  • If NULL, the upload limit is not limited and uploads could be + * theoretically unlimited.
  • + *
  • If not NULL, the 2 values must correspond to the default upload limit + * and the maximum upload limit.
  • + *
  • The default value is used inform the user about the server wishes.
  • + *
  • The maximum value is used to really limit the upload limit.
  • + *
  • The structure of the object returned by this function MUST be the same as the object returned by {@link #getUploadLimitType()}. + * Particularly, the type given by the N-th item of {@link #getUploadLimitType()} must correspond to the N-th limit returned by this function.
  • + *
  • The default value MUST be less or equals the maximum value.
  • + *
  • Both values must be positive. If a negative value is given it will be interpreted as "no limit".
  • + *
+ * + *

Important note: + * To save performances, it is recommended to use BYTES limit rather than in rows. Indeed, the bytes limit can be taken + * into account at directly when reading the bytes of the request, on the contrary of the rows limit which + * requires to parse the uploaded tables. + *

+ * + * @return NULL if no limit must be set, or a two-items array ([0]: default value, [1]: maximum value). + * + * @see #getUploadLimitType() + */ public int[] getUploadLimit(); + /** + * [OPTIONAL] + *

Get the type of each upload limit set by this service connection (and accessible with {@link #getUploadLimit()}).

+ * + *

Important notes:

+ *
    + *
  • Exactly 2 values or a NULL object is expected here.
  • + *
  • If NULL, the upload limit will be considered as expressed in ROWS.
  • + *
  • The structure of the object returned by this function MUST be the same as the object returned by {@link #getUploadLimit()}. + * Particularly, the type given by the N-th item of this function must correspond to the N-th limit returned by {@link #getUploadLimit()}.
  • + *
+ * + *

Important note: + * To save performances, it is recommended to use BYTES limit rather than in rows. Indeed, the bytes limit can be taken + * into account at directly when reading the bytes of the request, on the contrary of the rows limit which + * requires to parse the uploaded tables. + *

+ * + * @return NULL if limits should be expressed in ROWS, or a two-items array ([0]: type of getUploadLimit()[0], [1]: type of getUploadLimit()[1]). + * + * @see #getUploadLimit() + */ public LimitUnit[] getUploadLimitType(); + /** + * [OPTIONAL] + *

Get the maximum size of the whole set of all tables uploaded in one request. + * This size is expressed in bytes.

+ * + *

IMPORTANT 1: + * This value is always used when the upload capability is enabled. + *

+ * + *

IMPORTANT 2: + * The value returned by this function MUST always be positive. + * A zero or negative value will throw an exception later while + * reading parameters in a request with some uploaded tables. + *

+ * + * @return A positive (>0) value corresponding to the maximum number of bytes of all uploaded tables sent in one request. + */ public int getMaxUploadSize(); + /** + * [MANDATORY] + *

Get the list of all available tables and columns.

+ * + *

+ * This object is really important since it lets the library check ADQL queries properly and set the good type + * and formatting in the query results. + *

+ * + * @return A TAPMetadata object. NULL is not allowed and will throw a grave error at the service initialization. + */ public TAPMetadata getTAPMetadata(); + /** + * [OPTIONAL] + *

Get the list of all allowed coordinate systems.

+ * + * Special values + * + *

Two special values can be returned by this function:

+ *
    + *
  • NULL which means that all coordinate systems are allowed,
  • + *
  • the empty list which means that no coordinate system - except + * the default one (which can be reduced to an empty string) - is allowed.
  • + *
+ * + * List item syntax + * + *

+ * Each item of this list is a pattern and not a simple coordinate system. + * Thus each item MUST respect the following syntax: + *

+ *
{framePattern} {refposPattern} {flavorPattern}
+ *

+ * Contrary to a coordinate system expression, all these 3 information are required. + * Each may take 3 kinds of value: + *

+ *
    + *
  • a single value (i.e. "ICRS"),
  • + *
  • a list of values with the syntax ({value1}|{value2}|...) (i.e. "(ICRS|FK4)"),
  • + *
  • a "*" which means that all values are possible. + *
+ *

+ * For instance: (ICRS|FK4) HELIOCENTER * is a good syntax, + * but not ICRS or ICRS HELIOCENTER. + *

+ * + *

Note: + * Even if not explicitly part of the possible values, the default value of each part (i.e. UNKNOWNFRAME for frame) is always taken into account by the library. + * Particularly, the empty string will always be allowed even if not explicitly listed in the list returned by this function. + *

+ * + * @return NULL to allow ALL coordinate systems, an empty list to allow NO coordinate system, + * or a list of coordinate system patterns otherwise. + */ public Collection getCoordinateSystems(); + /** + * [OPTIONAL] + *

Get the list of all allowed geometrical functions.

+ * + * Special values + * + *

Two special values can be returned by this function:

+ *
    + *
  • NULL which means that all geometrical functions are allowed,
  • + *
  • the empty list which means that no geometrical functions is allowed.
  • + *
+ * + * List item syntax + * + *

+ * Each item of the returned list MUST be a function name (i.e. "CONTAINS", "POINT"). + * It can also be a type of STC region to forbid (i.e. "POSITION", "UNION"). + *

+ * + *

The given names are not case sensitive.

+ * + * @return NULL to allow ALL geometrical functions, an empty list to allow NO geometrical function, + * or a list of geometrical function names otherwise. + * + * @since 2.0 + */ + public Collection getGeometries(); + + /** + * [OPTIONAL] + *

Get the list of all allowed User Defined Functions (UDFs).

+ * + * Special values + * + *

Two special values can be returned by this function:

+ *
    + *
  • NULL which means that all unknown functions (which should be UDFs) are allowed,
  • + *
  • the empty list which means that no unknown functions (which should be UDFs) is allowed.
  • + *
+ * + * List item syntax + * + *

+ * Each item of the returned list MUST be an instance of {@link FunctionDef}. + *

+ * + * @return NULL to allow ALL unknown functions, an empty list to allow NO unknown function, + * or a list of user defined functions otherwise. + * + * @since 2.0 + */ + public Collection getUDFs(); + + /** + * [OPTIONAL] + * + *

Get the maximum number of asynchronous jobs that can run in the same time.

+ * + *

A null or negative value means no limit on the number of running asynchronous jobs.

+ * + * @return Maximum number of running jobs (≤0 => no limit). + * + * @since 2.0 + */ + public int getNbMaxAsyncJobs(); + + /** + * [MANDATORY] + *

Get the logger to use in the whole service when any error, warning or info happens.

+ * + *

IMPORTANT: + * If NULL is returned by this function, grave errors will occur while executing a query or managing an error. + * It is strongly recommended to provide a logger, even a basic implementation. + *

+ * + *

Piece of advice: + * A default implementation like {@link DefaultTAPLog} would be most of time largely enough. + *

+ * + * @return An instance of {@link TAPLog}. + */ public TAPLog getLogger(); - public TAPFactory getFactory(); + /** + * [MANDATORY] + *

Get the object able to build other objects essentials to configure the TAP service or to run every queries.

+ * + *

IMPORTANT: + * If NULL is returned by this function, grave errors will occur while initializing the service. + *

+ * + *

Piece of advice: + * The {@link TAPFactory} is an interface which contains a lot of functions to implement. + * It is rather recommended to extend {@link AbstractTAPFactory}: just 2 functions + * ({@link AbstractTAPFactory#freeConnection(DBConnection)} and {@link AbstractTAPFactory#getConnection(String)}) + * will have to be implemented. + *

+ * + * @return An instance of {@link TAPFactory}. + * + * @see AbstractTAPFactory + */ + public TAPFactory getFactory(); + + /** + * [MANDATORY] + *

Get the object in charge of the files management. + * This object manages log, error, result and backup files of the whole service.

+ * + *

IMPORTANT: + * If NULL is returned by this function, grave errors will occur while initializing the service. + *

+ * + *

Piece of advice: + * The library provides a default implementation of the interface {@link UWSFileManager}: + * {@link LocalUWSFileManager}, which stores all files on the local file-system. + *

+ * + * @return An instance of {@link UWSFileManager}. + */ + public UWSFileManager getFileManager(); - public TAPFileManager getFileManager(); + /** + * [MANDATORY] + *

Get the list of all available output formats.

+ * + *

IMPORTANT:

+ *
    + *
  • All formats of this list MUST have a different MIME type.
  • + *
  • At least one item must correspond to the MIME type "votable".
  • + *
  • If NULL is returned by this function, grave errors will occur while writing the capabilities of this service.
  • + * + * + * @return An iterator on the list of all available output formats. + */ + public Iterator getOutputFormats(); - public Iterator> getOutputFormats(); + /** + * [MANDATORY] + *

    Get the output format having the given MIME type (or short MIME type ~ alias).

    + * + *

    IMPORTANT: + * This function MUST always return an {@link OutputFormat} instance when the MIME type "votable" is given in parameter. + *

    + * + * @param mimeOrAlias MIME type or short MIME type of the format to get. + * + * @return The corresponding {@link OutputFormat} or NULL if not found. + */ + public OutputFormat getOutputFormat(final String mimeOrAlias); - public OutputFormat getOutputFormat(final String mimeOrAlias); + /** + * [OPTIONAL] + *

    Get the size of result blocks to fetch from the database.

    + * + *

    + * Rather than fetching a query result in a whole, it may be possible to specify to the database + * that results may be retrieved by blocks whose the size can be specified by this function. + * If supported by the DBMS and the JDBC driver, this feature may help sparing memory and avoid + * too much waiting time from the TAP /sync users (and thus, avoiding some HTTP client timeouts). + *

    + * + *

    Note: + * Generally, this feature is well supported by DBMS. But for that, the used JDBC driver must use + * the V3 protocol. If anyway, this feature is supported neither by the DBMS, the JDBC driver nor your + * {@link DBConnection}, no error will be thrown if a value is returned by this function: it will be silently + * ignored by the library. + *

    + * + * @return null or an array of 1 or 2 integers. + * If null (or empty array), no attempt to set fetch size will be done and so, ONLY the default + * value of the {@link DBConnection} will be used. + * [0]=fetchSize for async queries, [1]=fetchSize for sync queries. + * If [1] is omitted, it will be considered as equals to [0]. + * If a fetchSize is negative or null, the default value of your JDBC driver will be used. + * + * @since 2.0 + */ + public int[] getFetchSize(); } diff --git a/src/tap/TAPException.java b/src/tap/TAPException.java index 89a4fee..edfe745 100644 --- a/src/tap/TAPException.java +++ b/src/tap/TAPException.java @@ -16,129 +16,373 @@ package tap; * You should have received a copy of the GNU Lesser General Public License * along with TAPLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import uws.UWSException; +/** + *

    Any exception that occurred while a TAP service activity.

    + * + *

    Most of the time this exception wraps another exception (e.g. {@link UWSException}).

    + * + *

    It contains an HTTP status code, set by default to HTTP-500 (Internal Server Error).

    + * + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (04/2015) + */ public class TAPException extends Exception { private static final long serialVersionUID = 1L; + /** An ADQL query which were executed when the error occurred. */ private String adqlQuery = null; + + /** The ADQL query execution status (e.g. uploading, parsing, executing) just when the error occurred. */ private ExecutionProgression executionStatus = null; + /** The HTTP status code to set in the HTTP servlet response if the exception reaches the servlet. */ private int httpErrorCode = UWSException.INTERNAL_SERVER_ERROR; + /** + * Standard TAP exception: no ADQL query or execution status specified. + * The corresponding HTTP status code will be HTTP-500 (Internal Server Error). + * + * @param message Message explaining the error. + */ public TAPException(String message){ super(message); } + /** + * Standard TAP exception: no ADQL query or execution status specified. + * The corresponding HTTP status code is set by the second parameter. + * + * @param message Message explaining the error. + * @param httpErrorCode HTTP response status code. (if ≤ 0, 500 will be set by default) + */ public TAPException(String message, int httpErrorCode){ super(message); this.httpErrorCode = httpErrorCode; } + /** + * TAP exception with the ADQL query which were executed when the error occurred. + * No execution status specified. + * The corresponding HTTP status code will be HTTP-500 (Internal Server Error). + * + * @param message Message explaining the error. + * @param query The ADQL query which were executed when the error occurred. + */ public TAPException(String message, String query){ super(message); adqlQuery = query; } + /** + * TAP exception with the ADQL query which were executed when the error occurred. + * No execution status specified. + * The corresponding HTTP status code is set by the second parameter. + * + * @param message Message explaining the error. + * @param httpErrorCode HTTP response status code. (if ≤ 0, 500 will be set by default) + * @param query The ADQL query which were executed when the error occurred. + */ public TAPException(String message, int httpErrorCode, String query){ this(message, httpErrorCode); adqlQuery = query; } + /** + * TAP exception with the ADQL query which were executed when the error occurred, + * AND with its execution status (e.g. uploading, parsing, executing, ...). + * The corresponding HTTP status code will be HTTP-500 (Internal Server Error). + * + * @param message Message explaining the error. + * @param query The ADQL query which were executed when the error occurred. + * @param status Execution status/phase of the given ADQL query when the error occurred. + */ public TAPException(String message, String query, ExecutionProgression status){ this(message, query); executionStatus = status; } + /** + * TAP exception with the ADQL query which were executed when the error occurred, + * AND with its execution status (e.g. uploading, parsing, executing, ...). + * The corresponding HTTP status code is set by the second parameter. + * + * @param message Message explaining the error. + * @param httpErrorCode HTTP response status code. (if ≤ 0, 500 will be set by default) + * @param query The ADQL query which were executed when the error occurred. + * @param status Execution status/phase of the given ADQL query when the error occurred. + */ public TAPException(String message, int httpErrorCode, String query, ExecutionProgression status){ this(message, httpErrorCode, query); executionStatus = status; } + /** + *

    TAP exception wrapping the given {@link UWSException}.

    + * + *

    The message of this TAP exception will be exactly the same as the one of the given exception.

    + * + *

    + * Besides, the cause of this TAP exception will be the cause of the given exception ONLY if it has one ; + * otherwise it will the given exception. + *

    + * + *

    The HTTP status code will be the same as the one of the given {@link UWSException}.

    + * + * @param ue The exception to wrap. + */ public TAPException(UWSException ue){ - this(ue.getMessage(), ue.getCause(), ue.getHttpErrorCode()); + this(ue.getMessage(), (ue.getCause() == null ? ue : ue.getCause()), ue.getHttpErrorCode()); } + /** + *

    TAP exception wrapping the given {@link UWSException}.

    + * + *

    The message of this TAP exception will be exactly the same as the one of the given exception.

    + * + *

    + * Besides, the cause of this TAP exception will be the cause of the given exception ONLY if it has one ; + * otherwise it will the given exception. + *

    + * + *

    The HTTP status code will be the one given in second parameter.

    + * + * @param cause The exception to wrap. + * @param httpErrorCode HTTP response status code. (if ≤ 0, 500 will be set by default) + */ public TAPException(UWSException cause, int httpErrorCode){ this(cause); this.httpErrorCode = httpErrorCode; } + /** + *

    TAP exception wrapping the given {@link UWSException} and storing the current ADQL query execution status.

    + * + *

    The message of this TAP exception will be exactly the same as the one of the given exception.

    + * + *

    + * Besides, the cause of this TAP exception will be the cause of the given exception ONLY if it has one ; + * otherwise it will the given exception. + *

    + * + *

    The HTTP status code will be the one given in second parameter.

    + * + * @param cause The exception to wrap. + * @param httpErrorCode HTTP response status code. (if ≤ 0, 500 will be set by default) + * @param status Execution status/phase of the given ADQL query when the error occurred. + */ public TAPException(UWSException cause, int httpErrorCode, ExecutionProgression status){ this(cause, httpErrorCode); this.executionStatus = status; } + /** + * Build a {@link TAPException} with the given cause. The built exception will have NO MESSAGE. + * No execution status specified. + * The corresponding HTTP status code will be HTTP-500 (Internal Server Error). + * + * @param cause The cause of this exception. + */ public TAPException(Throwable cause){ super(cause); } + /** + * Build a {@link TAPException} with the given cause. The built exception will have NO MESSAGE. + * No execution status specified. + * The corresponding HTTP status code is set by the second parameter. + * + * @param cause The cause of this exception. + * @param httpErrorCode HTTP response status code. (if ≤ 0, 500 will be set by default) + */ public TAPException(Throwable cause, int httpErrorCode){ super(cause); this.httpErrorCode = httpErrorCode; } + /** + * Build a {@link TAPException} with the given cause AND with the ADQL query which were executed when the error occurred. + * The built exception will have NO MESSAGE. + * No execution status specified. + * The corresponding HTTP status code will be HTTP-500 (Internal Server Error). + * + * @param cause The cause of this exception. + * @param query The ADQL query which were executed when the error occurred. + */ public TAPException(Throwable cause, String query){ super(cause); adqlQuery = query; } + /** + * Build a {@link TAPException} with the given cause AND with the ADQL query which were executed when the error occurred. + * The built exception will have NO MESSAGE. + * No execution status specified. + * The corresponding HTTP status code is set by the second parameter. + * + * @param cause The cause of this exception. + * @param httpErrorCode HTTP response status code. (if ≤ 0, 500 will be set by default) + * @param query The ADQL query which were executed when the error occurred. + */ public TAPException(Throwable cause, int httpErrorCode, String query){ this(cause, httpErrorCode); adqlQuery = query; } + /** + * Build a {@link TAPException} with the given cause AND with the ADQL query which were executed when the error occurred + * AND with its execution status (e.g. uploading, parsing, executing, ...). + * The built exception will have NO MESSAGE. + * The corresponding HTTP status code will be HTTP-500 (Internal Server Error). + * + * @param cause The cause of this exception. + * @param query The ADQL query which were executed when the error occurred. + * @param status Execution status/phase of the given ADQL query when the error occurred. + */ public TAPException(Throwable cause, String query, ExecutionProgression status){ this(cause, query); executionStatus = status; } + /** + * Build a {@link TAPException} with the given cause AND with the ADQL query which were executed when the error occurred + * AND with its execution status (e.g. uploading, parsing, executing, ...). + * The built exception will have NO MESSAGE. + * The corresponding HTTP status code is set by the second parameter. + * + * @param cause The cause of this exception. + * @param httpErrorCode HTTP response status code. (if ≤ 0, 500 will be set by default) + * @param query The ADQL query which were executed when the error occurred. + * @param status Execution status/phase of the given ADQL query when the error occurred. + */ public TAPException(Throwable cause, int httpErrorCode, String query, ExecutionProgression status){ this(cause, httpErrorCode, query); executionStatus = status; } + /** + * Build a {@link TAPException} with the given message and cause. + * No execution status specified. + * The corresponding HTTP status code will be HTTP-500 (Internal Server Error). + * + * @param message Message of this exception. + * @param cause The cause of this exception. + */ public TAPException(String message, Throwable cause){ super(message, cause); } + /** + * Build a {@link TAPException} with the given message and cause. + * No execution status specified. + * The corresponding HTTP status code is set by the third parameter. + * + * @param message Message of this exception. + * @param cause The cause of this exception. + * @param httpErrorCode HTTP response status code. (if ≤ 0, 500 will be set by default) + */ public TAPException(String message, Throwable cause, int httpErrorCode){ super(message, cause); this.httpErrorCode = httpErrorCode; } + /** + * Build a {@link TAPException} with the given message and cause, + * AND with the ADQL query which were executed when the error occurred. + * No execution status specified. + * The corresponding HTTP status code will be HTTP-500 (Internal Server Error). + * + * @param message Message of this exception. + * @param cause The cause of this exception. + * @param query The ADQL query which were executed when the error occurred. + */ public TAPException(String message, Throwable cause, String query){ super(message, cause); adqlQuery = query; } + /** + * Build a {@link TAPException} with the given message and cause, + * AND with the ADQL query which were executed when the error occurred. + * No execution status specified. + * The corresponding HTTP status code is set by the third parameter. + * + * @param message Message of this exception. + * @param cause The cause of this exception. + * @param httpErrorCode HTTP response status code. (if ≤ 0, 500 will be set by default) + * @param query The ADQL query which were executed when the error occurred. + */ public TAPException(String message, Throwable cause, int httpErrorCode, String query){ this(message, cause, httpErrorCode); adqlQuery = query; } + /** + * Build a {@link TAPException} with the given message and cause, + * AND with the ADQL query which were executed when the error occurred + * AND with its execution status (e.g. uploading, parsing, executing, ...). + * No execution status specified. + * The corresponding HTTP status code will be HTTP-500 (Internal Server Error). + * + * @param message Message of this exception. + * @param cause The cause of this exception. + * @param query The ADQL query which were executed when the error occurred. + * @param status Execution status/phase of the given ADQL query when the error occurred. + */ public TAPException(String message, Throwable cause, String query, ExecutionProgression status){ this(message, cause, query); executionStatus = status; } + /** + * Build a {@link TAPException} with the given message and cause, + * AND with the ADQL query which were executed when the error occurred + * AND with its execution status (e.g. uploading, parsing, executing, ...). + * No execution status specified. + * The corresponding HTTP status code is set by the third parameter. + * + * @param message Message of this exception. + * @param cause The cause of this exception. + * @param httpErrorCode HTTP response status code. (if ≤ 0, 500 will be set by default) + * @param query The ADQL query which were executed when the error occurred. + * @param status Execution status/phase of the given ADQL query when the error occurred. + */ public TAPException(String message, Throwable cause, int httpErrorCode, String query, ExecutionProgression status){ this(message, cause, httpErrorCode, query); executionStatus = status; } + /** + *

    Get the HTTP status code to set in the HTTP response.

    + * + *

    If the set value is ≤ 0, 500 will be returned instead.

    + * + * @return The HTTP response status code. + */ public int getHttpErrorCode(){ - return httpErrorCode; + return (httpErrorCode <= 0) ? UWSException.INTERNAL_SERVER_ERROR : httpErrorCode; } + /** + * Get the ADQL query which were executed when the error occurred. + * + * @return Executed ADQL query. + */ public String getQuery(){ return adqlQuery; } + /** + * Get the execution status/phase of an ADQL query when the error occurred. + * + * @return ADQL query execution status. + */ public ExecutionProgression getExecutionStatus(){ return executionStatus; } diff --git a/src/tap/TAPExecutionReport.java b/src/tap/TAPExecutionReport.java index bf95af1..fe2733f 100644 --- a/src/tap/TAPExecutionReport.java +++ b/src/tap/TAPExecutionReport.java @@ -16,90 +16,167 @@ package tap; * You should have received a copy of the GNU Lesser General Public License * along with TAPLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ -import adql.db.DBColumn; - import tap.parameters.TAPParameters; +import adql.db.DBColumn; +/** + *

    Report the execution (including the parsing and the output writing) of an ADQL query. + * It gives information on the job parameters, the job ID, whether it is a synchronous task or not, times of each execution step (uploading, parsing, executing and writing), + * the resulting columns and the success or not of the execution.

    + * + *

    This report is completely filled by {@link ADQLExecutor}, and aims to be used/read only at the end of the job or when it is definitely finished.

    + * + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (04/2015) + */ public class TAPExecutionReport { + /** ID of the job whose the execution is reported here. */ public final String jobID; + + /** Indicate whether this execution is done in a synchronous or asynchronous job. */ public final boolean synchronous; + + /** List of all parameters provided in the user request. */ public final TAPParameters parameters; - public String sqlTranslation = null; + /** List of all resulting columns. Empty array, if not yet known. */ public DBColumn[] resultingColumns = new DBColumn[0]; - protected final long[] durations = new long[]{-1,-1,-1,-1,-1}; + /** Total number of written rows. + * @since 2.0 */ + public long nbRows = -1; + + /** Duration of all execution steps. For the moment only 4 steps (in the order): uploading, parsing, executing and writing. */ + protected final long[] durations = new long[]{-1,-1,-1,-1}; + + /** Total duration of the job execution. */ protected long totalDuration = -1; + /** Indicate whether this job has ended successfully or not. At the beginning or while executing, this field is always FALSE. */ public boolean success = false; + /** + * Build an empty execution report. + * + * @param jobID ID of the job whose the execution must be described here. + * @param synchronous true if the job is synchronous, false otherwise. + * @param params List of all parameters provided by the user for the execution. + */ public TAPExecutionReport(final String jobID, final boolean synchronous, final TAPParameters params){ this.jobID = jobID; this.synchronous = synchronous; parameters = params; } + /** + *

    Map the execution progression with an index inside the {@link #durations} array.

    + * + *

    Warning: for the moment, only {@link ExecutionProgression#UPLOADING}, {@link ExecutionProgression#PARSING}, + * {@link ExecutionProgression#EXECUTING_ADQL} and {@link ExecutionProgression#WRITING_RESULT} are managed.

    + * + * @param tapProgression Execution progression. + * + * @return Index in the array {@link #durations}, or -1 if the given execution progression is not managed. + */ protected int getIndexDuration(final ExecutionProgression tapProgression){ switch(tapProgression){ case UPLOADING: return 0; case PARSING: return 1; - case TRANSLATING: + case EXECUTING_ADQL: return 2; - case EXECUTING_SQL: - return 3; case WRITING_RESULT: - return 4; + return 3; default: return -1; } } - public final long getDuration(final ExecutionProgression tapProgression){ - int indDuration = getIndexDuration(tapProgression); + /** + * Get the duration corresponding to the given job execution step. + * + * @param tapStep Job execution step. + * + * @return The corresponding duration (in ms), or -1 if this step has not been (yet) processed. + * + * @see #getIndexDuration(ExecutionProgression) + */ + public final long getDuration(final ExecutionProgression tapStep){ + int indDuration = getIndexDuration(tapStep); if (indDuration < 0 || indDuration >= durations.length) return -1; else return durations[indDuration]; } - public final void setDuration(final ExecutionProgression tapProgression, final long duration){ - int indDuration = getIndexDuration(tapProgression); + /** + * Set the duration corresponding to the given execution step. + * + * @param tapStep Job execution step. + * @param duration Duration (in ms) of the given execution step. + */ + public final void setDuration(final ExecutionProgression tapStep, final long duration){ + int indDuration = getIndexDuration(tapStep); if (indDuration < 0 || indDuration >= durations.length) return; else durations[indDuration] = duration; } + /** + * Get the execution of the UPLOAD step. + * @return Duration (in ms). + * @see #getDuration(ExecutionProgression) + */ public final long getUploadDuration(){ return getDuration(ExecutionProgression.UPLOADING); } + /** + * Get the execution of the PARSE step. + * @return Duration (in ms). + * @see #getDuration(ExecutionProgression) + */ public final long getParsingDuration(){ return getDuration(ExecutionProgression.PARSING); } - public final long getTranslationDuration(){ - return getDuration(ExecutionProgression.TRANSLATING); - } - + /** + * Get the execution of the EXECUTION step. + * @return Duration (in ms). + * @see #getDuration(ExecutionProgression) + */ public final long getExecutionDuration(){ - return getDuration(ExecutionProgression.EXECUTING_SQL); + return getDuration(ExecutionProgression.EXECUTING_ADQL); } + /** + * Get the execution of the FORMAT step. + * @return Duration (in ms). + * @see #getDuration(ExecutionProgression) + */ public final long getFormattingDuration(){ return getDuration(ExecutionProgression.WRITING_RESULT); } + /** + * Get the total duration of the job execution. + * @return Duration (in ms). + */ public final long getTotalDuration(){ return totalDuration; } + /** + * Set the total duration of the job execution. + * @param duration Duration (in ms) to set. + */ public final void setTotalDuration(final long duration){ totalDuration = duration; } diff --git a/src/tap/TAPFactory.java b/src/tap/TAPFactory.java index 785be47..f4f6b7a 100644 --- a/src/tap/TAPFactory.java +++ b/src/tap/TAPFactory.java @@ -16,43 +16,469 @@ package tap; * You should have received a copy of the GNU Lesser General Public License * along with TAPLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ -import tap.db.DBConnection; +import java.util.List; +import java.util.Map; -import tap.metadata.TAPSchema; +import javax.servlet.http.HttpServletRequest; +import tap.db.DBConnection; +import tap.metadata.TAPSchema; +import tap.parameters.TAPParameters; import tap.upload.Uploader; - import uws.UWSException; - +import uws.job.ErrorSummary; +import uws.job.JobThread; +import uws.job.Result; +import uws.job.UWSJob; +import uws.job.parameters.UWSParameters; +import uws.job.user.JobOwner; import uws.service.UWSFactory; import uws.service.UWSService; - import uws.service.backup.UWSBackupManager; - +import uws.service.error.ServiceErrorWriter; +import uws.service.file.UWSFileManager; +import uws.service.request.RequestParser; +import adql.parser.ADQLParser; import adql.parser.ADQLQueryFactory; import adql.parser.QueryChecker; +import adql.query.ADQLQuery; + +/** + *

    Let build essential objects of the TAP service.

    + * + *

    Basically, it means answering to the following questions:

    + *
      + *
    • how to connect to the database? ({@link DBConnection})
    • + *
    • which UWS implementation (default implementation provided by default) to use? ({@link UWSService})
    • + *
    • whether and how UWS/asynchronous jobs must be backuped and restored? ({@link UWSBackupManager})
    • + *
    • how to create asynchronous jobs? ({@link TAPJob})
    • + *
    • whether and how tables must be updated? ({@link Uploader})
    • + *
    • how to execute an ADQL query? ({@link ADQLExecutor}) + *
    • how to parser an ADQL query? ({@link ADQLParser})
    • + *
    • how to check ADQL queries? ({@link QueryChecker})
    • + *
    + * + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (04/2015) + */ +public abstract class TAPFactory implements UWSFactory { + + /** Connection to the TAP service ; it provides all important service configuration information. */ + protected final ServiceConnection service; + + /** + * Build a basic {@link TAPFactory}. + * Nothing is done except setting the service connection. + * + * @param service Configuration of the TAP service. MUST NOT be NULL + * + * @throws NullPointerException If the given {@link ServiceConnection} is NULL. + */ + protected TAPFactory(final ServiceConnection service) throws NullPointerException{ + if (service == null) + throw new NullPointerException("Can not create a TAPFactory without a ServiceConnection instance !"); + + this.service = service; + } + + /** + *

    Get the object to use when an error must be formatted and written to the user.

    + * + *

    This formatted error will be either written in an HTTP response or in a job error summary.

    + * + * @return The error writer to use. + * + * @since 2.0 + */ + public abstract ServiceErrorWriter getErrorWriter(); + + /* ******************* */ + /* DATABASE CONNECTION */ + /* ******************* */ + + /** + *

    Get a free database connection.

    + * + *

    + * Free means this connection is not currently in use and will be exclusively dedicated to the function/process/thread + * which has asked for it by calling this function. + *

    + * + *

    Note: + * This function can create on the fly a new connection OR get a free one from a connection pool. Considering the + * creation time of a database connection, the second way is recommended. + *

    + * + *

    IMPORTANT: + * The returned connection MUST be freed after having used it. + *

    + * + *

    WARNING: + * Some implementation may free the connection automatically when not used for a specific time. + * So, do not forget to free the connection after use! + *

    + * + * @param jobID ID of the job/thread/process which has asked for this connection. note: The returned connection must then be identified thanks to this ID. + * + * @return A new and free connection to the database. MUST BE NOT NULL, or otherwise a TAPException should be returned. + * + * @throws TAPException If there is any error while getting a free connection. + * + * @since 2.0 + */ + public abstract DBConnection getConnection(final String jobID) throws TAPException; + + /** + *

    Free the given connection.

    + * + *

    + * This function is called by the TAP library when a job/thread does not need this connection any more. It aims + * to free resources associated to the given database connection. + *

    + * + *

    Note: + * This function can just close definitely the connection OR give it back to a connection pool. The implementation is + * here totally free! + *

    + * + * @param conn The connection to close. + * + * @since 2.0 + */ + public abstract void freeConnection(final DBConnection conn); + + /** + *

    Destroy all resources (and particularly DB connections and JDBC driver) allocated in this factory.

    + * + *

    Note: + * This function is called when the TAP service is shutting down. + * After this call, the factory may not be able to provide any closed resources ; its behavior may be unpredictable. + *

    + * + * @since 2.0 + */ + public abstract void destroy(); + + /* *************** */ + /* ADQL MANAGEMENT */ + /* *************** */ + + /** + *

    Create the object able to execute an ADQL query and to write and to format its result.

    + * + *

    Note: + * A default implementation is provided by {@link AbstractTAPFactory} + *

    + * + * @return An ADQL executor. + * + * @throws TAPException If any error occurs while creating an ADQL executor. + */ + public abstract ADQLExecutor createADQLExecutor() throws TAPException; + + /** + *

    Create a parser of ADQL query.

    + * + *

    Warning: + * This parser can be created with a query factory and/or a query checker. + * {@link #createQueryFactory()} will be used only if the default query factory (or none) is set + * in the ADQL parser returned by this function. + * Idem for {@link #createQueryChecker(TAPSchema)}: it will used only if no query checker is set + * in the returned ADQL parser. + *

    + * + *

    Note: + * A default implementation is provided by {@link AbstractTAPFactory}. + *

    + * + * @return An ADQL query parser. + * + * @throws TAPException If any error occurs while creating an ADQL parser. + * + * @since 2.0 + */ + public abstract ADQLParser createADQLParser() throws TAPException; + + /** + *

    Create a factory able to build every part of an {@link ADQLQuery} object.

    + * + *

    Warning: + * This function is used only if the default query factory (or none) is set in the ADQL parser + * returned by {@link #createADQLParser()}. + *

    + * + *

    Note: + * A default implementation is provided by {@link AbstractTAPFactory} + *

    + * + * @return An {@link ADQLQuery} factory. + * + * @throws TAPException If any error occurs while creating the factory. + */ + public abstract ADQLQueryFactory createQueryFactory() throws TAPException; + + /** + *

    Create an object able to check the consistency between the ADQL query and the database. + * That's to say, it checks whether the tables and columns used in the query really exist + * in the database.

    + * + *

    Warning: + * This function is used only if no query checker is set in the ADQL parser + * returned by {@link #createADQLParser()}. + *

    + * + *

    Note: + * A default implementation is provided by {@link AbstractTAPFactory} + *

    + * + * @param uploadSchema ADQL schema containing the description of all uploaded tables. + * + * @return A query checker. + * + * @throws TAPException If any error occurs while creating a query checker. + */ + public abstract QueryChecker createQueryChecker(final TAPSchema uploadSchema) throws TAPException; + + /* ****** */ + /* UPLOAD */ + /* ****** */ + + /** + *

    Create an object able to manage the creation of submitted user tables (in VOTable) into the database.

    + * + *

    Note: + * A default implementation is provided by {@link AbstractTAPFactory}. + *

    + * + * @param dbConn The database connection which has requested an {@link Uploader}. + * + * @return An {@link Uploader}. + * + * @throws TAPException If any error occurs while creating an {@link Uploader} instance. + */ + public abstract Uploader createUploader(final DBConnection dbConn) throws TAPException; + + /* ************** */ + /* UWS MANAGEMENT */ + /* ************** */ + + /** + *

    Create the object which will manage the asynchronous resource of the TAP service. + * This resource is a UWS service.

    + * + *

    Note: + * A default implementation is provided by {@link AbstractTAPFactory}. + *

    + * + * @return A UWS service which will be the asynchronous resource of this TAP service. + * + * @throws TAPException If any error occurs while creating this UWS service. + */ + public abstract UWSService createUWS() throws TAPException; + + /** + *

    Create the object which will manage the backup and restoration of all asynchronous jobs.

    + * + *

    Note: + * This function may return NULL. If it does, asynchronous jobs won't be backuped. + *

    + * + *

    Note: + * A default implementation is provided by {@link AbstractTAPFactory}. + *

    + * + * @param uws The UWS service which has to be backuped and restored. + * + * @return The backup manager to use. MAY be NULL + * + * @throws TAPException If any error occurs while creating this backup manager. + */ + public abstract UWSBackupManager createUWSBackupManager(final UWSService uws) throws TAPException; -import adql.translator.ADQLTranslator; + /** + *

    Creates a (PENDING) UWS job from the given HTTP request.

    + * + *

    + * This implementation just call {@link #createTAPJob(HttpServletRequest, JobOwner)} + * with the given request, in order to ensure that the returned object is always a {@link TAPJob}. + *

    + * + * @see uws.service.AbstractUWSFactory#createJob(javax.servlet.http.HttpServletRequest, uws.job.user.JobOwner) + * @see #createTAPJob(HttpServletRequest, JobOwner) + */ + @Override + public final UWSJob createJob(HttpServletRequest request, JobOwner owner) throws UWSException{ + return createTAPJob(request, owner); + } -public interface TAPFactory< R > extends UWSFactory { + /** + *

    Create a PENDING asynchronous job from the given HTTP request.

    + * + *

    Note: + * A default implementation is provided by {@link AbstractTAPFactory}. + *

    + * + * @param request Request which contains all parameters needed to set correctly the asynchronous job to create. + * @param owner The user which has requested the job creation. + * + * @return A new PENDING asynchronous job. + * + * @throws UWSException If any error occurs while reading the parameters in the request or while creating the job. + */ + protected abstract TAPJob createTAPJob(final HttpServletRequest request, final JobOwner owner) throws UWSException; - public UWSService createUWS() throws TAPException, UWSException; + /** + *

    Creates a UWS job with the following attributes.

    + * + *

    + * This implementation just call {@link #createTAPJob(String, JobOwner, TAPParameters, long, long, long, List, ErrorSummary)} + * with the given parameters, in order to ensure that the returned object is always a {@link TAPJob}. + *

    + * + *

    Note 1: + * This function is mainly used to restore a UWS job at the UWS initialization. + *

    + * + *

    Note 2: + * The job phase is chosen automatically from the given job attributes (i.e. no endTime => PENDING, no result and no error => ABORTED, ...). + *

    + * + * @see uws.service.AbstractUWSFactory#createJob(java.lang.String, uws.job.user.JobOwner, uws.job.parameters.UWSParameters, long, long, long, java.util.List, uws.job.ErrorSummary) + * @see #createTAPJob(String, JobOwner, TAPParameters, long, long, long, List, ErrorSummary) + */ + @Override + public final UWSJob createJob(String jobId, JobOwner owner, final UWSParameters params, long quote, long startTime, long endTime, List results, ErrorSummary error) throws UWSException{ + return createTAPJob(jobId, owner, (TAPParameters)params, quote, startTime, endTime, results, error); + } - public UWSBackupManager createUWSBackupManager(final UWSService uws) throws TAPException, UWSException; + /** + *

    Create a PENDING asynchronous job with the given parameters.

    + * + *

    Note: + * A default implementation is provided in {@link AbstractTAPFactory}. + *

    + * + * @param jobId ID of the job (NOT NULL). + * @param owner Owner of the job. + * @param params List of all input job parameters. + * @param quote Its quote (in seconds). + * @param startTime Date/Time of the start of this job. + * @param endTime Date/Time of the end of this job. + * @param results All results of this job. + * @param error The error which ended the job to create. + * + * @return A new PENDING asynchronous job. + * + * @throws UWSException If there is an error while creating the job. + */ + protected abstract TAPJob createTAPJob(final String jobId, final JobOwner owner, final TAPParameters params, final long quote, final long startTime, final long endTime, final List results, final ErrorSummary error) throws UWSException; - public ADQLExecutor createADQLExecutor() throws TAPException; + /** + *

    Create the thread which will execute the task described by the given UWSJob instance.

    + * + *

    + * This function is definitely implemented here and can not be overridden. The processing of + * an ADQL query must always be the same in a TAP service ; it is completely done by {@link AsyncThread}. + *

    + * + * @see uws.service.UWSFactory#createJobThread(uws.job.UWSJob) + * @see AsyncThread + */ + @Override + public final JobThread createJobThread(final UWSJob job) throws UWSException{ + try{ + return new AsyncThread((TAPJob)job, createADQLExecutor(), getErrorWriter()); + }catch(TAPException te){ + if (te.getCause() != null && te.getCause() instanceof UWSException) + throw (UWSException)te.getCause(); + else + throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, te, "Impossible to create an AsyncThread !"); + } + } - public ADQLQueryFactory createQueryFactory() throws TAPException; + /** + *

    Extract the parameters from the given request (multipart or not).

    + * + *

    + * This function is used only to create the set of parameters for a TAP job (synchronous or asynchronous). + * Thus, it just call {@link #createTAPParameters(HttpServletRequest)} with the given request, in order to ensure + * that the returned object is always a {@link TAPParameters}. + *

    + * + * @see #createTAPParameters(HttpServletRequest) + */ + @Override + public final UWSParameters createUWSParameters(HttpServletRequest request) throws UWSException{ + try{ + return createTAPParameters(request); + }catch(TAPException te){ + if (te.getCause() != null && te.getCause() instanceof UWSException) + throw (UWSException)te.getCause(); + else + throw new UWSException(te.getHttpErrorCode(), te); + } + } - public QueryChecker createQueryChecker(TAPSchema uploadSchema) throws TAPException; + /** + *

    Extract all the TAP parameters from the given HTTP request (multipart or not) and return them.

    + * + *

    Note: + * A default implementation is provided by {@link AbstractTAPFactory}. + *

    + * + * @param request The HTTP request containing the TAP parameters to extract. + * + * @return An object gathering all successfully extracted TAP parameters. + * + * @throws TAPException If any error occurs while extracting the parameters. + */ + public abstract TAPParameters createTAPParameters(final HttpServletRequest request) throws TAPException; - public ADQLTranslator createADQLTranslator() throws TAPException; + /** + *

    Identify and gather all identified parameters of the given map inside a {@link TAPParameters} object.

    + * + *

    + * This implementation just call {@link #createTAPParameters(Map)} with the given map, in order to ensure + * that the returned object is always a {@link TAPParameters}. + *

    + * + * @see uws.service.AbstractUWSFactory#createUWSParameters(java.util.Map) + * @see #createTAPParameters(Map) + */ + @Override + public final UWSParameters createUWSParameters(Map params) throws UWSException{ + try{ + return createTAPParameters(params); + }catch(TAPException te){ + if (te.getCause() != null && te.getCause() instanceof UWSException) + throw (UWSException)te.getCause(); + else + throw new UWSException(te.getHttpErrorCode(), te); + } + } - public DBConnection createDBConnection(final String jobID) throws TAPException; + /** + *

    Identify all TAP parameters and gather them inside a {@link TAPParameters} object.

    + * + *

    Note: + * A default implementation is provided by {@link AbstractTAPFactory}. + *

    + * + * @param params Map containing all parameters. + * + * @return An object gathering all successfully identified TAP parameters. + * + * @throws TAPException If any error occurs while creating the {@link TAPParameters} object. + */ + public abstract TAPParameters createTAPParameters(final Map params) throws TAPException; - public Uploader createUploader(final DBConnection dbConn) throws TAPException; + @Override + public RequestParser createRequestParser(final UWSFileManager fileManager) throws UWSException{ + return new TAPRequestParser(fileManager); + } } diff --git a/src/tap/TAPJob.java b/src/tap/TAPJob.java index 52510c5..aa89491 100644 --- a/src/tap/TAPJob.java +++ b/src/tap/TAPJob.java @@ -16,121 +16,224 @@ package tap; * You should have received a copy of the GNU Lesser General Public License * along with TAPLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ +import java.util.Date; import java.util.List; +import tap.log.TAPLog; +import tap.parameters.DALIUpload; import tap.parameters.TAPParameters; -import tap.upload.TableLoader; - import uws.UWSException; - import uws.job.ErrorSummary; +import uws.job.ExecutionPhase; +import uws.job.JobThread; import uws.job.Result; import uws.job.UWSJob; - +import uws.job.parameters.UWSParameters; import uws.job.user.JobOwner; +import uws.service.log.UWSLog.LogLevel; +/** + *

    Description of a TAP job. This class is used for asynchronous but also synchronous queries.

    + * + *

    + * On the contrary to {@link UWSJob}, it is loading parameters from {@link TAPParameters} instances rather than {@link UWSParameters}. + * However, {@link TAPParameters} is an extension of {@link UWSParameters}. That's what allow the UWS library to use both {@link TAPJob} and {@link TAPParameters}. + *

    + * + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (04/2015) + */ public class TAPJob extends UWSJob { - private static final long serialVersionUID = 1L; + /** Name of the standard TAP parameter which specifies the type of request to execute: "REQUEST". */ public static final String PARAM_REQUEST = "request"; + /** REQUEST value meaning an ADQL query must be executed: "doQuery". */ public static final String REQUEST_DO_QUERY = "doQuery"; + /** REQUEST value meaning VO service capabilities must be returned: "getCapabilities". */ public static final String REQUEST_GET_CAPABILITIES = "getCapabilities"; + /** Name of the standard TAP parameter which specifies the query language: "LANG". (only the ADQL language is supported by default in this version of the library) */ public static final String PARAM_LANGUAGE = "lang"; + /** LANG value meaning ADQL language: "ADQL". */ public static final String LANG_ADQL = "ADQL"; + /** LANG value meaning PQL language: "PQL". (this language is not supported in this version of the library) */ public static final String LANG_PQL = "PQL"; + /** Name of the standard TAP parameter which specifies the version of the TAP protocol that must be used: "VERSION". (only the version 1.0 is supported in this version of the library) */ public static final String PARAM_VERSION = "version"; + /** VERSION value meaning the version 1.0 of TAP: "1.0". */ public static final String VERSION_1_0 = "1.0"; + /** Name of the standard TAP parameter which specifies the output format (format of a query result): "FORMAT". */ public static final String PARAM_FORMAT = "format"; + /** FORMAT value meaning the VOTable format: "votable". */ public static final String FORMAT_VOTABLE = "votable"; + /** Name of the standard TAP parameter which specifies the maximum number of rows that must be returned in the query result: "MAXREC". */ public static final String PARAM_MAX_REC = "maxRec"; + /** Special MAXREC value meaning the number of output rows is not limited. */ public static final int UNLIMITED_MAX_REC = -1; + /** Name of the standard TAP parameter which specifies the query to execute: "QUERY". */ public static final String PARAM_QUERY = "query"; + + /** Name of the standard TAP parameter which defines the tables to upload in the database for the query execution: "UPLOAD". */ public static final String PARAM_UPLOAD = "upload"; + /** Name of the library parameter which informs about a query execution progression: "PROGRESSION". (this parameter is removed once the execution is finished) */ public static final String PARAM_PROGRESSION = "progression"; - protected TAPExecutionReport execReport; + /** Internal query execution report. */ + protected TAPExecutionReport execReport = null; + /** Parameters of this job for its execution. */ protected final TAPParameters tapParams; - public TAPJob(final JobOwner owner, final TAPParameters tapParams) throws UWSException, TAPException{ + /** + *

    Build a pending TAP job with the given parameters.

    + * + *

    Note: if the parameter {@link #PARAM_PHASE} (phase) is given with the value {@link #PHASE_RUN} + * the job execution starts immediately after the job has been added to a job list or after {@link #applyPhaseParam(JobOwner)} is called.

    + * + * @param owner User who owns this job. MAY BE NULL + * @param tapParams Set of parameters. + * + * @throws TAPException If one of the given parameters has a forbidden or wrong value. + */ + public TAPJob(final JobOwner owner, final TAPParameters tapParams) throws TAPException{ super(owner, tapParams); this.tapParams = tapParams; tapParams.check(); - //progression = ExecutionProgression.PENDING; - //loadTAPParams(tapParams); } - public TAPJob(final String jobID, final JobOwner owner, final TAPParameters params, final long quote, final long startTime, final long endTime, final List results, final ErrorSummary error) throws UWSException, TAPException{ + /** + *

    Restore a job in a state defined by the given parameters. + * The phase must be set separately with {@link #setPhase(uws.job.ExecutionPhase, boolean)}, where the second parameter is true.

    + * + * @param jobID ID of the job. + * @param owner User who owns this job. + * @param params Set of not-standard UWS parameters (i.e. what is called by {@link UWSJob} as additional parameters ; they includes all TAP parameters). + * @param quote Quote of this job. + * @param startTime Date/Time at which this job started. (if not null, it means the job execution was finished, so a endTime should be provided) + * @param endTime Date/Time at which this job finished. + * @param results List of results. NULL if the job has not been executed, has been aborted or finished with an error. + * @param error Error with which this job ends. + * + * @throws TAPException If one of the given parameters has a forbidden or wrong value. + */ + public TAPJob(final String jobID, final JobOwner owner, final TAPParameters params, final long quote, final long startTime, final long endTime, final List results, final ErrorSummary error) throws TAPException{ super(jobID, owner, params, quote, startTime, endTime, results, error); this.tapParams = params; this.tapParams.check(); } - /*protected void loadTAPParams(TAPParameters params) { - adqlQuery = params.query; - additionalParameters.put(TAPParameters.PARAM_QUERY, adqlQuery); - - format = (params.format == null)?"application/x-votable+xml":params.format; - additionalParameters.put(TAPParameters.PARAM_FORMAT, format); - - maxRec = params.maxrec; - additionalParameters.put(TAPParameters.PARAM_MAX_REC, maxRec+""); - - upload = params.upload; - tablesToUpload = params.tablesToUpload; - additionalParameters.put(TAPParameters.PARAM_UPLOAD, upload); - }*/ - /** - * @return The tapParams. + * Get the object storing and managing the set of all (UWS and TAP) parameters. + * + * @return The object managing all job parameters. */ public final TAPParameters getTapParams(){ return tapParams; } + /** + *

    Get the value of the REQUEST parameter.

    + * + *

    This value must be {@value #REQUEST_DO_QUERY}.

    + * + * @return REQUEST value. + */ public final String getRequest(){ return tapParams.getRequest(); } + /** + * Get the value of the FORMAT parameter. + * + * @return FORMAT value. + */ public final String getFormat(){ return tapParams.getFormat(); } + /** + *

    Get the value of the LANG parameter.

    + * + *

    This value should always be {@value #LANG_ADQL} in this version of the library

    + * + * @return LANG value. + */ public final String getLanguage(){ return tapParams.getLang(); } + /** + *

    Get the value of the MAXREC parameter.

    + * + *

    If this value is negative, it means the number of output rows is not limited.

    + * + * @return MAXREC value. + */ public final int getMaxRec(){ return tapParams.getMaxRec(); } + /** + * Get the value of the QUERY parameter (i.e. the query, in the language returned by {@link #getLanguage()}, to execute). + * + * @return QUERY value. + */ public final String getQuery(){ return tapParams.getQuery(); } + /** + *

    Get the value of the VERSION parameter.

    + * + *

    This value should be {@value #VERSION_1_0} in this version of the library.

    + * + * @return VERSION value. + */ public final String getVersion(){ return tapParams.getVersion(); } + /** + *

    Get the value of the UPLOAD parameter.

    + * + *

    This value must be formatted as specified by the TAP standard (= a semicolon separated list of DALI uploads).

    + * + * @return UPLOAD value. + */ public final String getUpload(){ return tapParams.getUpload(); } - public final TableLoader[] getTablesToUpload(){ - return tapParams.getTableLoaders(); + /** + *

    Get the list of tables to upload in the database for the query execution.

    + * + *

    The returned array is an interpretation of the UPLOAD parameter.

    + * + * @return List of tables to upload. + */ + public final DALIUpload[] getTablesToUpload(){ + return tapParams.getUploadedTables(); } /** + *

    Get the execution report.

    + * + *

    + * This report is available only during or after the job execution. + * It tells in which step the execution is, and how long was the previous steps. + * It can also give more information about the number of resulting rows and columns. + *

    + * * @return The execReport. */ public final TAPExecutionReport getExecReport(){ @@ -138,63 +241,120 @@ public class TAPJob extends UWSJob { } /** - * @param execReport The execReport to set. + *

    Set the execution report.

    + * + *

    IMPORTANT: + * This function can be called only if the job is running or is being restored, otherwise an exception would be thrown. + * It should not be used by implementors, but only by the internal library processing. + *

    + * + * @param execReport An execution report. + * + * @throws UWSException If this job has never been restored and is not running. */ - public final void setExecReport(TAPExecutionReport execReport) throws UWSException{ - if (getRestorationDate() == null && !isRunning()) + public final void setExecReport(final TAPExecutionReport execReport) throws UWSException{ + if (getRestorationDate() == null && (thread == null || thread.isFinished())) throw new UWSException("Impossible to set an execution report if the job is not in the EXECUTING phase ! Here, the job \"" + jobId + "\" is in the phase " + getPhase()); this.execReport = execReport; } - /* - *

    Starts in an asynchronous manner this ADQLExecutor.

    - *

    The execution will stop after the duration specified in the given {@link TAPJob} - * (see {@link TAPJob#getExecutionDuration()}).

    - * - * @param output - * @return - * @throws IllegalStateException - * @throws InterruptedException - * - public synchronized final boolean startSync(final OutputStream output) throws IllegalStateException, InterruptedException, UWSException { - // TODO Set the output stream so that the result is written directly in the given output ! - start(); - System.out.println("Joining..."); - thread.join(getExecutionDuration()); - System.out.println("Aborting..."); - thread.interrupt(); - thread.join(getTimeToWaitForEnd()); - return thread.isInterrupted(); - }*/ + /** + *

    Create the thread to use for the execution of this job.

    + * + *

    Note: If the job already exists, this function does nothing.

    + * + * @throws NullPointerException If the factory returned NULL rather than the asked {@link JobThread}. + * @throws UWSException If the thread creation fails. + * + * @see TAPFactory#createJobThread(UWSJob) + * + * @since 2.0 + */ + private final void createThread() throws NullPointerException, UWSException{ + if (thread == null){ + thread = getFactory().createJobThread(this); + if (thread == null) + throw new NullPointerException("Missing job work! The thread created by the factory is NULL => The job can't be executed!"); + } + } + + /** + *

    Check whether this job is able to start right now.

    + * + *

    + * Basically, this function try to get a database connection. If none is available, + * then this job can not start and this function return FALSE. In all the other cases, + * TRUE is returned. + *

    + * + *

    Warning: This function will indirectly open and keep a database connection, so that the job can be started just after its call. + * If it turns out that the execution won't start just after this call, the DB connection should be closed in some way in order to save database resources.

    + * + * @return true if this job can start right now, false otherwise. + * + * @since 2.0 + */ + public final boolean isReadyForExecution(){ + return thread != null && ((AsyncThread)thread).isReadyForExecution(); + } @Override - protected void stop(){ - if (!isStopped()){ - //try { - stopping = true; - // TODO closeDBConnection(); - super.stop(); - /*} catch (TAPException e) { - getLogger().error("Impossible to cancel the query execution !", e); - return; - }*/ + public final void start(final boolean useManager) throws UWSException{ + // This job must know its jobs list and this jobs list must know its UWS: + if (getJobList() == null || getJobList().getUWS() == null) + throw new IllegalStateException("A TAPJob can not start if it is not linked to a job list or if its job list is not linked to a UWS."); + + // If already running do nothing: + else if (isRunning()) + return; + + // If asked propagate this request to the execution manager: + else if (useManager){ + // Create its corresponding thread, if not already existing: + createThread(); + // Ask to the execution manager to test whether the job is ready for execution, and if, execute it (by calling this function with "false" as parameter): + getJobList().getExecutionManager().execute(this); + + }// Otherwise start directly the execution: + else{ + // Create its corresponding thread, if not already existing: + createThread(); + if (!isReadyForExecution()){ + UWSException ue = new NoDBConnectionAvailableException(); + ((TAPLog)getLogger()).logDB(LogLevel.ERROR, null, "CONNECTION_LACK", "No more database connection available for the moment!", ue); + getLogger().logJob(LogLevel.ERROR, this, "ERROR", "Asynchronous job " + jobId + " execution aborted: no database connection available!", null); + throw ue; + } + + // Change the job phase: + setPhase(ExecutionPhase.EXECUTING); + + // Set the start time: + setStartTime(new Date()); + + // Run the job: + thread.start(); + (new JobTimeOut()).start(); + + // Log the start of this job: + getLogger().logJob(LogLevel.INFO, this, "START", "Job \"" + jobId + "\" started.", null); } } - /*protected boolean deleteResultFiles(){ - try{ - // TODO service.deleteResults(this); - return true; - }catch(TAPException ex){ - service.log(LogType.ERROR, "Job "+getJobId()+" - Can't delete results files: "+ex.getMessage()); - return false; + /** + * This exception is thrown by a job execution when no database connection are available anymore. + * + * @author Grégory Mantelet (ARI) + * @version 2.0 (02/2015) + * @since 2.0 + */ + public static class NoDBConnectionAvailableException extends UWSException { + private static final long serialVersionUID = 1L; + + public NoDBConnectionAvailableException(){ + super("Service momentarily too busy! Please try again later."); } - }*/ - @Override - public void clearResources(){ - super.clearResources(); - // TODO deleteResultFiles(); } } diff --git a/src/tap/TAPRequestParser.java b/src/tap/TAPRequestParser.java new file mode 100644 index 0000000..91c24c6 --- /dev/null +++ b/src/tap/TAPRequestParser.java @@ -0,0 +1,216 @@ +package tap; + +/* + * This file is part of TAPLibrary. + * + * TAPLibrary is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * TAPLibrary is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with TAPLibrary. If not, see . + * + * Copyright 2014 - Astronomisches Rechen Institut (ARI) + */ + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + +import uws.UWSException; +import uws.UWSToolBox; +import uws.service.file.UWSFileManager; +import uws.service.request.FormEncodedParser; +import uws.service.request.MultipartParser; +import uws.service.request.NoEncodingParser; +import uws.service.request.RequestParser; +import uws.service.request.UploadFile; + +/** + *

    This parser adapts the request parser to use in function of the request content-type:

    + *
      + *
    • application/x-www-form-urlencoded: {@link FormEncodedParser}
    • + *
    • multipart/form-data: {@link MultipartParser}
    • + *
    • other: {@link NoEncodingParser} (the whole request body will be stored as one single parameter)
    • + *
    + * + *

    + * The request body size is limited for the multipart AND the no-encoding parsers. If you want to change this limit, + * you MUST do it for each of these parsers, setting the following static attributes: resp. {@link MultipartParser#SIZE_LIMIT} + * and {@link NoEncodingParser#SIZE_LIMIT}. + *

    + * + *

    Note: + * If you want to change the support other request parsing, you will have to write your own {@link RequestParser} implementation. + *

    + * + * @author Grégory Mantelet (ARI) + * @version 2.0 (12/2014) + * @since 2.0 + */ +public class TAPRequestParser implements RequestParser { + + /** File manager to use to create {@link UploadFile} instances. + * It is required by this new object to execute open, move and delete operations whenever it could be asked. */ + private final UWSFileManager fileManager; + + /** {@link RequestParser} to use when a application/x-www-form-urlencoded request must be parsed. This attribute is set by {@link #parse(HttpServletRequest)} + * only when needed, by calling the function {@link #getFormParser()}. */ + private RequestParser formParser = null; + + /** {@link RequestParser} to use when a multipart/form-data request must be parsed. This attribute is set by {@link #parse(HttpServletRequest)} + * only when needed, by calling the function {@link #getMultipartParser()}. */ + private RequestParser multipartParser = null; + + /** {@link RequestParser} to use when none of the other parsers can be used ; it will then transform the whole request body in a parameter called "JDL" + * (Job Description Language). This attribute is set by {@link #parse(HttpServletRequest)} only when needed, by calling the function + * {@link #getNoEncodingParser()}. */ + private RequestParser noEncodingParser = null; + + /** + * Build a {@link RequestParser} able to choose the most appropriate {@link RequestParser} in function of the request content-type. + * + * @param fileManager The file manager to use in order to store any eventual upload. MUST NOT be NULL + */ + public TAPRequestParser(final UWSFileManager fileManager){ + if (fileManager == null) + throw new NullPointerException("Missing file manager => can not create a TAPRequestParser!"); + this.fileManager = fileManager; + } + + @Override + public Map parse(final HttpServletRequest req) throws UWSException{ + if (req == null) + return new HashMap(); + + // Get the method: + String method = (req.getMethod() == null) ? "" : req.getMethod().toLowerCase(); + + if (method.equals("post") || method.equals("put")){ + Map params = null; + + // Get the parameters: + if (FormEncodedParser.isFormEncodedRequest(req)) + params = getFormParser().parse(req); + else if (MultipartParser.isMultipartContent(req)) + params = getMultipartParser().parse(req); + else + params = getNoEncodingParser().parse(req); + + // Only for POST requests, the parameters specified in the URL must be added: + if (method.equals("post")) + params = UWSToolBox.addGETParameters(req, (params == null) ? new HashMap() : params); + + return params; + }else + return UWSToolBox.addGETParameters(req, new HashMap()); + } + + /** + * Get the {@link RequestParser} to use for application/x-www-form-urlencoded HTTP requests. + * This parser may be created if not already done. + * + * @return The {@link RequestParser} to use for application/x-www-form-urlencoded requests. Never NULL + */ + private synchronized final RequestParser getFormParser(){ + return (formParser != null) ? formParser : (formParser = new FormEncodedParser(){ + @Override + protected void consumeParameter(String name, Object value, final Map allParams){ + // Modify the value if it is an UPLOAD parameter: + if (name != null && name.equalsIgnoreCase("upload")){ + // if no value, ignore this parameter: + if (value == null) + return; + // put in lower case the parameter name: + name = name.toLowerCase(); + // transform the value in a String array: + value = append((String)value, (allParams.containsKey("upload") ? (String[])allParams.get("upload") : null)); + } + + // Update the map, normally: + super.consumeParameter(name, value, allParams); + } + }); + } + + /** + * Get the {@link RequestParser} to use for multipart/form-data HTTP requests. + * This parser may be created if not already done. + * + * @return The {@link RequestParser} to use for multipart/form-data requests. Never NULL + */ + private synchronized final RequestParser getMultipartParser(){ + return (multipartParser != null) ? multipartParser : (multipartParser = new MultipartParser(fileManager){ + @Override + protected void consumeParameter(String name, Object value, final Map allParams){ + // Modify the value if it is an UPLOAD parameter: + if (name != null && name.equalsIgnoreCase(TAPJob.PARAM_UPLOAD)){ + // if no value, ignore this parameter: + if (value == null) + return; + // ignore also parameter having the same name in the same case and which is a file (only strings can be processed as DALI UPLOAD parameter): + else if (name.equals(TAPJob.PARAM_UPLOAD) && value instanceof UploadFile){ + try{ + ((UploadFile)value).deleteFile(); + }catch(IOException ioe){} + return; + } + // use the same case for the parameter name: + name = TAPJob.PARAM_UPLOAD; + // transform the value in a String array: + value = append((String)value, (allParams.containsKey(TAPJob.PARAM_UPLOAD) ? (String[])allParams.get(TAPJob.PARAM_UPLOAD) : null)); + } + + // Update the map, normally: + super.consumeParameter(name, value, allParams); + } + }); + } + + /** + * Get the {@link RequestParser} to use for HTTP requests whose the content type is neither application/x-www-form-urlencoded nor multipart/form-data. + * This parser may be created if not already done. + * + * @return The {@link RequestParser} to use for requests whose the content-type is not supported. Never NULL + */ + private synchronized final RequestParser getNoEncodingParser(){ + return (noEncodingParser == null) ? (noEncodingParser = new NoEncodingParser(fileManager)) : noEncodingParser; + } + + /** + * Create a new array in which the given String is appended at the end of the given array. + * + * @param value String to append in the array. + * @param oldValue The array after which the given String must be appended. + * + * @return The new array containing the values of the array and then the given String. + */ + private final static String[] append(final String value, final String[] oldValue){ + // Create the corresponding array of Strings: + // ...if the array already exists, extend it: + String[] newValue; + if (oldValue != null){ + newValue = new String[oldValue.length + 1]; + for(int i = 0; i < oldValue.length; i++) + newValue[i] = oldValue[i]; + } + // ...otherwise, create a new array: + else + newValue = new String[1]; + + // Add the new value in the array: + newValue[newValue.length - 1] = value; + + // Update the value to put inside the map: + return newValue; + } + +} diff --git a/src/tap/TAPSyncJob.java b/src/tap/TAPSyncJob.java index 7267128..01421a3 100644 --- a/src/tap/TAPSyncJob.java +++ b/src/tap/TAPSyncJob.java @@ -16,9 +16,11 @@ package tap; * You should have received a copy of the GNU Lesser General Public License * along with TAPLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ +import java.io.IOException; import java.util.Date; import javax.servlet.http.HttpServletResponse; @@ -26,26 +28,66 @@ import javax.servlet.http.HttpServletResponse; import tap.parameters.TAPParameters; import uws.UWSException; import uws.job.JobThread; +import uws.service.log.UWSLog.LogLevel; +/** + *

    This class represent a TAP synchronous job. + * A such job must execute an ADQL query and return immediately its result.

    + * + *

    Timeout

    + * + *

    + * The execution of a such job is limited to a short time. Once this time elapsed, the job is stopped. + * For a longer job, an asynchronous job should be used. + *

    + * + *

    Error management

    + * + *

    + * If an error occurs it must be propagated ; it will be written later in the HTTP response on a top level. + *

    + * + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (02/2015) + */ public class TAPSyncJob { /** The time (in ms) to wait the end of the thread after an interruption. */ protected long waitForStop = 1000; + /** Last generated ID of a synchronous job. */ protected static String lastId = null; - protected final ServiceConnection service; + /** Description of the TAP service in charge of this synchronous job. */ + protected final ServiceConnection service; + /** ID of this job. This ID is also used to identify the thread. */ protected final String ID; + + /** Parameters of the execution. It mainly contains the ADQL query to execute. */ protected final TAPParameters tapParams; + /** The thread in which the query execution will be done. */ protected SyncThread thread; + /** Report of the query execution. It stays NULL until the execution ends. */ protected TAPExecutionReport execReport = null; + /** Date at which this synchronous job has really started. It is NULL when the job has never been started. + * + *

    Note: A synchronous job can be run just once ; so if an attempt of executing it again, the start date will be tested: + * if NULL, the second starting is not considered and an exception is thrown.

    */ private Date startedAt = null; - public TAPSyncJob(final ServiceConnection service, final TAPParameters params) throws NullPointerException{ + /** + * Create a synchronous TAP job. + * + * @param service Description of the TAP service which is in charge of this synchronous job. + * @param params Parameters of the query to execute. It must mainly contain the ADQL query to execute. + * + * @throws NullPointerException If one of the parameters is NULL. + */ + public TAPSyncJob(final ServiceConnection service, final TAPParameters params) throws NullPointerException{ if (params == null) throw new NullPointerException("Missing TAP parameters ! => Impossible to create a synchronous TAP job."); tapParams = params; @@ -63,8 +105,8 @@ public class TAPSyncJob { * *

    By default: "S"+System.currentTimeMillis()+UpperCharacter (UpperCharacter: one upper-case character: A, B, C, ....)

    * - *

    note: DO NOT USE in this function any of the following functions: {@link #getLogger()}, - * {@link #getFileManager()} and {@link #getFactory()}. All of them will return NULL, because this job does not + *

    note: DO NOT USE in this function any of the following functions: {@link ServiceConnection#getLogger()}, + * {@link ServiceConnection#getFileManager()} and {@link ServiceConnection#getFactory()}. All of them will return NULL, because this job does not * yet know its jobs list (which is needed to know the UWS and so, all of the objects returned by these functions).

    * * @return A unique job identifier. @@ -79,94 +121,214 @@ public class TAPSyncJob { return generatedId; } + /** + * Get the ID of this synchronous job. + * + * @return The job ID. + */ public final String getID(){ return ID; } + /** + * Get the TAP parameters provided by the user and which will be used for the execution of this job. + * + * @return Job parameters. + */ public final TAPParameters getTapParams(){ return tapParams; } + /** + * Get the report of the execution of this job. + * This report is NULL if the execution has not yet started. + * + * @return Report of this job execution. + */ public final TAPExecutionReport getExecReport(){ return execReport; } - public synchronized boolean start(final HttpServletResponse response) throws IllegalStateException, UWSException, TAPException{ + /** + *

    Start the execution of this job in order to execute the given ADQL query.

    + * + *

    The execution itself will be processed by an {@link ADQLExecutor} inside a thread ({@link SyncThread}).

    + * + *

    Important: + * No error should be written in this function. If any error occurs it should be thrown, in order to be manager on a top level. + *

    + * + * @param response Response in which the result must be written. + * + * @return true if the execution was successful, false otherwise. + * + * @throws IllegalStateException If this synchronous job has already been started before. + * @throws IOException If any error occurs while writing the query result in the given {@link HttpServletResponse}. + * @throws TAPException If any error occurs while executing the ADQL query. + * + * @see SyncThread + */ + public synchronized boolean start(final HttpServletResponse response) throws IllegalStateException, IOException, TAPException{ if (startedAt != null) - throw new IllegalStateException("Impossible to restart a synchronous TAP query !"); + throw new IllegalStateException("Impossible to restart a synchronous TAP query!"); + + // Log the start of this sync job: + service.getLogger().logTAP(LogLevel.INFO, this, "START", "Synchronous job " + ID + " is starting!", null); - ADQLExecutor executor; + // Create the object having the knowledge about how to execute an ADQL query: + ADQLExecutor executor = service.getFactory().createADQLExecutor(); try{ - executor = service.getFactory().createADQLExecutor(); - }catch(TAPException e){ - // TODO Log this error ! - return true; + executor.initDBConnection(ID); + }catch(TAPException te){ + service.getLogger().logDB(LogLevel.ERROR, null, "CONNECTION_LACK", "No more database connection available for the moment!", te); + service.getLogger().logTAP(LogLevel.ERROR, this, "END", "Synchronous job " + ID + " execution aborted: no database connection available!", null); + throw new TAPException("TAP service too busy! No connection available for the moment. You should try later or create an asynchronous query (which will be executed when enough resources will be available again).", UWSException.SERVICE_UNAVAILABLE); } + + // Give to a thread which will execute the query: thread = new SyncThread(executor, ID, tapParams, response); thread.start(); - boolean timeout = false; + // Wait the end of the thread until the maximum execution duration is reached: + boolean timeout = false; try{ - System.out.println("Joining..."); - thread.join(tapParams.getExecutionDuration()); + // wait the end: + thread.join(tapParams.getExecutionDuration() * 1000); + // if still alive after this duration, interrupt it: if (thread.isAlive()){ timeout = true; - System.out.println("Aborting..."); thread.interrupt(); thread.join(waitForStop); } }catch(InterruptedException ie){ - ; + /* Having a such exception here, is not surprising, because we may have interrupted the thread! */ }finally{ + // Whatever the way the execution stops (normal, cancel or error), an execution report must be fulfilled: execReport = thread.getExecutionReport(); } - if (!thread.isSuccess()){ + // Report any error that may have occurred while the thread execution: + Throwable error = thread.getError(); + // CASE: TIMEOUT + if (timeout && error != null && error instanceof InterruptedException){ + // Log the timeout: if (thread.isAlive()) - throw new TAPException("Time out (=" + tapParams.getExecutionDuration() + "ms) ! However, the thread (synchronous query) can not be stopped !", HttpServletResponse.SC_INTERNAL_SERVER_ERROR); - else if (timeout) - throw new TAPException("Time out ! The execution of this synchronous TAP query was limited to " + tapParams.getExecutionDuration() + "ms.", HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + service.getLogger().logTAP(LogLevel.WARNING, this, "TIME_OUT", "Time out (after " + tapParams.getExecutionDuration() + "ms) for the synchonous job " + ID + ", but the thread can not be interrupted!", null); + else + service.getLogger().logTAP(LogLevel.INFO, this, "TIME_OUT", "Time out (after " + tapParams.getExecutionDuration() + "ms) for the synchonous job " + ID + ".", null); + + // Report the timeout to the user: + throw new TAPException("Time out! The execution of this synchronous TAP query was limited to " + tapParams.getExecutionDuration() + "ms. You should try again but in asynchronous execution.", UWSException.ACCEPTED_BUT_NOT_COMPLETE); + } + // CASE: ERRORS + else if (!thread.isSuccess()){ + // INTERRUPTION: + if (error instanceof InterruptedException){ + // log the unexpected interruption (unexpected because not caused by a timeout): + service.getLogger().logTAP(LogLevel.ERROR, this, "END", "The execution of the synchronous job " + ID + " has been unexpectedly interrupted!", error); + // report the unexpected interruption to the user: + throw new TAPException("The execution of this synchronous job " + ID + " has been unexpectedly aborted!", UWSException.ACCEPTED_BUT_NOT_COMPLETE); + } + // REQUEST ABORTION: + else if (error instanceof IOException){ + // log the unexpected interruption (unexpected because not caused by a timeout): + service.getLogger().logTAP(LogLevel.INFO, this, "END", "Abortion of the synchronous job " + ID + "! Cause: connection with the HTTP client unexpectedly closed.", null); + // throw the error until the TAP instance to notify it about the abortion: + throw (IOException)error; + } + // TAP EXCEPTION: + else if (error instanceof TAPException){ + // log the error: + service.getLogger().logTAP(LogLevel.ERROR, this, "END", "The following error interrupted the execution of the synchronous job " + ID + ".", error); + // report the error to the user: + throw (TAPException)error; + } + // ANY OTHER EXCEPTION: else{ - Throwable t = thread.getError(); - if (t instanceof InterruptedException) - throw new TAPException("The execution of this synchronous TAP query has been unexpectedly aborted !"); - else if (t instanceof UWSException) - throw (UWSException)t; + // log the error: + service.getLogger().logTAP(LogLevel.FATAL, this, "END", "The following GRAVE error interrupted the execution of the synchronous job " + ID + ".", error); + // report the error to the user: + if (error instanceof Error) + throw (Error)error; else - throw new TAPException(t); + throw new TAPException(error); } - } + }else + service.getLogger().logTAP(LogLevel.INFO, this, "END", "Success of the synchronous job " + ID + ".", null); - return thread.isInterrupted(); + return thread.isSuccess(); } - public class SyncThread extends Thread { + /** + *

    Thread which will process the job execution.

    + * + *

    + * Actually, it will basically just call {@link ADQLExecutor#start(Thread, String, TAPParameters, HttpServletResponse)} + * with the given {@link ADQLExecutor} and TAP parameters (containing the ADQL query to execute). + *

    + * + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (04/2015) + */ + protected class SyncThread extends Thread { - private final String taskDescription; - public final ADQLExecutor executor; + /** Object knowing how to execute an ADQL query and which will execute it by calling {@link ADQLExecutor#start(Thread, String, TAPParameters, HttpServletResponse)}. */ + protected final ADQLExecutor executor; + /** Response in which the query result must be written. No error should be written in it directly at this level ; + * the error must be propagated and it will be written in this HTTP response later on a top level. */ protected final HttpServletResponse response; + /** ID of this thread. It is also the ID of the synchronous job owning this thread. */ protected final String ID; + /** Parameters containing the ADQL query to execute and other execution parameters/options. */ protected final TAPParameters tapParams; + + /** Exception that occurs while executing this thread. NULL if the execution was a success. */ protected Throwable exception = null; + /** Query execution report. NULL if the execution has not yet started. */ protected TAPExecutionReport report = null; - public SyncThread(final ADQLExecutor executor, final String ID, final TAPParameters tapParams, final HttpServletResponse response){ + /** + * Create a thread that will run the given executor with the given parameters. + * + * @param executor Object to execute and which knows how to execute an ADQL query. + * @param ID ID of the synchronous job owning this thread. + * @param tapParams TAP parameters to use to get the query to execute and the execution parameters. + * @param response HTTP response in which the ADQL query result must be written. + */ + public SyncThread(final ADQLExecutor executor, final String ID, final TAPParameters tapParams, final HttpServletResponse response){ super(JobThread.tg, ID); - taskDescription = "Executing the synchronous TAP query " + ID; this.executor = executor; this.ID = ID; this.tapParams = tapParams; this.response = response; } + /** + * Tell whether the execution has ended with success. + * + * @return true if the query has been successfully executed, + * false otherwise (or if this thread is still executed). + */ public final boolean isSuccess(){ return !isAlive() && report != null && exception == null; } + /** + * Get the error that has interrupted/stopped this thread. + * This function returns NULL if the query has been successfully executed. + * + * @return Error that occurs while executing the query + * or NULL if the execution was a success. + */ public final Throwable getError(){ return exception; } + /** + * Get the report of the query execution. + * + * @return Query execution report. + */ public final TAPExecutionReport getExecutionReport(){ return report; } @@ -174,17 +336,30 @@ public class TAPSyncJob { @Override public void run(){ // Log the start of this thread: - executor.getLogger().threadStarted(this, taskDescription); + executor.getLogger().logThread(LogLevel.INFO, thread, "START", "Synchronous thread \"" + ID + "\" started.", null); try{ + // Execute the ADQL query: report = executor.start(this, ID, tapParams, response); - executor.getLogger().threadFinished(this, taskDescription); + + // Log the successful end of this thread: + executor.getLogger().logThread(LogLevel.INFO, thread, "END", "Synchronous thread \"" + ID + "\" successfully ended.", null); + }catch(Throwable e){ + + // Save the exception for later reporting: exception = e; - if (e instanceof InterruptedException){ - // Log the abortion: - executor.getLogger().threadInterrupted(this, taskDescription, e); - } + + // Log the end of the job: + if (e instanceof InterruptedException || e instanceof IOException) + // Abortion: + executor.getLogger().logThread(LogLevel.INFO, this, "END", "Synchronous thread \"" + ID + "\" cancelled.", null); + else if (e instanceof TAPException) + // Error: + executor.getLogger().logThread(LogLevel.ERROR, this, "END", "Synchronous thread \"" + ID + "\" ended with an error.", null); + else + // GRAVE error: + executor.getLogger().logThread(LogLevel.FATAL, this, "END", "Synchronous thread \"" + ID + "\" ended with a FATAL error.", null); } } diff --git a/src/tap/backup/DefaultTAPBackupManager.java b/src/tap/backup/DefaultTAPBackupManager.java index 4e4a86f..5e740dd 100644 --- a/src/tap/backup/DefaultTAPBackupManager.java +++ b/src/tap/backup/DefaultTAPBackupManager.java @@ -16,105 +16,288 @@ package tap.backup; * You should have received a copy of the GNU Lesser General Public License * along with TAPLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; + +import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; +import org.json.Json4Uws; import tap.ExecutionProgression; import tap.TAPExecutionReport; import tap.TAPJob; +import tap.parameters.DALIUpload; import uws.UWSException; import uws.job.UWSJob; import uws.service.UWS; import uws.service.backup.DefaultUWSBackupManager; +import uws.service.log.UWSLog.LogLevel; +import uws.service.request.UploadFile; +/** + *

    Let backup all TAP asynchronous jobs.

    + * + *

    note: Basically the saved data are the same, but in addition some execution statistics are also added.

    + * + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (12/2014) + * + * @see DefaultUWSBackupManager + */ public class DefaultTAPBackupManager extends DefaultUWSBackupManager { + /** + * Build a default TAP jobs backup manager. + * + * @param uws The UWS containing all the jobs to backup. + * + * @see DefaultUWSBackupManager#DefaultUWSBackupManager(UWS) + */ public DefaultTAPBackupManager(UWS uws){ super(uws); } + /** + * Build a default TAP jobs backup manager. + * + * @param uws The UWS containing all the jobs to backup. + * @param frequency The backup frequency (in ms ; MUST BE positive and different from 0. + * If negative or 0, the frequency will be automatically set to DEFAULT_FREQUENCY). + * + * @see DefaultUWSBackupManager#DefaultUWSBackupManager(UWS, long) + */ public DefaultTAPBackupManager(UWS uws, long frequency){ super(uws, frequency); } + /** + * Build a default TAP jobs backup manager. + * + * @param uws The UWS containing all the jobs to backup. + * @param byUser Backup mode. + * + * @see DefaultUWSBackupManager#DefaultUWSBackupManager(UWS, boolean) + */ public DefaultTAPBackupManager(UWS uws, boolean byUser) throws UWSException{ super(uws, byUser); } + /** + * Build a default TAP jobs backup manager. + * + * @param uws The UWS containing all the jobs to backup. + * @param byUser Backup mode. + * @param frequency The backup frequency (in ms ; MUST BE positive and different from 0. + * If negative or 0, the frequency will be automatically set to DEFAULT_FREQUENCY). + * + * @see DefaultUWSBackupManager#DefaultUWSBackupManager(UWS, boolean, long) + */ public DefaultTAPBackupManager(UWS uws, boolean byUser, long frequency) throws UWSException{ super(uws, byUser, frequency); } @Override protected JSONObject getJSONJob(UWSJob job, String jlName) throws UWSException, JSONException{ - JSONObject json = super.getJSONJob(job, jlName); + JSONObject jsonJob = Json4Uws.getJson(job); + + // Re-Build the parameters map, by separating the uploads and the "normal" parameters: + JSONArray uploads = new JSONArray(); + JSONObject params = new JSONObject(); + Object val; + for(String name : job.getAdditionalParameters()){ + // get the raw value: + val = job.getAdditionalParameterValue(name); + // if no value, skip this item: + if (val == null) + continue; + // if an array, build a JSON array of strings: + else if (val.getClass().isArray()){ + JSONArray array = new JSONArray(); + for(Object o : (Object[])val){ + if (o != null && o instanceof DALIUpload) + array.put(getDALIUploadJson((DALIUpload)o)); + else if (o != null) + array.put(o.toString()); + } + params.put(name, array); + } + // if upload file: + else if (val instanceof UploadFile) + uploads.put(getUploadJson((UploadFile)val)); + // if DALIUpload: + else if (val instanceof DALIUpload) + params.put(name, getDALIUploadJson((DALIUpload)val)); + // otherwise, just put the value: + else + params.put(name, val); + } + // Deal with the execution report of the job: if (job instanceof TAPJob && ((TAPJob)job).getExecReport() != null){ TAPExecutionReport execReport = ((TAPJob)job).getExecReport(); + // Build the JSON representation of the execution report of this job: JSONObject jsonExecReport = new JSONObject(); jsonExecReport.put("success", execReport.success); - jsonExecReport.put("sql", execReport.sqlTranslation); jsonExecReport.put("uploadduration", execReport.getUploadDuration()); jsonExecReport.put("parsingduration", execReport.getParsingDuration()); - jsonExecReport.put("translationduration", execReport.getTranslationDuration()); jsonExecReport.put("executionduration", execReport.getExecutionDuration()); jsonExecReport.put("formattingduration", execReport.getFormattingDuration()); jsonExecReport.put("totalduration", execReport.getTotalDuration()); - JSONObject params = json.getJSONObject(UWSJob.PARAM_PARAMETERS); - if (params == null) - params = new JSONObject(); + // Add the execution report into the parameters list: params.put("tapexecreport", jsonExecReport); - - json.put(UWSJob.PARAM_PARAMETERS, params); } - return json; + // Add the parameters and the uploads inside the JSON representation of the job: + jsonJob.put(UWSJob.PARAM_PARAMETERS, params); + jsonJob.put("uwsUploads", uploads); + + // Add the job owner: + jsonJob.put(UWSJob.PARAM_OWNER, (job != null && job.getOwner() != null) ? job.getOwner().getID() : null); + + // Add the name of the job list owning the given job: + jsonJob.put("jobListName", jlName); + + return jsonJob; + } + + /** + * Get the JSON representation of the given {@link DALIUpload}. + * + * @param upl The DALI upload specification to serialize in JSON. + * + * @return Its JSON representation. + * + * @throws JSONException If there is an error while building the JSON object. + * + * @since 2.0 + */ + protected JSONObject getDALIUploadJson(final DALIUpload upl) throws JSONException{ + if (upl == null) + return null; + JSONObject o = new JSONObject(); + o.put("label", upl.label); + o.put("uri", upl.uri); + o.put("file", (upl.file == null ? null : upl.file.paramName)); + return o; } @Override protected void restoreOtherJobParams(JSONObject json, UWSJob job) throws UWSException{ - if (job != null && json != null && job instanceof TAPJob){ - TAPJob tapJob = (TAPJob)job; - Object obj = job.getAdditionalParameterValue("tapexecreport"); - if (obj != null){ - if (obj instanceof JSONObject){ - JSONObject jsonExecReport = (JSONObject)obj; - TAPExecutionReport execReport = new TAPExecutionReport(job.getJobId(), false, tapJob.getTapParams()); - String[] keys = JSONObject.getNames(jsonExecReport); - for(String key : keys){ + // 0. Nothing to do in this function if the job is missing OR if it is not an instance of TAPJob: + if (job == null || !(job instanceof TAPJob)) + return; + + // 1. Build correctly the TAP UPLOAD parameter (the value of this parameter should be an array of DALIUpload): + if (json != null && json.has(TAPJob.PARAM_PARAMETERS)){ + try{ + // Retrieve the whole list of parameters: + JSONObject params = json.getJSONObject(TAPJob.PARAM_PARAMETERS); + // If there is an UPLOAD parameter, convert the JSON array into a DALIUpload[] and add it to the job: + if (params.has(TAPJob.PARAM_UPLOAD)){ + // retrieve the JSON array: + JSONArray uploads = params.getJSONArray(TAPJob.PARAM_UPLOAD); + // for each item of this array, build the corresponding DALIUpload and add it into an ArrayList: + DALIUpload upl; + ArrayList lstTAPUploads = new ArrayList(); + for(int i = 0; i < uploads.length(); i++){ try{ - if (key.equalsIgnoreCase("success")) - execReport.success = jsonExecReport.getBoolean(key); - else if (key.equalsIgnoreCase("sql")) - execReport.sqlTranslation = jsonExecReport.getString(key); - else if (key.equalsIgnoreCase("uploadduration")) - execReport.setDuration(ExecutionProgression.UPLOADING, jsonExecReport.getLong(key)); - else if (key.equalsIgnoreCase("parsingduration")) - execReport.setDuration(ExecutionProgression.PARSING, jsonExecReport.getLong(key)); - else if (key.equalsIgnoreCase("translationduration")) - execReport.setDuration(ExecutionProgression.TRANSLATING, jsonExecReport.getLong(key)); - else if (key.equalsIgnoreCase("executionduration")) - execReport.setDuration(ExecutionProgression.EXECUTING_SQL, jsonExecReport.getLong(key)); - else if (key.equalsIgnoreCase("formattingduration")) - execReport.setDuration(ExecutionProgression.WRITING_RESULT, jsonExecReport.getLong(key)); - else if (key.equalsIgnoreCase("totalduration")) - execReport.setTotalDuration(jsonExecReport.getLong(key)); - else - getLogger().warning("The execution report attribute '" + key + "' of the job \"" + job.getJobId() + "\" has been ignored because unknown !"); + upl = getDALIUpload(uploads.getJSONObject(i), job); + if (upl != null) + lstTAPUploads.add(upl); }catch(JSONException je){ - getLogger().error("[restoration] Incorrect JSON format for the execution report serialization of the job \"" + job.getJobId() + "\" (attribute: \"" + key + "\") !", je); + getLogger().logUWS(LogLevel.ERROR, uploads.get(i), "RESTORATION", "Incorrect JSON format for a DALIUpload of the job \"" + job.getJobId() + "\": a JSONObject was expected!", null); } } - tapJob.setExecReport(execReport); - }else if (!(obj instanceof JSONObject)) - getLogger().warning("[restoration] Impossible to restore the execution report of the job \"" + job.getJobId() + "\" because the stored object is not a JSONObject !"); - } + // finally convert the ArrayList into a DALIUpload[] and add it inside the parameters list of the job: + job.addOrUpdateParameter(TAPJob.PARAM_UPLOAD, lstTAPUploads.toArray(new DALIUpload[lstTAPUploads.size()])); + } + }catch(JSONException ex){} + } + + // 2. Get the execution report and add it into the given job: + TAPJob tapJob = (TAPJob)job; + Object obj = job.getAdditionalParameterValue("tapexecreport"); + if (obj != null){ + if (obj instanceof JSONObject){ + JSONObject jsonExecReport = (JSONObject)obj; + TAPExecutionReport execReport = new TAPExecutionReport(job.getJobId(), false, tapJob.getTapParams()); + String[] keys = JSONObject.getNames(jsonExecReport); + for(String key : keys){ + try{ + if (key.equalsIgnoreCase("success")) + execReport.success = jsonExecReport.getBoolean(key); + else if (key.equalsIgnoreCase("uploadduration")) + execReport.setDuration(ExecutionProgression.UPLOADING, jsonExecReport.getLong(key)); + else if (key.equalsIgnoreCase("parsingduration")) + execReport.setDuration(ExecutionProgression.PARSING, jsonExecReport.getLong(key)); + else if (key.equalsIgnoreCase("executionduration")) + execReport.setDuration(ExecutionProgression.EXECUTING_ADQL, jsonExecReport.getLong(key)); + else if (key.equalsIgnoreCase("formattingduration")) + execReport.setDuration(ExecutionProgression.WRITING_RESULT, jsonExecReport.getLong(key)); + else if (key.equalsIgnoreCase("totalduration")) + execReport.setTotalDuration(jsonExecReport.getLong(key)); + else + getLogger().logUWS(LogLevel.WARNING, obj, "RESTORATION", "The execution report attribute '" + key + "' of the job \"" + job.getJobId() + "\" has been ignored because unknown!", null); + }catch(JSONException je){ + getLogger().logUWS(LogLevel.ERROR, obj, "RESTORATION", "Incorrect JSON format for the execution report serialization of the job \"" + job.getJobId() + "\" (attribute: \"" + key + "\")!", je); + } + } + tapJob.setExecReport(execReport); + }else if (!(obj instanceof JSONObject)) + getLogger().logUWS(LogLevel.WARNING, obj, "RESTORATION", "Impossible to restore the execution report of the job \"" + job.getJobId() + "\" because the stored object is not a JSONObject!", null); } } + /** + * Restore a {@link DALIUpload} from its JSON representation. + * + * @param item {@link JSONObject} representing the {@link DALIUpload} to restore. + * @param job The job which owns this upload. + * + * @return The corresponding {@link DALIUpload} or NULL, if an error occurs while converting the JSON. + * + * @since 2.0 + */ + private DALIUpload getDALIUpload(final JSONObject item, final UWSJob job){ + try{ + + // Get its label: + String label = item.getString("label"); + + // Build the DALIUpload object: + /* If the upload spec. IS A FILE, the attribute 'file' should point toward a job parameter + * being an UploadFile. If so, get it and use it to build the DALIUpload: */ + if (item.has("file")){ + Object f = job.getAdditionalParameterValue(item.getString("file")); + if (f == null || !(f instanceof UploadFile)) + getLogger().logUWS(LogLevel.ERROR, item, "RESTORATION", "Incorrect JSON format for the DALIUpload labelled \"" + label + "\" of the job \"" + job.getJobId() + "\": \"" + item.getString("file") + "\" is not pointing a job parameter representing a file!", null); + return new DALIUpload(label, (UploadFile)f); + } + /* If the upload spec. IS A URI, the attribute 'uri' should contain it + * and should be used to build the DALIUpload: */ + else if (item.has("uri")){ + try{ + return new DALIUpload(label, new URI(item.getString("uri")), uws.getFileManager()); + }catch(URISyntaxException e){ + getLogger().logUWS(LogLevel.ERROR, item, "RESTORATION", "Incorrect URI for the DALIUpload labelled \"" + label + "\" of the job \"" + job.getJobId() + "\": \"" + item.getString("uri") + "\"!", null); + } + } + /* If none of this both attribute is provided, it is an error and it is not possible to build the DALIUpload. */ + else + getLogger().logUWS(LogLevel.ERROR, item, "RESTORATION", "Incorrect JSON format for the DALIUpload labelled \"" + label + "\" of the job \"" + job.getJobId() + "\": missing attribute 'file' or 'uri'!", null); + + }catch(JSONException je){ + getLogger().logUWS(LogLevel.ERROR, item, "RESTORATION", "Incorrect JSON format for a DALIUpload of the job \"" + job.getJobId() + "\": missing attribute 'label'!", null); + } + + return null; + } } diff --git a/src/tap/config/ConfigurableServiceConnection.java b/src/tap/config/ConfigurableServiceConnection.java new file mode 100644 index 0000000..058740a --- /dev/null +++ b/src/tap/config/ConfigurableServiceConnection.java @@ -0,0 +1,1594 @@ +package tap.config; + +/* + * This file is part of TAPLibrary. + * + * TAPLibrary is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * TAPLibrary is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with TAPLibrary. If not, see . + * + * Copyright 2015 - Astronomisches Rechen Institut (ARI) + */ + +import static tap.config.TAPConfiguration.DEFAULT_ASYNC_FETCH_SIZE; +import static tap.config.TAPConfiguration.DEFAULT_DIRECTORY_PER_USER; +import static tap.config.TAPConfiguration.DEFAULT_EXECUTION_DURATION; +import static tap.config.TAPConfiguration.DEFAULT_GROUP_USER_DIRECTORIES; +import static tap.config.TAPConfiguration.DEFAULT_MAX_ASYNC_JOBS; +import static tap.config.TAPConfiguration.DEFAULT_RETENTION_PERIOD; +import static tap.config.TAPConfiguration.DEFAULT_SYNC_FETCH_SIZE; +import static tap.config.TAPConfiguration.DEFAULT_UPLOAD_MAX_FILE_SIZE; +import static tap.config.TAPConfiguration.KEY_ASYNC_FETCH_SIZE; +import static tap.config.TAPConfiguration.KEY_COORD_SYS; +import static tap.config.TAPConfiguration.KEY_DEFAULT_EXECUTION_DURATION; +import static tap.config.TAPConfiguration.KEY_DEFAULT_OUTPUT_LIMIT; +import static tap.config.TAPConfiguration.KEY_DEFAULT_RETENTION_PERIOD; +import static tap.config.TAPConfiguration.KEY_DEFAULT_UPLOAD_LIMIT; +import static tap.config.TAPConfiguration.KEY_DIRECTORY_PER_USER; +import static tap.config.TAPConfiguration.KEY_FILE_MANAGER; +import static tap.config.TAPConfiguration.KEY_FILE_ROOT_PATH; +import static tap.config.TAPConfiguration.KEY_GEOMETRIES; +import static tap.config.TAPConfiguration.KEY_GROUP_USER_DIRECTORIES; +import static tap.config.TAPConfiguration.KEY_LOG_ROTATION; +import static tap.config.TAPConfiguration.KEY_MAX_ASYNC_JOBS; +import static tap.config.TAPConfiguration.KEY_MAX_EXECUTION_DURATION; +import static tap.config.TAPConfiguration.KEY_MAX_OUTPUT_LIMIT; +import static tap.config.TAPConfiguration.KEY_MAX_RETENTION_PERIOD; +import static tap.config.TAPConfiguration.KEY_MAX_UPLOAD_LIMIT; +import static tap.config.TAPConfiguration.KEY_METADATA; +import static tap.config.TAPConfiguration.KEY_METADATA_FILE; +import static tap.config.TAPConfiguration.KEY_MIN_LOG_LEVEL; +import static tap.config.TAPConfiguration.KEY_OUTPUT_FORMATS; +import static tap.config.TAPConfiguration.KEY_PROVIDER_NAME; +import static tap.config.TAPConfiguration.KEY_SERVICE_DESCRIPTION; +import static tap.config.TAPConfiguration.KEY_SYNC_FETCH_SIZE; +import static tap.config.TAPConfiguration.KEY_TAP_FACTORY; +import static tap.config.TAPConfiguration.KEY_UDFS; +import static tap.config.TAPConfiguration.KEY_UPLOAD_ENABLED; +import static tap.config.TAPConfiguration.KEY_UPLOAD_MAX_FILE_SIZE; +import static tap.config.TAPConfiguration.KEY_USER_IDENTIFIER; +import static tap.config.TAPConfiguration.VALUE_ALL; +import static tap.config.TAPConfiguration.VALUE_ANY; +import static tap.config.TAPConfiguration.VALUE_CSV; +import static tap.config.TAPConfiguration.VALUE_DB; +import static tap.config.TAPConfiguration.VALUE_FITS; +import static tap.config.TAPConfiguration.VALUE_HTML; +import static tap.config.TAPConfiguration.VALUE_JSON; +import static tap.config.TAPConfiguration.VALUE_LOCAL; +import static tap.config.TAPConfiguration.VALUE_NONE; +import static tap.config.TAPConfiguration.VALUE_SV; +import static tap.config.TAPConfiguration.VALUE_TEXT; +import static tap.config.TAPConfiguration.VALUE_TSV; +import static tap.config.TAPConfiguration.VALUE_VOT; +import static tap.config.TAPConfiguration.VALUE_VOTABLE; +import static tap.config.TAPConfiguration.VALUE_XML; +import static tap.config.TAPConfiguration.fetchClass; +import static tap.config.TAPConfiguration.getProperty; +import static tap.config.TAPConfiguration.isClassName; +import static tap.config.TAPConfiguration.newInstance; +import static tap.config.TAPConfiguration.parseLimit; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.Properties; + +import tap.ServiceConnection; +import tap.TAPException; +import tap.TAPFactory; +import tap.db.DBConnection; +import tap.formatter.FITSFormat; +import tap.formatter.HTMLFormat; +import tap.formatter.JSONFormat; +import tap.formatter.OutputFormat; +import tap.formatter.SVFormat; +import tap.formatter.TextFormat; +import tap.formatter.VOTableFormat; +import tap.log.DefaultTAPLog; +import tap.log.TAPLog; +import tap.metadata.TAPMetadata; +import tap.metadata.TableSetParser; +import uk.ac.starlink.votable.DataFormat; +import uk.ac.starlink.votable.VOTableVersion; +import uws.UWSException; +import uws.service.UserIdentifier; +import uws.service.file.LocalUWSFileManager; +import uws.service.file.UWSFileManager; +import uws.service.log.UWSLog.LogLevel; +import adql.db.FunctionDef; +import adql.db.STCS; +import adql.parser.ParseException; +import adql.query.operand.function.UserDefinedFunction; + +/** + *

    Concrete implementation of {@link ServiceConnection}, fully parameterized with a TAP configuration file.

    + * + *

    + * Every aspects of the TAP service are configured here. This instance is also creating the {@link TAPFactory} using the + * TAP configuration file thanks to the implementation {@link ConfigurableTAPFactory}. + *

    + * + * @author Grégory Mantelet (ARI) + * @version 2.0 (04/2015) + * @since 2.0 + */ +public final class ConfigurableServiceConnection implements ServiceConnection { + + /** File manager to use in the TAP service. */ + private UWSFileManager fileManager; + + /** Object to use in the TAP service in order to log different types of messages (e.g. DEBUG, INFO, WARNING, ERROR, FATAL). */ + private TAPLog logger; + + /** Factory which can create different types of objects for the TAP service (e.g. database connection). */ + private TAPFactory tapFactory; + + /** Object gathering all metadata of this TAP service. */ + private final TAPMetadata metadata; + + /** Name of the organization/person providing the TAP service. */ + private final String providerName; + /** Description of the TAP service. */ + private final String serviceDescription; + + /** Indicate whether the TAP service is available or not. */ + private boolean isAvailable = false; // the TAP service must be disabled until the end of its connection initialization + /** Description of the available or unavailable state of the TAP service. */ + private String availability = "TAP service not yet initialized."; + + /** Maximum number of asynchronous jobs that can run simultaneously. */ + private int maxAsyncJobs = DEFAULT_MAX_ASYNC_JOBS; + + /** Array of 2 integers: resp. default and maximum execution duration. + * Both duration are expressed in milliseconds. */ + private int[] executionDuration = new int[2]; + /** Array of 2 integers: resp. default and maximum retention period. + * Both period are expressed in seconds. */ + private int[] retentionPeriod = new int[2]; + + /** List of all available output formatters. */ + private final ArrayList outputFormats; + + /** Array of 2 integers: resp. default and maximum output limit. + * Each limit is expressed in a unit specified in the array {@link #outputLimitTypes}. */ + private int[] outputLimits = new int[]{-1,-1}; + /** Array of 2 limit units: resp. unit of the default output limit and unit of the maximum output limit. */ + private LimitUnit[] outputLimitTypes = new LimitUnit[2]; + + /** Indicate whether the UPLOAD feature is enabled or not. */ + private boolean isUploadEnabled = false; + /** Array of 2 integers: resp. default and maximum upload limit. + * Each limit is expressed in a unit specified in the array {@link #uploadLimitTypes}. */ + private int[] uploadLimits = new int[]{-1,-1}; + /** Array of 2 limit units: resp. unit of the default upload limit and unit of the maximum upload limit. */ + private LimitUnit[] uploadLimitTypes = new LimitUnit[2]; + /** The maximum size of a set of uploaded files. + * This size is expressed in bytes. */ + private int maxUploadSize = DEFAULT_UPLOAD_MAX_FILE_SIZE; + + /** Array of 2 integers: resp. default and maximum fetch size. + * Both sizes are expressed in number of rows. */ + private int[] fetchSize = new int[]{DEFAULT_ASYNC_FETCH_SIZE,DEFAULT_SYNC_FETCH_SIZE}; + + /** The method to use in order to identify a TAP user. */ + private UserIdentifier userIdentifier = null; + + /** List of all allowed coordinate systems. + * If NULL, all coord. sys. are allowed. If empty list, none is allowed. */ + private ArrayList lstCoordSys = null; + + /** List of all allowed ADQL geometrical functions. + * If NULL, all geometries are allowed. If empty list, none is allowed. */ + private ArrayList geometries = null; + private final String GEOMETRY_REGEXP = "(AREA|BOX|CENTROID|CIRCLE|CONTAINS|DISTANCE|COORD1|COORD2|COORDSYS|INTERSECTS|POINT|POLYGON|REGION)"; + + /** List of all known and allowed User Defined Functions. + * If NULL, any unknown function is allowed. If empty list, none is allowed. */ + private Collection udfs = new ArrayList(0); + + /** + * Create a TAP service description thanks to the given TAP configuration file. + * + * @param tapConfig The content of the TAP configuration file. + * + * @throws NullPointerException If the given properties set is NULL. + * @throws TAPException If a property is wrong or missing. + */ + public ConfigurableServiceConnection(final Properties tapConfig) throws NullPointerException, TAPException{ + this(tapConfig, null); + } + + /** + * Create a TAP service description thanks to the given TAP configuration file. + * + * @param tapConfig The content of the TAP configuration file. + * @param webAppRootDir The directory of the Web Application running this TAP service. + * In this directory another directory may be created in order to store all TAP service files + * if none is specified in the given TAP configuration file. + * + * @throws NullPointerException If the given properties set is NULL. + * @throws TAPException If a property is wrong or missing. + */ + public ConfigurableServiceConnection(final Properties tapConfig, final String webAppRootDir) throws NullPointerException, TAPException{ + if (tapConfig == null) + throw new NullPointerException("Missing TAP properties! "); + + // 1. INITIALIZE THE FILE MANAGER: + initFileManager(tapConfig, webAppRootDir); + + // 2. CREATE THE LOGGER: + initLogger(tapConfig); + + // 3. BUILD THE TAP FACTORY: + initFactory(tapConfig); + + // 4. GET THE METADATA: + metadata = initMetadata(tapConfig, webAppRootDir); + + // 5. SET ALL GENERAL SERVICE CONNECTION INFORMATION: + providerName = getProperty(tapConfig, KEY_PROVIDER_NAME); + serviceDescription = getProperty(tapConfig, KEY_SERVICE_DESCRIPTION); + initMaxAsyncJobs(tapConfig); + initRetentionPeriod(tapConfig); + initExecutionDuration(tapConfig); + + // 6. CONFIGURE OUTPUT: + // default output format = VOTable: + outputFormats = new ArrayList(1); + // set output formats: + addOutputFormats(tapConfig); + // set output limits: + initOutputLimits(tapConfig); + // set fetch size: + initFetchSize(tapConfig); + + // 7. CONFIGURE THE UPLOAD: + // is upload enabled ? + isUploadEnabled = Boolean.parseBoolean(getProperty(tapConfig, KEY_UPLOAD_ENABLED)); + // set upload limits: + initUploadLimits(tapConfig); + // set the maximum upload file size: + initMaxUploadSize(tapConfig); + + // 8. SET A USER IDENTIFIER: + initUserIdentifier(tapConfig); + + // 9. CONFIGURE ADQL: + initCoordSys(tapConfig); + initADQLGeometries(tapConfig); + initUDFs(tapConfig); + } + + /** + * Initialize the management of TAP service files using the given TAP configuration file. + * + * @param tapConfig The content of the TAP configuration file. + * @param webAppRootDir The directory of the Web Application running this TAP service. + * This directory may be used only to search the root TAP directory + * if specified with a relative path in the TAP configuration file. + * + * @throws TAPException If a property is wrong or missing, or if an error occurs while creating the file manager. + */ + private void initFileManager(final Properties tapConfig, final String webAppRootDir) throws TAPException{ + // Read the desired file manager: + String fileManagerType = getProperty(tapConfig, KEY_FILE_MANAGER); + if (fileManagerType == null) + throw new TAPException("The property \"" + KEY_FILE_MANAGER + "\" is missing! It is required to create a TAP Service. Two possible values: " + VALUE_LOCAL + " or a class name between {...}."); + else + fileManagerType = fileManagerType.trim(); + + // LOCAL file manager: + if (fileManagerType.equalsIgnoreCase(VALUE_LOCAL)){ + // Read the desired root path: + String rootPath = getProperty(tapConfig, KEY_FILE_ROOT_PATH); + if (rootPath == null) + throw new TAPException("The property \"" + KEY_FILE_ROOT_PATH + "\" is missing! It is required to create a TAP Service. Please provide a path toward a directory which will contain all files related to the service."); + File rootFile = getFile(rootPath, webAppRootDir, KEY_FILE_ROOT_PATH); + + // Determine whether there should be one directory for each user: + String propValue = getProperty(tapConfig, KEY_DIRECTORY_PER_USER); + boolean oneDirectoryPerUser = (propValue == null) ? DEFAULT_DIRECTORY_PER_USER : Boolean.parseBoolean(propValue); + + // Determine whether there should be one directory for each user: + propValue = getProperty(tapConfig, KEY_GROUP_USER_DIRECTORIES); + boolean groupUserDirectories = (propValue == null) ? DEFAULT_GROUP_USER_DIRECTORIES : Boolean.parseBoolean(propValue); + + // Build the Local TAP File Manager: + try{ + fileManager = new LocalUWSFileManager(rootFile, oneDirectoryPerUser, groupUserDirectories); + }catch(UWSException e){ + throw new TAPException("The property \"" + KEY_FILE_ROOT_PATH + "\" (" + rootPath + ") is incorrect: " + e.getMessage()); + } + } + // CUSTOM file manager: + else + fileManager = newInstance(fileManagerType, KEY_FILE_MANAGER, UWSFileManager.class, new Class[]{Properties.class}, new Object[]{tapConfig}); + } + + /** + *

    Resolve the given file name/path.

    + * + *

    Only the URI protocol "file:" is allowed. If the protocol is different a {@link TAPException} is thrown.

    + * + *

    + * If not an absolute URI, the given path may be either relative or absolute. A relative path is always considered + * as relative from the Web Application directory (supposed to be given in 2nd parameter). + *

    + * + * @param filePath URI/Path/Name of the file to get. + * @param webAppRootPath Web Application directory local path. + * @param propertyName Name of the property which gives the given file path. + * + * @return The specified File instance. + * + * @throws TAPException If the given URI is malformed or if the used URI scheme is different from "file:". + */ + protected static final File getFile(final String filePath, final String webAppRootPath, final String propertyName) throws TAPException{ + if (filePath == null) + return null; + + try{ + URI uri = new URI(filePath); + if (uri.isAbsolute()){ + if (uri.getScheme().equalsIgnoreCase("file")) + return new File(uri); + else + throw new TAPException("Incorrect file URI for the property \"" + propertyName + "\": \"" + filePath + "\"! Only URI with the protocol \"file:\" are allowed."); + }else{ + File f = new File(filePath); + if (f.isAbsolute()) + return f; + else + return new File(webAppRootPath, filePath); + } + }catch(URISyntaxException use){ + throw new TAPException("Incorrect file URI for the property \"" + propertyName + "\": \"" + filePath + "\"! Bad syntax for the given file URI.", use); + } + } + + /** + * Initialize the TAP logger with the given TAP configuration file. + * + * @param tapConfig The content of the TAP configuration file. + */ + private void initLogger(final Properties tapConfig){ + // Create the logger: + logger = new DefaultTAPLog(fileManager); + + StringBuffer buf = new StringBuffer("Logger initialized"); + + // Set the minimum log level: + String propValue = getProperty(tapConfig, KEY_MIN_LOG_LEVEL); + if (propValue != null){ + try{ + ((DefaultTAPLog)logger).setMinLogLevel(LogLevel.valueOf(propValue.toUpperCase())); + }catch(IllegalArgumentException iae){} + } + buf.append(" (minimum log level: ").append(((DefaultTAPLog)logger).getMinLogLevel()); + + // Set the log rotation period, if any: + if (fileManager instanceof LocalUWSFileManager){ + propValue = getProperty(tapConfig, KEY_LOG_ROTATION); + if (propValue != null) + ((LocalUWSFileManager)fileManager).setLogRotationFreq(propValue); + buf.append(", log rotation: ").append(((LocalUWSFileManager)fileManager).getLogRotationFreq()); + } + + // Log the successful initialization with set parameters: + buf.append(")."); + logger.info(buf.toString()); + } + + /** + *

    Initialize the {@link TAPFactory} to use.

    + * + *

    + * The built factory is either a {@link ConfigurableTAPFactory} instance (by default) or + * an instance of the class specified in the TAP configuration file. + *

    + * + * @param tapConfig The content of the TAP configuration file. + * + * @throws TAPException If an error occurs while building the specified {@link TAPFactory}. + * + * @see ConfigurableTAPFactory + */ + private void initFactory(final Properties tapConfig) throws TAPException{ + String propValue = getProperty(tapConfig, KEY_TAP_FACTORY); + if (propValue == null) + tapFactory = new ConfigurableTAPFactory(this, tapConfig); + else + tapFactory = newInstance(propValue, KEY_TAP_FACTORY, TAPFactory.class, new Class[]{ServiceConnection.class}, new Object[]{this}); + } + + /** + * Initialize the TAP metadata (i.e. database schemas, tables and columns and their attached metadata). + * + * @param tapConfig The content of the TAP configuration file. + * @param webAppRootDir Web Application directory local path. + * This directory may be used if a relative path is given for an XML metadata file. + * + * @return The extracted TAP metadata. + * + * @throws TAPException If some TAP configuration file properties are wrong or missing, + * or if an error has occurred while extracting the metadata from the database or the XML file. + * + * @see DBConnection#getTAPSchema() + * @see TableSetParser + */ + private TAPMetadata initMetadata(final Properties tapConfig, final String webAppRootDir) throws TAPException{ + // Get the fetching method to use: + String metaFetchType = getProperty(tapConfig, KEY_METADATA); + if (metaFetchType == null) + throw new TAPException("The property \"" + KEY_METADATA + "\" is missing! It is required to create a TAP Service. Three possible values: " + VALUE_XML + " (to get metadata from a TableSet XML document), " + VALUE_DB + " (to fetch metadata from the database schema TAP_SCHEMA) or the name (between {}) of a class extending TAPMetadata."); + + TAPMetadata metadata = null; + + // GET METADATA FROM XML & UPDATE THE DATABASE (schema TAP_SCHEMA only): + if (metaFetchType.equalsIgnoreCase(VALUE_XML)){ + // Get the XML file path: + String xmlFilePath = getProperty(tapConfig, KEY_METADATA_FILE); + if (xmlFilePath == null) + throw new TAPException("The property \"" + KEY_METADATA_FILE + "\" is missing! According to the property \"" + KEY_METADATA + "\", metadata must be fetched from an XML document. The local file path of it MUST be provided using the property \"" + KEY_METADATA_FILE + "\"."); + + // Parse the XML document and build the corresponding metadata: + try{ + metadata = (new TableSetParser()).parse(getFile(xmlFilePath, webAppRootDir, KEY_METADATA_FILE)); + }catch(IOException ioe){ + throw new TAPException("A grave error occurred while reading/parsing the TableSet XML document: \"" + xmlFilePath + "\"!", ioe); + } + + // Update the database: + DBConnection conn = null; + try{ + conn = tapFactory.getConnection("SET_TAP_SCHEMA"); + conn.setTAPSchema(metadata); + }finally{ + if (conn != null) + tapFactory.freeConnection(conn); + } + } + // GET METADATA FROM DATABASE (schema TAP_SCHEMA): + else if (metaFetchType.equalsIgnoreCase(VALUE_DB)){ + DBConnection conn = null; + try{ + conn = tapFactory.getConnection("GET_TAP_SCHEMA"); + metadata = conn.getTAPSchema(); + }finally{ + if (conn != null) + tapFactory.freeConnection(conn); + } + } + // MANUAL ~ TAPMETADATA CLASS + else if (isClassName(metaFetchType)){ + /* 1. Get the metadata */ + // get the class: + Class metaClass = fetchClass(metaFetchType, KEY_METADATA, TAPMetadata.class); + if (metaClass == TAPMetadata.class) + throw new TAPException("Wrong class for the property \"" + KEY_METADATA + "\": \"" + metaClass.getName() + "\"! The class provided in this property MUST EXTEND tap.metadata.TAPMetadata."); + try{ + // get one of the expected constructors: + try{ + // (UWSFileManager, TAPFactory, TAPLog): + Constructor constructor = metaClass.getConstructor(UWSFileManager.class, TAPFactory.class, TAPLog.class); + // create the TAP metadata: + metadata = constructor.newInstance(fileManager, tapFactory, logger); + }catch(NoSuchMethodException nsme){ + // () (empty constructor): + Constructor constructor = metaClass.getConstructor(); + // create the TAP metadata: + metadata = constructor.newInstance(); + } + }catch(NoSuchMethodException nsme){ + throw new TAPException("Missing constructor tap.metadata.TAPMetadata() or tap.metadata.TAPMetadata(uws.service.file.UWSFileManager, tap.TAPFactory, tap.log.TAPLog)! See the value \"" + metaFetchType + "\" of the property \"" + KEY_METADATA + "\"."); + }catch(InstantiationException ie){ + throw new TAPException("Impossible to create an instance of an abstract class: \"" + metaClass.getName() + "\"! See the value \"" + metaFetchType + "\" of the property \"" + KEY_METADATA + "\"."); + }catch(InvocationTargetException ite){ + if (ite.getCause() != null){ + if (ite.getCause() instanceof TAPException) + throw (TAPException)ite.getCause(); + else + throw new TAPException(ite.getCause()); + }else + throw new TAPException(ite); + }catch(Exception ex){ + throw new TAPException("Impossible to create an instance of tap.metadata.TAPMetadata as specified in the property \"" + KEY_METADATA + "\": \"" + metaFetchType + "\"!", ex); + } + + /* 2. Update the database */ + DBConnection conn = null; + try{ + conn = tapFactory.getConnection("SET_TAP_SCHEMA"); + conn.setTAPSchema(metadata); + }finally{ + if (conn != null) + tapFactory.freeConnection(conn); + } + } + // INCORRECT VALUE => ERROR! + else + throw new TAPException("Unsupported value for the property \"" + KEY_METADATA + "\": \"" + metaFetchType + "\"! Only two values are allowed: " + VALUE_XML + " (to get metadata from a TableSet XML document) or " + VALUE_DB + " (to fetch metadata from the database schema TAP_SCHEMA)."); + + return metadata; + } + + /** + * Initialize the maximum number of asynchronous jobs. + * + * @param tapConfig The content of the TAP configuration file. + * + * @throws TAPException If the corresponding TAP configuration property is wrong. + */ + private void initMaxAsyncJobs(final Properties tapConfig) throws TAPException{ + // Get the property value: + String propValue = getProperty(tapConfig, KEY_MAX_ASYNC_JOBS); + try{ + // If a value is provided, cast it into an integer and set the attribute: + maxAsyncJobs = (propValue == null) ? DEFAULT_MAX_ASYNC_JOBS : Integer.parseInt(propValue); + }catch(NumberFormatException nfe){ + throw new TAPException("Integer expected for the property \"" + KEY_MAX_ASYNC_JOBS + "\", instead of: \"" + propValue + "\"!"); + } + } + + /** + * Initialize the default and maximum retention period. + * + * @param tapConfig The content of the TAP configuration file. + * + * @throws TAPException If the corresponding TAP configuration properties are wrong. + */ + private void initRetentionPeriod(final Properties tapConfig) throws TAPException{ + retentionPeriod = new int[2]; + + // Set the default period: + String propValue = getProperty(tapConfig, KEY_DEFAULT_RETENTION_PERIOD); + try{ + retentionPeriod[0] = (propValue == null) ? DEFAULT_RETENTION_PERIOD : Integer.parseInt(propValue); + }catch(NumberFormatException nfe){ + throw new TAPException("Integer expected for the property \"" + KEY_DEFAULT_RETENTION_PERIOD + "\", instead of: \"" + propValue + "\"!"); + } + + // Set the maximum period: + propValue = getProperty(tapConfig, KEY_MAX_RETENTION_PERIOD); + try{ + retentionPeriod[1] = (propValue == null) ? DEFAULT_RETENTION_PERIOD : Integer.parseInt(propValue); + }catch(NumberFormatException nfe){ + throw new TAPException("Integer expected for the property \"" + KEY_MAX_RETENTION_PERIOD + "\", instead of: \"" + propValue + "\"!"); + } + + // The maximum period MUST be greater or equals than the default period. + // If not, the default period is set (so decreased) to the maximum period. + if (retentionPeriod[1] > 0 && retentionPeriod[1] < retentionPeriod[0]) + retentionPeriod[0] = retentionPeriod[1]; + } + + /** + * Initialize the default and maximum execution duration. + * + * @param tapConfig The content of the TAP configuration file. + * + * @throws TAPException If the corresponding TAP configuration properties are wrong. + */ + private void initExecutionDuration(final Properties tapConfig) throws TAPException{ + executionDuration = new int[2]; + + // Set the default duration: + String propValue = getProperty(tapConfig, KEY_DEFAULT_EXECUTION_DURATION); + try{ + executionDuration[0] = (propValue == null) ? DEFAULT_EXECUTION_DURATION : Integer.parseInt(propValue); + }catch(NumberFormatException nfe){ + throw new TAPException("Integer expected for the property \"" + KEY_DEFAULT_EXECUTION_DURATION + "\", instead of: \"" + propValue + "\"!"); + } + + // Set the maximum duration: + propValue = getProperty(tapConfig, KEY_MAX_EXECUTION_DURATION); + try{ + executionDuration[1] = (propValue == null) ? DEFAULT_EXECUTION_DURATION : Integer.parseInt(propValue); + }catch(NumberFormatException nfe){ + throw new TAPException("Integer expected for the property \"" + KEY_MAX_EXECUTION_DURATION + "\", instead of: \"" + propValue + "\"!"); + } + + // The maximum duration MUST be greater or equals than the default duration. + // If not, the default duration is set (so decreased) to the maximum duration. + if (executionDuration[1] > 0 && executionDuration[1] < executionDuration[0]) + executionDuration[0] = executionDuration[1]; + } + + /** + *

    Initialize the list of all output format that the TAP service must support.

    + * + *

    + * This function ensures that at least one VOTable format is part of the returned list, + * even if none has been specified in the TAP configuration file. Indeed, the VOTable format is the only + * format required for a TAP service. + *

    + * + * @param tapConfig The content of the TAP configuration file. + * + * @throws TAPException If the corresponding TAP configuration properties are wrong. + */ + private void addOutputFormats(final Properties tapConfig) throws TAPException{ + // Fetch the value of the property for additional output formats: + String formats = getProperty(tapConfig, KEY_OUTPUT_FORMATS); + + // SPECIAL VALUE "ALL": + if (formats == null || formats.equalsIgnoreCase(VALUE_ALL)){ + outputFormats.add(new VOTableFormat(this, DataFormat.BINARY)); + outputFormats.add(new VOTableFormat(this, DataFormat.BINARY2)); + outputFormats.add(new VOTableFormat(this, DataFormat.TABLEDATA)); + outputFormats.add(new VOTableFormat(this, DataFormat.FITS)); + outputFormats.add(new FITSFormat(this)); + outputFormats.add(new JSONFormat(this)); + outputFormats.add(new SVFormat(this, ",", true)); + outputFormats.add(new SVFormat(this, "\t", true)); + outputFormats.add(new TextFormat(this)); + outputFormats.add(new HTMLFormat(this)); + return; + } + + // LIST OF FORMATS: + // Since it is a comma separated list of output formats, a loop will parse this list comma by comma: + String f; + int indexSep, indexLPar, indexRPar; + boolean hasVotableFormat = false; + while(formats != null && formats.length() > 0){ + // Get a format item from the list: + indexSep = formats.indexOf(','); + // if a comma is after a left parenthesis + indexLPar = formats.indexOf('('); + if (indexSep > 0 && indexLPar > 0 && indexSep > indexLPar){ + indexRPar = formats.indexOf(')', indexLPar); + if (indexRPar > 0) + indexSep = formats.indexOf(',', indexRPar); + else + throw new TAPException("Missing right parenthesis in: \"" + formats + "\"!"); + } + // no comma => only one format + if (indexSep < 0){ + f = formats; + formats = null; + } + // comma at the first position => empty list item => go to the next item + else if (indexSep == 0){ + formats = formats.substring(1).trim(); + continue; + } + // else => get the first format item, and then remove it from the list for the next iteration + else{ + f = formats.substring(0, indexSep).trim(); + formats = formats.substring(indexSep + 1).trim(); + } + + // Identify the format and append it to the output format list of the service: + // FITS + if (f.equalsIgnoreCase(VALUE_FITS)) + outputFormats.add(new FITSFormat(this)); + // JSON + else if (f.equalsIgnoreCase(VALUE_JSON)) + outputFormats.add(new JSONFormat(this)); + // HTML + else if (f.equalsIgnoreCase(VALUE_HTML)) + outputFormats.add(new HTMLFormat(this)); + // TEXT + else if (f.equalsIgnoreCase(VALUE_TEXT)) + outputFormats.add(new TextFormat(this)); + // CSV + else if (f.equalsIgnoreCase(VALUE_CSV)) + outputFormats.add(new SVFormat(this, ",", true)); + // TSV + else if (f.equalsIgnoreCase(VALUE_TSV)) + outputFormats.add(new SVFormat(this, "\t", true)); + // any SV (separated value) format + else if (f.toLowerCase().startsWith(VALUE_SV)){ + // get the separator: + int endSep = f.indexOf(')'); + if (VALUE_SV.length() < f.length() && f.charAt(VALUE_SV.length()) == '(' && endSep > VALUE_SV.length() + 1){ + String separator = f.substring(VALUE_SV.length() + 1, f.length() - 1); + // get the MIME type and its alias, if any of them is provided: + String mimeType = null, shortMimeType = null; + if (endSep + 1 < f.length() && f.charAt(endSep + 1) == ':'){ + int endMime = f.indexOf(':', endSep + 2); + if (endMime < 0) + mimeType = f.substring(endSep + 2, f.length()); + else if (endMime > 0){ + mimeType = f.substring(endSep + 2, endMime); + shortMimeType = f.substring(endMime + 1); + } + } + // add the defined SV(...) format: + outputFormats.add(new SVFormat(this, separator, true, mimeType, shortMimeType)); + }else + throw new TAPException("Missing separator char/string for the SV output format: \"" + f + "\"!"); + } + // VOTABLE + else if (f.toLowerCase().startsWith(VALUE_VOTABLE) || f.toLowerCase().startsWith(VALUE_VOT)){ + // Parse the format: + VOTableFormat votFormat = parseVOTableFormat(f); + + // Add the VOTable format: + outputFormats.add(votFormat); + + // Determine whether the MIME type is the VOTable expected one: + if (votFormat.getShortMimeType().equals("votable") || votFormat.getMimeType().equals("votable")) + hasVotableFormat = true; + } + // custom OutputFormat + else if (isClassName(f)) + outputFormats.add(TAPConfiguration.newInstance(f, KEY_OUTPUT_FORMATS, OutputFormat.class, new Class[]{ServiceConnection.class}, new Object[]{this})); + // unknown format + else + throw new TAPException("Unknown output format: " + f); + } + + // Add by default VOTable format if none is specified: + if (!hasVotableFormat) + outputFormats.add(new VOTableFormat(this)); + } + + /** + *

    Parse the given VOTable format specification.

    + * + *

    This specification is expected to be an item of the property {@link TAPConfiguration#KEY_OUTPUT_FORMATS}.

    + * + * @param propValue A single VOTable format specification. + * + * @return The corresponding configured {@link VOTableFormat} instance. + * + * @throws TAPException If the syntax of the given specification is incorrect, + * or if the specified VOTable version or serialization does not exist. + */ + private VOTableFormat parseVOTableFormat(final String propValue) throws TAPException{ + DataFormat serialization = null; + VOTableVersion votVersion = null; + String mimeType = null, shortMimeType = null; + + // Get the parameters, if any: + int beginSep = propValue.indexOf('('); + if (beginSep > 0){ + int endSep = propValue.indexOf(')'); + if (endSep <= beginSep) + throw new TAPException("Wrong output format specification syntax in: \"" + propValue + "\"! A VOTable parameters list must end with ')'."); + // split the parameters: + String[] params = propValue.substring(beginSep + 1, endSep).split(","); + if (params.length > 2) + throw new TAPException("Wrong number of parameters for the output format VOTable: \"" + propValue + "\"! Only two parameters may be provided: serialization and version."); + else if (params.length >= 1){ + // resolve the serialization format: + params[0] = params[0].trim().toLowerCase(); + if (params[0].length() == 0 || params[0].equals("b") || params[0].equals("binary")) + serialization = DataFormat.BINARY; + else if (params[0].equals("b2") || params[0].equals("binary2")) + serialization = DataFormat.BINARY2; + else if (params[0].equals("td") || params[0].equals("tabledata")) + serialization = DataFormat.TABLEDATA; + else if (params[0].equals("fits")) + serialization = DataFormat.FITS; + else + throw new TAPException("Unsupported VOTable serialization: \"" + params[0] + "\"! Accepted values: 'binary' (or 'b'), 'binary2' (or 'b2'), 'tabledata' (or 'td') and 'fits'."); + // resolve the version: + if (params.length == 2){ + params[1] = params[1].trim(); + if (params[1].equals("1.0") || params[1].equalsIgnoreCase("v1.0")) + votVersion = VOTableVersion.V10; + else if (params[1].equals("1.1") || params[1].equalsIgnoreCase("v1.1")) + votVersion = VOTableVersion.V11; + else if (params[1].equals("1.2") || params[1].equalsIgnoreCase("v1.2")) + votVersion = VOTableVersion.V12; + else if (params[1].equals("1.3") || params[1].equalsIgnoreCase("v1.3")) + votVersion = VOTableVersion.V13; + else + throw new TAPException("Unsupported VOTable version: \"" + params[1] + "\"! Accepted values: '1.0' (or 'v1.0'), '1.1' (or 'v1.1'), '1.2' (or 'v1.2') and '1.3' (or 'v1.3')."); + } + } + } + + // Get the MIME type and its alias, if any: + beginSep = propValue.indexOf(':'); + if (beginSep > 0){ + int endSep = propValue.indexOf(':', beginSep + 1); + if (endSep < 0) + endSep = propValue.length(); + // extract the MIME type, if any: + mimeType = propValue.substring(beginSep + 1, endSep).trim(); + if (mimeType.length() == 0) + mimeType = null; + // extract the short MIME type, if any: + if (endSep < propValue.length()){ + beginSep = endSep; + endSep = propValue.indexOf(':', beginSep + 1); + if (endSep >= 0) + throw new TAPException("Wrong output format specification syntax in: \"" + propValue + "\"! After a MIME type and a short MIME type, no more information is expected."); + else + endSep = propValue.length(); + shortMimeType = propValue.substring(beginSep + 1, endSep).trim(); + if (shortMimeType.length() == 0) + shortMimeType = null; + } + } + + // Create the VOTable format: + VOTableFormat votFormat = new VOTableFormat(this, serialization, votVersion); + votFormat.setMimeType(mimeType, shortMimeType); + + return votFormat; + } + + /** + * Initialize the default and maximum output limits. + * + * @param tapConfig The content of the TAP configuration file. + * + * @throws TAPException If the corresponding TAP configuration properties are wrong. + */ + private void initOutputLimits(final Properties tapConfig) throws TAPException{ + Object[] limit = parseLimit(getProperty(tapConfig, KEY_DEFAULT_OUTPUT_LIMIT), KEY_DEFAULT_OUTPUT_LIMIT, false); + outputLimitTypes[0] = (LimitUnit)limit[1]; // it should be "rows" since the parameter areBytesAllowed of parseLimit =false + setDefaultOutputLimit((Integer)limit[0]); + + limit = parseLimit(getProperty(tapConfig, KEY_MAX_OUTPUT_LIMIT), KEY_DEFAULT_OUTPUT_LIMIT, false); + outputLimitTypes[1] = (LimitUnit)limit[1]; // it should be "rows" since the parameter areBytesAllowed of parseLimit =false + setMaxOutputLimit((Integer)limit[0]); + } + + /** + * Initialize the fetch size for the synchronous and for the asynchronous resources. + * + * @param tapConfig The content of the TAP configuration file. + * + * @throws TAPException If the corresponding TAP configuration properties are wrong. + */ + private void initFetchSize(final Properties tapConfig) throws TAPException{ + fetchSize = new int[2]; + + // Set the fetch size for asynchronous queries: + String propVal = getProperty(tapConfig, KEY_ASYNC_FETCH_SIZE); + if (propVal == null) + fetchSize[0] = DEFAULT_ASYNC_FETCH_SIZE; + else{ + try{ + fetchSize[0] = Integer.parseInt(propVal); + if (fetchSize[0] < 0) + fetchSize[0] = 0; + }catch(NumberFormatException nfe){ + throw new TAPException("Integer expected for the property " + KEY_ASYNC_FETCH_SIZE + ": \"" + propVal + "\"!"); + } + } + + // Set the fetch size for synchronous queries: + propVal = getProperty(tapConfig, KEY_SYNC_FETCH_SIZE); + if (propVal == null) + fetchSize[1] = DEFAULT_SYNC_FETCH_SIZE; + else{ + try{ + fetchSize[1] = Integer.parseInt(propVal); + if (fetchSize[1] < 0) + fetchSize[1] = 0; + }catch(NumberFormatException nfe){ + throw new TAPException("Integer expected for the property " + KEY_SYNC_FETCH_SIZE + ": \"" + propVal + "\"!"); + } + } + } + + /** + * Initialize the default and maximum upload limits. + * + * @param tapConfig The content of the TAP configuration file. + * + * @throws TAPException If the corresponding TAP configuration properties are wrong. + */ + private void initUploadLimits(final Properties tapConfig) throws TAPException{ + Object[] limit = parseLimit(getProperty(tapConfig, KEY_DEFAULT_UPLOAD_LIMIT), KEY_DEFAULT_UPLOAD_LIMIT, true); + uploadLimitTypes[0] = (LimitUnit)limit[1]; + setDefaultUploadLimit((Integer)limit[0]); + + limit = parseLimit(getProperty(tapConfig, KEY_MAX_UPLOAD_LIMIT), KEY_MAX_UPLOAD_LIMIT, true); + if (!((LimitUnit)limit[1]).isCompatibleWith(uploadLimitTypes[0])) + throw new TAPException("The default upload limit (in " + uploadLimitTypes[0] + ") and the maximum upload limit (in " + limit[1] + ") MUST be expressed in the same unit!"); + else + uploadLimitTypes[1] = (LimitUnit)limit[1]; + setMaxUploadLimit((Integer)limit[0]); + } + + /** + * Initialize the maximum size (in bytes) of a VOTable files set upload. + * + * @param tapConfig The content of the TAP configuration file. + * + * @throws TAPException If the corresponding TAP configuration property is wrong. + */ + private void initMaxUploadSize(final Properties tapConfig) throws TAPException{ + String propValue = getProperty(tapConfig, KEY_UPLOAD_MAX_FILE_SIZE); + // If a value is specified... + if (propValue != null){ + // ...parse the value: + Object[] limit = parseLimit(propValue, KEY_UPLOAD_MAX_FILE_SIZE, true); + if (((Integer)limit[0]).intValue() <= 0) + limit[0] = new Integer(TAPConfiguration.DEFAULT_UPLOAD_MAX_FILE_SIZE); + // ...check that the unit is correct (bytes): + if (!LimitUnit.bytes.isCompatibleWith((LimitUnit)limit[1])) + throw new TAPException("The maximum upload file size " + KEY_UPLOAD_MAX_FILE_SIZE + " (here: " + propValue + ") can not be expressed in a unit different from bytes (B, kB, MB, GB)!"); + // ...set the max file size: + int value = (int)((Integer)limit[0] * ((LimitUnit)limit[1]).bytesFactor()); + setMaxUploadSize(value); + } + } + + /** + * Initialize the TAP user identification method. + * + * @param tapConfig The content of the TAP configuration file. + * + * @throws TAPException If the corresponding TAP configuration property is wrong. + */ + private void initUserIdentifier(final Properties tapConfig) throws TAPException{ + // Get the property value: + String propValue = getProperty(tapConfig, KEY_USER_IDENTIFIER); + if (propValue != null) + userIdentifier = newInstance(propValue, KEY_USER_IDENTIFIER, UserIdentifier.class); + } + + /** + * Initialize the list of all allowed coordinate systems. + * + * @param tapConfig The content of the TAP configuration file. + * + * @throws TAPException If the corresponding TAP configuration properties are wrong. + */ + private void initCoordSys(final Properties tapConfig) throws TAPException{ + // Get the property value: + String propValue = getProperty(tapConfig, KEY_COORD_SYS); + + // NO VALUE => ALL COORD SYS ALLOWED! + if (propValue == null) + lstCoordSys = null; + + // "NONE" => ALL COORD SYS FORBIDDEN (= no coordinate system expression is allowed)! + else if (propValue.equalsIgnoreCase(VALUE_NONE)) + lstCoordSys = new ArrayList(0); + + // "ANY" => ALL COORD SYS ALLOWED (= any coordinate system is allowed)! + else if (propValue.equalsIgnoreCase(VALUE_ANY)) + lstCoordSys = null; + + // OTHERWISE, JUST THE ALLOWED ONE ARE LISTED: + else{ + // split all the list items: + String[] items = propValue.split(","); + if (items.length > 0){ + lstCoordSys = new ArrayList(items.length); + for(String item : items){ + item = item.trim(); + // empty item => ignored + if (item.length() <= 0) + continue; + // "NONE" is not allowed inside a list => error! + else if (item.toUpperCase().equals(VALUE_NONE)) + throw new TAPException("The special value \"" + VALUE_NONE + "\" can not be used inside a list! It MUST be used in replacement of a whole list to specify that no value is allowed."); + // "ANY" is not allowed inside a list => error! + else if (item.toUpperCase().equals(VALUE_ANY)) + throw new TAPException("The special value \"" + VALUE_ANY + "\" can not be used inside a list! It MUST be used in replacement of a whole list to specify that any value is allowed."); + // parse the coordinate system regular expression in order to check it: + else{ + try{ + STCS.buildCoordSysRegExp(new String[]{item}); + lstCoordSys.add(item); + }catch(ParseException pe){ + throw new TAPException("Incorrect coordinate system regular expression (\"" + item + "\"): " + pe.getMessage(), pe); + } + } + } + // if finally no item has been specified, consider it as "any coordinate system allowed": + if (lstCoordSys.size() == 0) + lstCoordSys = null; + }else + lstCoordSys = null; + } + } + + /** + * Initialize the list of all allowed ADQL geometrical functions. + * + * @param tapConfig The content of the TAP configuration file. + * + * @throws TAPException If the corresponding TAP configuration properties are wrong. + */ + private void initADQLGeometries(final Properties tapConfig) throws TAPException{ + // Get the property value: + String propValue = getProperty(tapConfig, KEY_GEOMETRIES); + + // NO VALUE => ALL FCT ALLOWED! + if (propValue == null) + geometries = null; + + // "NONE" => ALL FCT FORBIDDEN (= none of these functions are allowed)! + else if (propValue.equalsIgnoreCase(VALUE_NONE)) + geometries = new ArrayList(0); + + // "ANY" => ALL FCT ALLOWED (= all of these functions are allowed)! + else if (propValue.equalsIgnoreCase(VALUE_ANY)) + geometries = null; + + // OTHERWISE, JUST THE ALLOWED ONE ARE LISTED: + else{ + // split all the list items: + String[] items = propValue.split(","); + if (items.length > 0){ + geometries = new ArrayList(items.length); + for(String item : items){ + item = item.trim(); + // empty item => ignored + if (item.length() <= 0) + continue; + // if it is a name of known ADQL geometrical function, add it to the list: + else if (item.toUpperCase().matches(GEOMETRY_REGEXP)) + geometries.add(item.toUpperCase()); + // "NONE" is not allowed inside a list => error! + else if (item.toUpperCase().equals(VALUE_NONE)) + throw new TAPException("The special value \"" + VALUE_NONE + "\" can not be used inside a list! It MUST be used in replacement of a whole list to specify that no value is allowed."); + // "ANY" is not allowed inside a list => error! + else if (item.toUpperCase().equals(VALUE_ANY)) + throw new TAPException("The special value \"" + VALUE_ANY + "\" can not be used inside a list! It MUST be used in replacement of a whole list to specify that any value is allowed."); + // unknown value => error! + else + throw new TAPException("Unknown ADQL geometrical function: \"" + item + "\"!"); + } + // if finally no item has been specified, consider it as "all functions allowed": + if (geometries.size() == 0) + geometries = null; + }else + geometries = null; + } + } + + /** + * Initialize the list of all known and allowed User Defined Functions. + * + * @param tapConfig The content of the TAP configuration file. + * + * @throws TAPException If the corresponding TAP configuration properties are wrong. + */ + private void initUDFs(final Properties tapConfig) throws TAPException{ + // Get the property value: + String propValue = getProperty(tapConfig, KEY_UDFS); + + // NO VALUE => NO UNKNOWN FCT ALLOWED! + if (propValue == null) + udfs = new ArrayList(0); + + // "NONE" => NO UNKNOWN FCT ALLOWED (= none of the unknown functions are allowed)! + else if (propValue.equalsIgnoreCase(VALUE_NONE)) + udfs = new ArrayList(0); + + // "ANY" => ALL UNKNOWN FCT ALLOWED (= all of the unknown functions are allowed)! + else if (propValue.equalsIgnoreCase(VALUE_ANY)) + udfs = null; + + // OTHERWISE, JUST THE ALLOWED ONE ARE LISTED: + else{ + + char c; + int ind = 0; + short nbComma = 0; + boolean within_item = false, within_params = false, within_classpath = false; + StringBuffer buf = new StringBuffer(); + String signature, classpath; + int[] posSignature = new int[]{-1,-1}, posClassPath = new int[]{-1,-1}; + + signature = null; + classpath = null; + buf.delete(0, buf.length()); + + while(ind < propValue.length()){ + // Get the character: + c = propValue.charAt(ind++); + // If space => ignore + if (!within_params && Character.isWhitespace(c)) + continue; + // If inside a parameters list, keep all characters until the list end (')'): + if (within_params){ + if (c == ')') + within_params = false; + buf.append(c); + } + // If inside a classpath, keep all characters until the classpath end ('}'): + else if (within_classpath){ + if (c == '}') + within_classpath = false; + buf.append(c); + } + // If inside an UDF declaration: + else if (within_item){ + switch(c){ + case '(': /* start of a parameters list */ + within_params = true; + buf.append(c); + break; + case '{': /* start of a class name */ + within_classpath = true; + buf.append(c); + break; + case ',': /* separation between the signature and the class name */ + // count commas within this item: + if (++nbComma > 1) + // if more than 1, throw an error: + throw new TAPException("Wrong UDF declaration syntax: only two items (signature and class name) can be given within brackets. (position in the property " + KEY_UDFS + ": " + ind + ")"); + else{ + // end of the signature and start of the class name: + signature = buf.toString(); + buf.delete(0, buf.length()); + posSignature[1] = ind; + posClassPath[0] = ind + 1; + } + break; + case ']': /* end of a UDF declaration */ + within_item = false; + if (nbComma == 0){ + signature = buf.toString(); + posSignature[1] = ind; + }else{ + classpath = (buf.length() == 0 ? null : buf.toString()); + if (classpath != null) + posClassPath[1] = ind; + } + buf.delete(0, buf.length()); + + // no signature... + if (signature == null || signature.length() == 0){ + // ...BUT a class name => error + if (classpath != null) + throw new TAPException("Missing UDF declaration! (position in the property " + KEY_UDFS + ": " + posSignature[0] + "-" + posSignature[1] + ")"); + // ... => ignore this item + else + continue; + } + + // add the new UDF in the list: + try{ + // resolve the function signature: + FunctionDef def = FunctionDef.parse(signature); + // resolve the class name: + if (classpath != null){ + if (isClassName(classpath)){ + Class fctClass = null; + try{ + // fetch the class: + fctClass = fetchClass(classpath, KEY_UDFS, UserDefinedFunction.class); + // set the class inside the UDF definition: + def.setUDFClass(fctClass); + }catch(TAPException te){ + throw new TAPException("Invalid class name for the UDF definition \"" + def + "\": " + te.getMessage() + " (position in the property " + KEY_UDFS + ": " + posClassPath[0] + "-" + posClassPath[1] + ")", te); + }catch(IllegalArgumentException iae){ + throw new TAPException("Invalid class name for the UDF definition \"" + def + "\": missing a constructor with a single parameter of type ADQLOperand[] " + (fctClass != null ? "in the class \"" + fctClass.getName() + "\"" : "") + "! (position in the property " + KEY_UDFS + ": " + posClassPath[0] + "-" + posClassPath[1] + ")"); + } + }else + throw new TAPException("Invalid class name for the UDF definition \"" + def + "\": \"" + classpath + "\" is not a class name (or is not surrounding by {} as expected in this property file)! (position in the property " + KEY_UDFS + ": " + posClassPath[0] + "-" + posClassPath[1] + ")"); + } + // add the UDF: + udfs.add(def); + }catch(ParseException pe){ + throw new TAPException("Wrong UDF declaration syntax: " + pe.getMessage() + " (position in the property " + KEY_UDFS + ": " + posSignature[0] + "-" + posSignature[1] + ")", pe); + } + + // reset some variables: + nbComma = 0; + signature = null; + classpath = null; + break; + default: /* keep all other characters */ + buf.append(c); + break; + } + } + // If outside of everything, just starting a UDF declaration or separate each declaration is allowed: + else{ + switch(c){ + case '[': + within_item = true; + posSignature[0] = ind + 1; + break; + case ',': + break; + default: + throw new TAPException("Wrong UDF declaration syntax: unexpected character at position " + ind + " in the property " + KEY_UDFS + ": \"" + c + "\"! A UDF declaration must have one of the following syntaxes: \"[signature]\" or \"[signature,{className}]\"."); + } + } + } + + // If the parsing is not finished, throw an error: + if (within_item) + throw new TAPException("Wrong UDF declaration syntax: missing closing bracket at position " + propValue.length() + "!"); + } + } + + @Override + public String getProviderName(){ + return providerName; + } + + @Override + public String getProviderDescription(){ + return serviceDescription; + } + + @Override + public boolean isAvailable(){ + return isAvailable; + } + + @Override + public String getAvailability(){ + return availability; + } + + @Override + public void setAvailable(boolean isAvailable, String message){ + this.isAvailable = isAvailable; + availability = message; + } + + @Override + public int[] getRetentionPeriod(){ + return retentionPeriod; + } + + /** + *

    Set the default retention period.

    + * + *

    This period is set by default if the user did not specify one before the execution of his query.

    + * + *

    Important note: + * This function will apply the given retention period only if legal compared to the currently set maximum value. + * In other words, if the given value is less or equals to the current maximum retention period. + *

    + * + * @param period New default retention period (in seconds). + * + * @return true if the given retention period has been successfully set, false otherwise. + */ + public boolean setDefaultRetentionPeriod(final int period){ + if ((retentionPeriod[1] <= 0) || (period > 0 && period <= retentionPeriod[1])){ + retentionPeriod[0] = period; + return true; + }else + return false; + } + + /** + *

    Set the maximum retention period.

    + * + *

    This period limits the default retention period and the retention period specified by a user.

    + * + *

    Important note: + * This function may reduce the default retention period if the current default retention period is bigger + * to the new maximum retention period. In a such case, the default retention period is set to the + * new maximum retention period. + *

    + * + * @param period New maximum retention period (in seconds). + */ + public void setMaxRetentionPeriod(final int period){ + // Decrease the default retention period if it will be bigger than the new maximum retention period: + if (period > 0 && (retentionPeriod[0] <= 0 || period < retentionPeriod[0])) + retentionPeriod[0] = period; + // Set the new maximum retention period: + retentionPeriod[1] = period; + } + + @Override + public int[] getExecutionDuration(){ + return executionDuration; + } + + /** + *

    Set the default execution duration.

    + * + *

    This duration is set by default if the user did not specify one before the execution of his query.

    + * + *

    Important note: + * This function will apply the given execution duration only if legal compared to the currently set maximum value. + * In other words, if the given value is less or equals to the current maximum execution duration. + *

    + * + * @param duration New default execution duration (in milliseconds). + * + * @return true if the given execution duration has been successfully set, false otherwise. + */ + public boolean setDefaultExecutionDuration(final int duration){ + if ((executionDuration[1] <= 0) || (duration > 0 && duration <= executionDuration[1])){ + executionDuration[0] = duration; + return true; + }else + return false; + } + + /** + *

    Set the maximum execution duration.

    + * + *

    This duration limits the default execution duration and the execution duration specified by a user.

    + * + *

    Important note: + * This function may reduce the default execution duration if the current default execution duration is bigger + * to the new maximum execution duration. In a such case, the default execution duration is set to the + * new maximum execution duration. + *

    + * + * @param duration New maximum execution duration (in milliseconds). + */ + public void setMaxExecutionDuration(final int duration){ + // Decrease the default execution duration if it will be bigger than the new maximum execution duration: + if (duration > 0 && (executionDuration[0] <= 0 || duration < executionDuration[0])) + executionDuration[0] = duration; + // Set the new maximum execution duration: + executionDuration[1] = duration; + } + + @Override + public Iterator getOutputFormats(){ + return outputFormats.iterator(); + } + + @Override + public OutputFormat getOutputFormat(final String mimeOrAlias){ + if (mimeOrAlias == null || mimeOrAlias.trim().isEmpty()) + return null; + + for(OutputFormat f : outputFormats){ + if ((f.getMimeType() != null && f.getMimeType().equalsIgnoreCase(mimeOrAlias)) || (f.getShortMimeType() != null && f.getShortMimeType().equalsIgnoreCase(mimeOrAlias))) + return f; + } + return null; + } + + /** + *

    Add the given {@link OutputFormat} in the list of output formats supported by the TAP service.

    + * + *

    Warning: + * No verification is done in order to avoid duplicated output formats in the list. + * NULL objects are merely ignored silently. + *

    + * + * @param newOutputFormat New output format. + */ + public void addOutputFormat(final OutputFormat newOutputFormat){ + if (newOutputFormat != null) + outputFormats.add(newOutputFormat); + } + + /** + * Remove the specified output format. + * + * @param mimeOrAlias Full or short MIME type of the output format to remove. + * + * @return true if the specified format has been found and successfully removed from the list, + * false otherwise. + */ + public boolean removeOutputFormat(final String mimeOrAlias){ + OutputFormat of = getOutputFormat(mimeOrAlias); + if (of != null) + return outputFormats.remove(of); + else + return false; + } + + @Override + public int[] getOutputLimit(){ + return outputLimits; + } + + /** + *

    Set the default output limit.

    + * + *

    This limit is set by default if the user did not specify one before the execution of his query.

    + * + *

    Important note: + * This function will apply the given output limit only if legal compared to the currently set maximum value. + * In other words, if the given value is less or equals to the current maximum output limit. + *

    + * + * @param limit New default output limit (in number of rows). + * + * @return true if the given output limit has been successfully set, false otherwise. + */ + public boolean setDefaultOutputLimit(final int limit){ + if ((outputLimits[1] <= 0) || (limit > 0 && limit <= outputLimits[1])){ + outputLimits[0] = limit; + return true; + }else + return false; + } + + /** + *

    Set the maximum output limit.

    + * + *

    This output limit limits the default output limit and the output limit specified by a user.

    + * + *

    Important note: + * This function may reduce the default output limit if the current default output limit is bigger + * to the new maximum output limit. In a such case, the default output limit is set to the + * new maximum output limit. + *

    + * + * @param limit New maximum output limit (in number of rows). + */ + public void setMaxOutputLimit(final int limit){ + // Decrease the default output limit if it will be bigger than the new maximum output limit: + if (limit > 0 && (outputLimits[0] <= 0 || limit < outputLimits[0])) + outputLimits[0] = limit; + // Set the new maximum output limit: + outputLimits[1] = limit; + } + + @Override + public final LimitUnit[] getOutputLimitType(){ + return new LimitUnit[]{LimitUnit.rows,LimitUnit.rows}; + } + + @Override + public Collection getCoordinateSystems(){ + return lstCoordSys; + } + + @Override + public TAPLog getLogger(){ + return logger; + } + + @Override + public TAPFactory getFactory(){ + return tapFactory; + } + + @Override + public UWSFileManager getFileManager(){ + return fileManager; + } + + @Override + public boolean uploadEnabled(){ + return isUploadEnabled; + } + + public void setUploadEnabled(final boolean enabled){ + isUploadEnabled = enabled; + } + + @Override + public int[] getUploadLimit(){ + return uploadLimits; + } + + @Override + public LimitUnit[] getUploadLimitType(){ + return uploadLimitTypes; + } + + /** + * Set the unit of the upload limit. + * + * @param type Unit of upload limit (rows or bytes). + */ + public void setUploadLimitType(final LimitUnit type){ + if (type != null) + uploadLimitTypes = new LimitUnit[]{type,type}; + } + + /** + *

    Set the default upload limit.

    + * + *

    Important note: + * This function will apply the given upload limit only if legal compared to the currently set maximum value. + * In other words, if the given value is less or equals to the current maximum upload limit. + *

    + * + * @param limit New default upload limit. + * + * @return true if the given upload limit has been successfully set, false otherwise. + */ + public boolean setDefaultUploadLimit(final int limit){ + try{ + if ((uploadLimits[1] <= 0) || (limit > 0 && LimitUnit.compare(limit, uploadLimitTypes[0], uploadLimits[1], uploadLimitTypes[1]) <= 0)){ + uploadLimits[0] = limit; + return true; + } + }catch(TAPException e){} + return false; + } + + /** + *

    Set the maximum upload limit.

    + * + *

    This upload limit limits the default upload limit.

    + * + *

    Important note: + * This function may reduce the default upload limit if the current default upload limit is bigger + * to the new maximum upload limit. In a such case, the default upload limit is set to the + * new maximum upload limit. + *

    + * + * @param limit New maximum upload limit. + */ + public void setMaxUploadLimit(final int limit){ + try{ + // Decrease the default output limit if it will be bigger than the new maximum output limit: + if (limit > 0 && (uploadLimits[0] <= 0 || LimitUnit.compare(limit, uploadLimitTypes[1], uploadLimits[0], uploadLimitTypes[0]) < 0)) + uploadLimits[0] = limit; + // Set the new maximum output limit: + uploadLimits[1] = limit; + }catch(TAPException e){} + } + + @Override + public int getMaxUploadSize(){ + return maxUploadSize; + } + + /** + *

    Set the maximum size of a VOTable files set that can be uploaded in once.

    + * + *

    Warning: + * This size can not be negative or 0. If the given value is in this case, nothing will be done + * and false will be returned. + * On the contrary to the other limits, no "unlimited" limit is possible here ; only the + * maximum value can be set (i.e. maximum positive integer value). + *

    + * + * @param maxSize New maximum size (in bytes). + * + * @return true if the size has been successfully set, false otherwise. + */ + public boolean setMaxUploadSize(final int maxSize){ + // No "unlimited" value possible there: + if (maxSize <= 0) + return false; + + // Otherwise, set the maximum upload file size: + maxUploadSize = maxSize; + return true; + } + + @Override + public int getNbMaxAsyncJobs(){ + return maxAsyncJobs; + } + + @Override + public UserIdentifier getUserIdentifier(){ + return userIdentifier; + } + + @Override + public TAPMetadata getTAPMetadata(){ + return metadata; + } + + @Override + public Collection getGeometries(){ + return geometries; + } + + @Override + public Collection getUDFs(){ + return udfs; + } + + @Override + public int[] getFetchSize(){ + return fetchSize; + } + +} diff --git a/src/tap/config/ConfigurableTAPFactory.java b/src/tap/config/ConfigurableTAPFactory.java new file mode 100644 index 0000000..02432f7 --- /dev/null +++ b/src/tap/config/ConfigurableTAPFactory.java @@ -0,0 +1,334 @@ +package tap.config; + +/* + * This file is part of TAPLibrary. + * + * TAPLibrary is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * TAPLibrary is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with TAPLibrary. If not, see . + * + * Copyright 2015 - Astronomisches Rechen Institut (ARI) + */ + +import static tap.config.TAPConfiguration.DEFAULT_BACKUP_BY_USER; +import static tap.config.TAPConfiguration.DEFAULT_BACKUP_FREQUENCY; +import static tap.config.TAPConfiguration.KEY_BACKUP_BY_USER; +import static tap.config.TAPConfiguration.KEY_BACKUP_FREQUENCY; +import static tap.config.TAPConfiguration.KEY_DATABASE_ACCESS; +import static tap.config.TAPConfiguration.KEY_DATASOURCE_JNDI_NAME; +import static tap.config.TAPConfiguration.KEY_DB_PASSWORD; +import static tap.config.TAPConfiguration.KEY_DB_USERNAME; +import static tap.config.TAPConfiguration.KEY_JDBC_DRIVER; +import static tap.config.TAPConfiguration.KEY_JDBC_URL; +import static tap.config.TAPConfiguration.KEY_SQL_TRANSLATOR; +import static tap.config.TAPConfiguration.VALUE_JDBC; +import static tap.config.TAPConfiguration.VALUE_JDBC_DRIVERS; +import static tap.config.TAPConfiguration.VALUE_JNDI; +import static tap.config.TAPConfiguration.VALUE_NEVER; +import static tap.config.TAPConfiguration.VALUE_PGSPHERE; +import static tap.config.TAPConfiguration.VALUE_POSTGRESQL; +import static tap.config.TAPConfiguration.VALUE_USER_ACTION; +import static tap.config.TAPConfiguration.getProperty; + +import java.sql.Driver; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.Enumeration; +import java.util.Properties; + +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.sql.DataSource; + +import tap.AbstractTAPFactory; +import tap.ServiceConnection; +import tap.TAPException; +import tap.TAPFactory; +import tap.backup.DefaultTAPBackupManager; +import tap.db.DBConnection; +import tap.db.JDBCConnection; +import uws.UWSException; +import uws.service.UWSService; +import uws.service.backup.UWSBackupManager; +import uws.service.log.UWSLog.LogLevel; +import adql.translator.JDBCTranslator; +import adql.translator.PgSphereTranslator; +import adql.translator.PostgreSQLTranslator; + +/** + *

    Concrete implementation of a {@link TAPFactory} which is parameterized by a TAP configuration file.

    + * + *

    + * All abstract or NULL-implemented methods/functions left by {@link AbstractTAPFactory} are implemented using values + * of a TAP configuration file. The concerned methods are: {@link #getConnection(String)}, {@link #freeConnection(DBConnection)}, + * {@link #destroy()}, {@link #createADQLTranslator()} and {@link #createUWSBackupManager(UWSService)}. + *

    + * + * @author Grégory Mantelet (ARI) + * @version 2.0 (04/2015) + * @since 2.0 + */ +public final class ConfigurableTAPFactory extends AbstractTAPFactory { + + /* ADQL to SQL translation: */ + /** The {@link JDBCTranslator} to use when a ADQL query must be executed in the database. + * This translator is also used to convert ADQL types into database types. */ + private Class translator; + + /* JNDI DB access: */ + /** The {@link DataSource} to use in order to access the database. + * This attribute is actually used only if the chosen database access method is JNDI. */ + private final DataSource datasource; + + /* Simple JDBC access: */ + /** Classpath of the JDBC driver to use in order to access the database. + * This attribute is actually used only if the chosen database access method is JDBC. */ + private final String driverPath; + /** JDBC URL of the database to access. + * This attribute is actually used only if the chosen database access method is JDBC. */ + private final String dbUrl; + /** Name of the database user to use in order to access the database. + * This attribute is actually used only if the chosen database access method is JDBC. */ + private final String dbUser; + /** Password of the database user to use in order to access the database. + * This attribute is actually used only if the chosen database access method is JDBC. */ + private final String dbPassword; + + /* UWS's jobs backup: */ + /** Indicate whether the jobs must be backuped gathered by user or just all mixed together. */ + private boolean backupByUser; + /** Frequency at which the jobs must be backuped. */ + private long backupFrequency; + + /** + * Build a {@link TAPFactory} using the given TAP service description and TAP configuration file. + * + * @param service The TAP service description. + * @param tapConfig The TAP configuration file containing particularly information about the database access. + * + * @throws NullPointerException If one of the parameter is NULL. + * @throws TAPException If some properties of the TAP configuration file are wrong. + */ + public ConfigurableTAPFactory(ServiceConnection service, final Properties tapConfig) throws NullPointerException, TAPException{ + super(service); + + if (tapConfig == null) + throw new NullPointerException("Missing TAP properties! "); + + /* 1. Configure the database access */ + final String dbAccessMethod = getProperty(tapConfig, KEY_DATABASE_ACCESS); + + // Case a: Missing access method => error! + if (dbAccessMethod == null) + throw new TAPException("The property \"" + KEY_DATABASE_ACCESS + "\" is missing! It is required to connect to the database. Two possible values: \"" + VALUE_JDBC + "\" and \"" + VALUE_JNDI + "\"."); + + // Case b: JDBC ACCESS + else if (dbAccessMethod.equalsIgnoreCase(VALUE_JDBC)){ + // Extract the DB type and deduce the JDBC Driver path: + String jdbcDriver = getProperty(tapConfig, KEY_JDBC_DRIVER); + String dbUrl = getProperty(tapConfig, KEY_JDBC_URL); + if (jdbcDriver == null){ + if (dbUrl == null) + throw new TAPException("The property \"" + KEY_JDBC_URL + "\" is missing! Since the choosen database access method is \"" + VALUE_JDBC + "\", this property is required."); + else if (!dbUrl.startsWith(JDBCConnection.JDBC_PREFIX + ":")) + throw new TAPException("JDBC URL format incorrect! It MUST begins with " + JDBCConnection.JDBC_PREFIX + ":"); + else{ + String dbType = dbUrl.substring(JDBCConnection.JDBC_PREFIX.length() + 1); + if (dbType.indexOf(':') <= 0) + throw new TAPException("JDBC URL format incorrect! Database type name is missing."); + dbType = dbType.substring(0, dbType.indexOf(':')); + + jdbcDriver = VALUE_JDBC_DRIVERS.get(dbType); + if (jdbcDriver == null) + throw new TAPException("No JDBC driver known for the DBMS \"" + dbType + "\"!"); + } + } + // Set the DB connection parameters: + this.driverPath = jdbcDriver; + this.dbUrl = dbUrl; + this.dbUser = getProperty(tapConfig, KEY_DB_USERNAME); + this.dbPassword = getProperty(tapConfig, KEY_DB_PASSWORD); + // Set the other DB connection parameters: + this.datasource = null; + } + // Case c: JNDI ACCESS + else if (dbAccessMethod.equalsIgnoreCase(VALUE_JNDI)){ + // Get the datasource JDNI name: + String dsName = getProperty(tapConfig, KEY_DATASOURCE_JNDI_NAME); + if (dsName == null) + throw new TAPException("The property \"" + KEY_DATASOURCE_JNDI_NAME + "\" is missing! Since the choosen database access method is \"" + VALUE_JNDI + "\", this property is required."); + try{ + // Load the JNDI context: + InitialContext cxt = new InitialContext(); + // Look for the specified datasource: + datasource = (DataSource)cxt.lookup(dsName); + if (datasource == null) + throw new TAPException("No datasource found with the JNDI name \"" + dsName + "\"!"); + // Set the other DB connection parameters: + this.driverPath = null; + this.dbUrl = null; + this.dbUser = null; + this.dbPassword = null; + }catch(NamingException ne){ + throw new TAPException("No datasource found with the JNDI name \"" + dsName + "\"!"); + } + } + // Case d: unsupported value + else + throw new TAPException("Unsupported value for the property " + KEY_DATABASE_ACCESS + ": \"" + dbAccessMethod + "\"! Allowed values: \"" + VALUE_JNDI + "\" or \"" + VALUE_JDBC + "\"."); + + /* 2. Set the ADQLTranslator to use in function of the sql_translator property */ + String sqlTranslator = getProperty(tapConfig, KEY_SQL_TRANSLATOR); + // case a: no translator specified + if (sqlTranslator == null) + throw new TAPException("The property \"" + KEY_SQL_TRANSLATOR + "\" is missing! ADQL queries can not be translated without it. Allowed values: \"" + VALUE_POSTGRESQL + "\", \"" + VALUE_PGSPHERE + "\" or a class path of a class implementing SQLTranslator."); + + // case b: PostgreSQL translator + else if (sqlTranslator.equalsIgnoreCase(VALUE_POSTGRESQL)) + translator = PostgreSQLTranslator.class; + + // case c: PgSphere translator + else if (sqlTranslator.equalsIgnoreCase(VALUE_PGSPHERE)) + translator = PgSphereTranslator.class; + + // case d: a client defined ADQLTranslator (with the provided class name) + else if (TAPConfiguration.isClassName(sqlTranslator)) + translator = TAPConfiguration.fetchClass(sqlTranslator, KEY_SQL_TRANSLATOR, JDBCTranslator.class); + + // case e: unsupported value + else + throw new TAPException("Unsupported value for the property " + KEY_SQL_TRANSLATOR + ": \"" + sqlTranslator + "\" !"); + + /* 3. Test the construction of the ADQLTranslator */ + createADQLTranslator(); + + /* 4. Test the DB connection (note: a translator is needed to create a connection) */ + DBConnection dbConn = getConnection("0"); + freeConnection(dbConn); + + /* 5. Set the UWS Backup Parameter */ + // Set the backup frequency: + String propValue = getProperty(tapConfig, KEY_BACKUP_FREQUENCY); + // determine whether the value is a time period ; if yes, set the frequency: + if (propValue != null){ + try{ + backupFrequency = Long.parseLong(propValue); + if (backupFrequency <= 0) + backupFrequency = DEFAULT_BACKUP_FREQUENCY; + }catch(NumberFormatException nfe){ + // if the value was not a valid numeric time period, try to identify the different textual options: + if (propValue.equalsIgnoreCase(VALUE_NEVER)) + backupFrequency = DefaultTAPBackupManager.MANUAL; + else if (propValue.equalsIgnoreCase(VALUE_USER_ACTION)) + backupFrequency = DefaultTAPBackupManager.AT_USER_ACTION; + else + throw new TAPException("Long expected for the property \"" + KEY_BACKUP_FREQUENCY + "\", instead of: \"" + propValue + "\"!"); + } + }else + backupFrequency = DEFAULT_BACKUP_FREQUENCY; + // Specify whether the backup must be organized by user or not: + propValue = getProperty(tapConfig, KEY_BACKUP_BY_USER); + backupByUser = (propValue == null) ? DEFAULT_BACKUP_BY_USER : Boolean.parseBoolean(propValue); + } + + /** + * Build a {@link JDBCTranslator} instance with the given class ({@link #translator} ; + * specified by the property sql_translator). If the instance can not be build, + * whatever is the reason, a TAPException MUST be thrown. + * + * Note: This function is called at the initialization of {@link ConfigurableTAPFactory} + * in order to check that a translator can be created. + */ + protected JDBCTranslator createADQLTranslator() throws TAPException{ + try{ + return translator.getConstructor().newInstance(); + }catch(Exception ex){ + if (ex instanceof TAPException) + throw (TAPException)ex; + else + throw new TAPException("Impossible to create a JDBCTranslator instance with the empty constructor of \"" + translator.getName() + "\" (see the property " + KEY_SQL_TRANSLATOR + ") for the following reason: " + ex.getMessage()); + } + } + + /** + * Build a {@link JDBCConnection} thanks to the database parameters specified + * in the TAP configuration file (the properties: jdbc_driver_path, db_url, db_user, db_password). + * + * @see JDBCConnection#JDBCConnection(java.sql.Connection, JDBCTranslator, String, tap.log.TAPLog) + * @see JDBCConnection#JDBCConnection(String, String, String, String, JDBCTranslator, String, tap.log.TAPLog) + */ + @Override + public DBConnection getConnection(String jobID) throws TAPException{ + if (datasource != null){ + try{ + return new JDBCConnection(datasource.getConnection(), createADQLTranslator(), jobID, this.service.getLogger()); + }catch(SQLException se){ + throw new TAPException("Impossible to establish a connection to the database using the set up datasource!", se); + } + }else + return new JDBCConnection(driverPath, dbUrl, dbUser, dbPassword, createADQLTranslator(), jobID, this.service.getLogger()); + } + + @Override + public void freeConnection(DBConnection conn){ + try{ + ((JDBCConnection)conn).getInnerConnection().close(); + }catch(SQLException se){ + service.getLogger().error("Can not close properly the connection \"" + conn.getID() + "\"!", se); + } + } + + @Override + public void destroy(){ + // Unregister the JDBC driver, only if registered by the library (i.e. database_access=jdbc): + if (dbUrl != null){ + // Now deregister JDBC drivers in this context's ClassLoader: + // Get the webapp's ClassLoader + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + // Loop through all drivers + Enumeration drivers = DriverManager.getDrivers(); + while(drivers.hasMoreElements()){ + Driver driver = drivers.nextElement(); + if (driver.getClass().getClassLoader() == cl){ + // This driver was registered by the webapp's ClassLoader, so deregister it: + try{ + DriverManager.deregisterDriver(driver); + service.getLogger().logTAP(LogLevel.INFO, null, "STOP", "JDBC driver " + driver.getClass().getName() + " successfully deregistered!", null); + }catch(SQLException ex){ + service.getLogger().logTAP(LogLevel.FATAL, null, "STOP", "Error deregistering JDBC driver " + driver.getClass().getName() + "!", ex); + } + } + } + } + } + + /** + * Build an {@link DefaultTAPBackupManager} thanks to the backup manager parameters specified + * in the TAP configuration file (the properties: backup_frequency, backup_by_user). + * + * Note: If the specified backup_frequency is negative, no backup manager is returned. + * + * @return null if the specified backup frequency is negative, or an instance of {@link DefaultTAPBackupManager} otherwise. + * + * @see tap.AbstractTAPFactory#createUWSBackupManager(uws.service.UWSService) + * @see DefaultTAPBackupManager + */ + @Override + public UWSBackupManager createUWSBackupManager(UWSService uws) throws TAPException{ + try{ + return (backupFrequency < 0) ? null : new DefaultTAPBackupManager(uws, backupByUser, backupFrequency); + }catch(UWSException ex){ + throw new TAPException("Impossible to create a backup manager, because: " + ex.getMessage(), ex); + } + } + +} diff --git a/src/tap/config/ConfigurableTAPServlet.java b/src/tap/config/ConfigurableTAPServlet.java new file mode 100644 index 0000000..83f217d --- /dev/null +++ b/src/tap/config/ConfigurableTAPServlet.java @@ -0,0 +1,234 @@ +package tap.config; + +/* + * This file is part of TAPLibrary. + * + * TAPLibrary is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * TAPLibrary is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with TAPLibrary. If not, see . + * + * Copyright 2015 - Astronomisches Rechen Institut (ARI) + */ + +import static tap.config.TAPConfiguration.DEFAULT_TAP_CONF_FILE; +import static tap.config.TAPConfiguration.KEY_ADD_TAP_RESOURCES; +import static tap.config.TAPConfiguration.KEY_HOME_PAGE; +import static tap.config.TAPConfiguration.KEY_HOME_PAGE_MIME_TYPE; +import static tap.config.TAPConfiguration.TAP_CONF_PARAMETER; +import static tap.config.TAPConfiguration.getProperty; +import static tap.config.TAPConfiguration.isClassName; +import static tap.config.TAPConfiguration.newInstance; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import tap.ServiceConnection; +import tap.TAPException; +import tap.resource.HomePage; +import tap.resource.TAP; +import tap.resource.TAPResource; + +/** + *

    HTTP servlet fully configured with a TAP configuration file.

    + * + *

    + * This configuration file may be specified in the initial parameter named {@link TAPConfiguration#TAP_CONF_PARAMETER} + * of this servlet inside the WEB-INF/web.xml file. If none is specified, the file {@link TAPConfiguration#DEFAULT_TAP_CONF_FILE} + * will be searched inside the directories of the classpath, and inside WEB-INF and META-INF. + *

    + * + * @author Grégory Mantelet (ARI) + * @version 2.0 (04/2015) + * @since 2.0 + */ +public class ConfigurableTAPServlet extends HttpServlet { + private static final long serialVersionUID = 1L; + + /** TAP object representing the TAP service. */ + private TAP tap = null; + + @Override + public void init(final ServletConfig config) throws ServletException{ + // Nothing to do, if TAP is already initialized: + if (tap != null) + return; + + /* 1. GET THE FILE PATH OF THE TAP CONFIGURATION FILE */ + String tapConfPath = config.getInitParameter(TAP_CONF_PARAMETER); + if (tapConfPath == null || tapConfPath.trim().length() == 0) + tapConfPath = null; + //throw new ServletException("Configuration file path missing! You must set a servlet init parameter whose the name is \"" + TAP_CONF_PARAMETER + "\"."); + + /* 2. OPEN THE CONFIGURATION FILE */ + InputStream input = null; + // CASE: No file specified => search in the classpath for a file having the default name "tap.properties". + if (tapConfPath == null) + input = searchFile(DEFAULT_TAP_CONF_FILE, config); + else{ + File f = new File(tapConfPath); + // CASE: The given path matches to an existing local file. + if (f.exists()){ + try{ + input = new FileInputStream(f); + }catch(IOException ioe){ + throw new ServletException("Impossible to read the TAP configuration file (" + tapConfPath + ")!", ioe); + } + } + // CASE: The given path seems to be relative to the servlet root directory. + else + input = searchFile(tapConfPath, config); + } + // If no file has been found, cancel the servlet loading: + if (input == null) + throw new ServletException("Configuration file not found with the path: \"" + ((tapConfPath == null) ? DEFAULT_TAP_CONF_FILE : tapConfPath) + "\"! Please provide a correct file path in servlet init parameter (\"" + TAP_CONF_PARAMETER + "\") or put your configuration file named \"" + DEFAULT_TAP_CONF_FILE + "\" in a directory of the classpath or in WEB-INF or META-INF."); + + /* 3. PARSE IT INTO A PROPERTIES SET */ + Properties tapConf = new Properties(); + try{ + tapConf.load(input); + }catch(IOException ioe){ + throw new ServletException("Impossible to read the TAP configuration file (" + tapConfPath + ")!", ioe); + }finally{ + try{ + input.close(); + }catch(IOException ioe2){} + } + + /* 4. CREATE THE TAP SERVICE */ + ServiceConnection serviceConn = null; + try{ + // Create the service connection: + serviceConn = new ConfigurableServiceConnection(tapConf, config.getServletContext().getRealPath("")); + // Create all the TAP resources: + tap = new TAP(serviceConn); + }catch(Exception ex){ + tap = null; + if (ex instanceof TAPException) + throw new ServletException(ex.getMessage(), ex.getCause()); + else + throw new ServletException("Impossible to initialize the TAP service!", ex); + } + + /* 4Bis. SET THE HOME PAGE */ + String propValue = getProperty(tapConf, KEY_HOME_PAGE); + if (propValue != null){ + // If it is a class path, replace the current home page by an instance of this class: + if (isClassName(propValue)){ + try{ + tap.setHomePage(newInstance(propValue, KEY_HOME_PAGE, HomePage.class, new Class[]{TAP.class}, new Object[]{tap})); + }catch(TAPException te){ + throw new ServletException(te.getMessage(), te.getCause()); + } + } + // If it is a file URI (null, file inside WebContent, file://..., http://..., etc...): + else{ + // ...set the given URI: + tap.setHomePageURI(propValue); + // ...and its MIME type (if any): + propValue = getProperty(tapConf, KEY_HOME_PAGE_MIME_TYPE); + if (propValue != null) + tap.setHomePageMimeType(propValue); + } + } + + /* 5. SET ADDITIONAL TAP RESOURCES */ + propValue = getProperty(tapConf, KEY_ADD_TAP_RESOURCES); + if (propValue != null){ + // split all list items: + String[] lstResources = propValue.split(","); + for(String addRes : lstResources){ + addRes = addRes.trim(); + // ignore empty items: + if (addRes.length() > 0){ + try{ + // create an instance of the resource: + TAPResource newRes = newInstance(addRes, KEY_ADD_TAP_RESOURCES, TAPResource.class, new Class[]{TAP.class}, new Object[]{tap}); + if (newRes.getName() == null || newRes.getName().trim().length() == 0) + throw new TAPException("TAP resource name missing for the new resource \"" + addRes + "\"! The function getName() of the new TAPResource must return a non-empty and not NULL name. See the property \"" + KEY_ADD_TAP_RESOURCES + "\"."); + // add it into TAP: + tap.addResource(newRes); + }catch(TAPException te){ + throw new ServletException(te.getMessage(), te.getCause()); + } + } + } + } + + /* 6. DEFAULT SERVLET INITIALIZATION */ + super.init(config); + + /* 7. FINALLY MAKE THE SERVICE AVAILABLE */ + serviceConn.setAvailable(true, "TAP service available."); + } + + /** + * Search the given file name/path in the directories of the classpath, then inside WEB-INF and finally inside META-INF. + * + * @param filePath A file name/path. + * @param config Servlet configuration (containing also the context class loader - link with the servlet classpath). + * + * @return The input stream toward the specified file, or NULL if no file can be found. + * + * @since 2.0 + */ + protected final InputStream searchFile(String filePath, final ServletConfig config){ + InputStream input = null; + + // Try to search in the classpath (with just a file name or a relative path): + input = Thread.currentThread().getContextClassLoader().getResourceAsStream(filePath); + + // If not found, try searching in WEB-INF and META-INF (as this fileName is a file path relative to one of these directories): + if (input == null){ + if (filePath.startsWith("/")) + filePath = filePath.substring(1); + // ...try at the root of WEB-INF: + input = config.getServletContext().getResourceAsStream("/WEB-INF/" + filePath); + // ...and at the root of META-INF: + if (input == null) + input = config.getServletContext().getResourceAsStream("/META-INF/" + filePath); + } + + return input; + } + + @Override + public void destroy(){ + // Free all resources used by TAP: + if (tap != null){ + tap.destroy(); + tap = null; + } + super.destroy(); + } + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException{ + if (tap != null){ + try{ + tap.executeRequest(req, resp); + }catch(Throwable t){ + resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, t.getMessage()); + } + }else + resp.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "TAP service not yet initialized!"); + } + +} diff --git a/src/tap/config/TAPConfiguration.java b/src/tap/config/TAPConfiguration.java new file mode 100644 index 0000000..11934ed --- /dev/null +++ b/src/tap/config/TAPConfiguration.java @@ -0,0 +1,539 @@ +package tap.config; + +/* + * This file is part of TAPLibrary. + * + * TAPLibrary is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * TAPLibrary is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with TAPLibrary. If not, see . + * + * Copyright 2015 - Astronomisches Rechen Institut (ARI) + */ + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.HashMap; +import java.util.Properties; + +import tap.ServiceConnection.LimitUnit; +import tap.TAPException; +import tap.TAPFactory; +import tap.backup.DefaultTAPBackupManager; + +/** + *

    Utility class gathering tool functions and properties' names useful to deal with a TAP configuration file.

    + * + *

    This class implements the Design Pattern "Utility": no instance of this class can be created, it can not be extended, + * and it must be used only thanks to its static classes and attributes.

    + * + * @author Grégory Mantelet (ARI) + * @version 2.0 (04/2015) + * @since 2.0 + */ +public final class TAPConfiguration { + + /** Name of the initial parameter to set in the WEB-INF/web.xml file + * in order to specify the location and the name of the TAP configuration file to load. */ + public final static String TAP_CONF_PARAMETER = "tapconf"; + /** Default TAP configuration file. This file is research automatically + * if none is specified in the WEB-INF/web.xml initial parameter {@value #TAP_CONF_PARAMETER}. */ + public final static String DEFAULT_TAP_CONF_FILE = "tap.properties"; + + /* FILE MANAGER KEYS */ + /** Name/Key of the property setting the file manager to use in the TAP service. */ + public final static String KEY_FILE_MANAGER = "file_manager"; + /** Value of the property {@link #KEY_FILE_MANAGER} specifying a local file manager. */ + public final static String VALUE_LOCAL = "local"; + /** Default value of the property {@link #KEY_FILE_MANAGER}: {@value #DEFAULT_FILE_MANAGER}. */ + public final static String DEFAULT_FILE_MANAGER = VALUE_LOCAL; + /** Name/Key of the property setting the local root directory where all TAP files must be stored. + * This property is used only if {@link #KEY_FILE_MANAGER} is set to {@link #VALUE_LOCAL}. */ + public final static String KEY_FILE_ROOT_PATH = "file_root_path"; + /** Name/Key of the property indicating whether the jobs must be saved by user or not. + * If yes, there will be one directory per user. Otherwise, all jobs are backuped in the same directory + * (generally {@link #KEY_FILE_ROOT_PATH}). */ + public final static String KEY_DIRECTORY_PER_USER = "directory_per_user"; + /** Default value of the property {@link #KEY_DIRECTORY_PER_USER}: {@value #DEFAULT_DIRECTORY_PER_USER}. */ + public final static boolean DEFAULT_DIRECTORY_PER_USER = false; + /** Name/Key of the property indicating whether the user directories (in which jobs of the user are backuped) + * must be gathered in less directories. If yes, the groups are generally made using the alphabetic order. + * The idea is to reduce the number of apparent directories and to easier the research of a user directory. */ + public final static String KEY_GROUP_USER_DIRECTORIES = "group_user_directories"; + /** Default value of the property {@link #KEY_GROUP_USER_DIRECTORIES}: {@value #DEFAULT_GROUP_USER_DIRECTORIES}. */ + public final static boolean DEFAULT_GROUP_USER_DIRECTORIES = false; + /** Name/Key of the property specifying the default period (in seconds) while a job must remain on the server. + * This value is set automatically to any job whose the retention period has never been specified by the user. */ + public final static String KEY_DEFAULT_RETENTION_PERIOD = "default_retention_period"; + /** Name/Key of the property specifying the maximum period (in seconds) while a job can remain on the server. */ + public final static String KEY_MAX_RETENTION_PERIOD = "max_retention_period"; + /** Default value of the properties {@link #KEY_DEFAULT_RETENTION_PERIOD} and {@link #KEY_MAX_RETENTION_PERIOD}: + * {@value #DEFAULT_RETENTION_PERIOD}. */ + public final static int DEFAULT_RETENTION_PERIOD = 0; + + /* LOG KEYS */ + /** Name/Key of the property specifying the minimum type of messages (i.e. DEBUG, INFO, WARNING, ERROR, FATAL) + * that must be logged. By default all messages are logged...which is equivalent to set this property to "DEBUG". */ + public final static String KEY_MIN_LOG_LEVEL = "min_log_level"; + /** Name/Key of the property specifying the frequency of the log file rotation. + * By default the log rotation occurs every day at midnight. */ + public final static String KEY_LOG_ROTATION = "log_rotation"; + + /* UWS BACKUP */ + /** Name/Key of the property specifying the frequency (in milliseconds) of jobs backup. + * This property accepts three types of value: "never" (default), "user_action" (the backup of a job is done when + * it is modified), or a numeric positive value (expressed in milliseconds). */ + public final static String KEY_BACKUP_FREQUENCY = "backup_frequency"; + /** Value of the property {@link #KEY_BACKUP_FREQUENCY} indicating that jobs should never be backuped. */ + public final static String VALUE_NEVER = "never"; + /** Value of the property {@link #KEY_BACKUP_FREQUENCY} indicating that job backup should occur only when the user + * creates or modifies one of his jobs. This value can be used ONLY IF {@link #KEY_BACKUP_BY_USER} is "true". */ + public final static String VALUE_USER_ACTION = "user_action"; + /** Default value of the property {@link #KEY_BACKUP_FREQUENCY}: {@link #DEFAULT_BACKUP_FREQUENCY}. */ + public final static long DEFAULT_BACKUP_FREQUENCY = DefaultTAPBackupManager.MANUAL; // = "never" => no UWS backup manager + /** Name/Key of the property indicating whether there should be one backup file per user or one file for all. */ + public final static String KEY_BACKUP_BY_USER = "backup_by_user"; + /** Default value of the property {@link #KEY_BACKUP_BY_USER}: {@value #DEFAULT_BACKUP_BY_USER}. + * This property can be enabled only if a user identification method is provided. */ + public final static boolean DEFAULT_BACKUP_BY_USER = false; + + /* ASYNCHRONOUS JOBS */ + /** Name/Key of the property specifying the maximum number of asynchronous jobs that can run simultaneously. + * A negative or null value means "no limit". */ + public final static String KEY_MAX_ASYNC_JOBS = "max_async_jobs"; + /** Default value of the property {@link #KEY_MAX_ASYNC_JOBS}: {@value #DEFAULT_MAX_ASYNC_JOBS}. */ + public final static int DEFAULT_MAX_ASYNC_JOBS = 0; + + /* EXECUTION DURATION */ + /** Name/Key of the property specifying the default execution duration (in milliseconds) set automatically to a job + * if none has been specified by the user. */ + public final static String KEY_DEFAULT_EXECUTION_DURATION = "default_execution_duration"; + /** Name/Key of the property specifying the maximum execution duration (in milliseconds) that can be set on a job. */ + public final static String KEY_MAX_EXECUTION_DURATION = "max_execution_duration"; + /** Default value of the property {@link #KEY_DEFAULT_EXECUTION_DURATION} and {@link #KEY_MAX_EXECUTION_DURATION}: {@value #DEFAULT_EXECUTION_DURATION}. */ + public final static int DEFAULT_EXECUTION_DURATION = 0; + + /* DATABASE KEYS */ + /** Name/Key of the property specifying the database access method to use. */ + public final static String KEY_DATABASE_ACCESS = "database_access"; + /** Value of the property {@link #KEY_DATABASE_ACCESS} to select the simple JDBC method. */ + public final static String VALUE_JDBC = "jdbc"; + /** Value of the property {@link #KEY_DATABASE_ACCESS} to access the database using a DataSource stored in JNDI. */ + public final static String VALUE_JNDI = "jndi"; + /** Name/Key of the property specifying the ADQL to SQL translator to use. */ + public final static String KEY_SQL_TRANSLATOR = "sql_translator"; + /** Value of the property {@link #KEY_SQL_TRANSLATOR} to select a PostgreSQL translator (no support for geometrical functions). */ + public final static String VALUE_POSTGRESQL = "postgres"; + /** Value of the property {@link #KEY_SQL_TRANSLATOR} to select a PgSphere translator. */ + public final static String VALUE_PGSPHERE = "pgsphere"; + /** Name/Key of the property specifying by how many rows the library should fetch a query result from the database. + * This is the fetch size for to apply for synchronous queries. */ + public final static String KEY_SYNC_FETCH_SIZE = "sync_fetch_size"; + /** Default value of the property {@link #KEY_SYNC_FETCH_SIZE}: {@value #DEFAULT_SYNC_FETCH_SIZE}. */ + public final static int DEFAULT_SYNC_FETCH_SIZE = 10000; + /** Name/Key of the property specifying by how many rows the library should fetch a query result from the database. + * This is the fetch size for to apply for asynchronous queries. */ + public final static String KEY_ASYNC_FETCH_SIZE = "async_fetch_size"; + /** Default value of the property {@link #KEY_ASYNC_FETCH_SIZE}: {@value #DEFAULT_ASYNC_FETCH_SIZE}. */ + public final static int DEFAULT_ASYNC_FETCH_SIZE = 100000; + /** Name/Key of the property specifying the name of the DataSource into the JDNI. */ + public final static String KEY_DATASOURCE_JNDI_NAME = "datasource_jndi_name"; + /** Name/Key of the property specifying the full class name of the JDBC driver. + * Alternatively, a shortcut the most known JDBC drivers can be used. The list of these drivers is stored + * in {@link #VALUE_JDBC_DRIVERS}. */ + public final static String KEY_JDBC_DRIVER = "jdbc_driver"; + /** List of the most known JDBC drivers. For the moment this list contains 4 drivers: + * oracle ("oracle.jdbc.OracleDriver"), postgresql ("org.postgresql.Driver"), mysql ("com.mysql.jdbc.Driver") + * and sqlite ("org.sqlite.JDBC"). */ + public final static HashMap VALUE_JDBC_DRIVERS = new HashMap(4); + static{ + VALUE_JDBC_DRIVERS.put("oracle", "oracle.jdbc.OracleDriver"); + VALUE_JDBC_DRIVERS.put("postgresql", "org.postgresql.Driver"); + VALUE_JDBC_DRIVERS.put("mysql", "com.mysql.jdbc.Driver"); + VALUE_JDBC_DRIVERS.put("sqlite", "org.sqlite.JDBC"); + } + /** Name/Key of the property specifying the JDBC URL of the database to access. */ + public final static String KEY_JDBC_URL = "jdbc_url"; + /** Name/Key of the property specifying the database user name to use to access the database. */ + public final static String KEY_DB_USERNAME = "db_username"; + /** Name/Key of the property specifying the password of the database user. */ + public final static String KEY_DB_PASSWORD = "db_password"; + + /* METADATA KEYS */ + /** Name/Key of the property specifying where the list of schemas, tables and columns and their respective metadata + * is provided. */ + public final static String KEY_METADATA = "metadata"; + /** Value of the property {@link #KEY_METADATA} which indicates that metadata are provided in an XML file, whose the + * local path is given by the property {@link #KEY_METADATA_FILE}. */ + public final static String VALUE_XML = "xml"; + /** Value of the property {@link #KEY_METADATA} which indicates that metadata are already in the TAP_SCHEMA of the database. */ + public final static String VALUE_DB = "db"; + /** Name/Key of the property specifying the local file path of the XML file containing the TAP metadata to load. */ + public final static String KEY_METADATA_FILE = "metadata_file"; + + /* HOME PAGE KEY */ + /** Name/Key of the property specifying the TAP home page to use. + * It can be a file, a URL or a class. If null, the default TAP home page of the library is used. + * By default the default library home page is used. */ + public final static String KEY_HOME_PAGE = "home_page"; + /** Name/Key of the property specifying the MIME type of the set home page. + * By default, "text/html" is set. */ + public final static String KEY_HOME_PAGE_MIME_TYPE = "home_page_mime_type"; + + /* PROVIDER KEYS */ + /** Name/Key of the property specifying the name of the organization/person providing the TAP service. */ + public final static String KEY_PROVIDER_NAME = "provider_name"; + /** Name/Key of the property specifying the description of the TAP service. */ + public final static String KEY_SERVICE_DESCRIPTION = "service_description"; + + /* UPLOAD KEYS */ + /** Name/Key of the property indicating whether the UPLOAD feature must be enabled or not. + * By default, this feature is disabled. */ + public final static String KEY_UPLOAD_ENABLED = "upload_enabled"; + /** Name/Key of the property specifying the default limit (in rows or bytes) on the uploaded VOTable(s). */ + public final static String KEY_DEFAULT_UPLOAD_LIMIT = "upload_default_db_limit"; + /** Name/Key of the property specifying the maximum limit (in rows or bytes) on the uploaded VOTable(s). */ + public final static String KEY_MAX_UPLOAD_LIMIT = "upload_max_db_limit"; + /** Name/Key of the property specifying the maximum size of all VOTable(s) uploaded in a query. */ + public final static String KEY_UPLOAD_MAX_FILE_SIZE = "upload_max_file_size"; + /** Default value of the property {@link #KEY_UPLOAD_MAX_FILE_SIZE}: {@value #DEFAULT_UPLOAD_MAX_FILE_SIZE}. */ + public final static int DEFAULT_UPLOAD_MAX_FILE_SIZE = Integer.MAX_VALUE; + + /* OUTPUT KEYS */ + /** Name/Key of the property specifying the list of all result output formats to support. + * By default all formats provided by the library are allowed. */ + public final static String KEY_OUTPUT_FORMATS = "output_formats"; + /** Value of the property {@link #KEY_OUTPUT_FORMATS} which select all formats that the library can provide. */ + public final static String VALUE_ALL = "ALL"; + /** Value of the property {@link #KEY_OUTPUT_FORMATS} which select a VOTable format. + * The format can be parameterized with the VOTable version and serialization. */ + public final static String VALUE_VOTABLE = "votable"; + /** Value of the property {@link #KEY_OUTPUT_FORMATS} which select a VOTable format. + * The format can be parameterized with the VOTable version and serialization. + * This value is just an alias of {@link #VALUE_VOTABLE}. */ + public final static String VALUE_VOT = "vot"; + /** Value of the property {@link #KEY_OUTPUT_FORMATS} which select a FITS format. */ + public final static String VALUE_FITS = "fits"; + /** Value of the property {@link #KEY_OUTPUT_FORMATS} which select a JSON format. */ + public final static String VALUE_JSON = "json"; + /** Value of the property {@link #KEY_OUTPUT_FORMATS} which select an HTML format. */ + public final static String VALUE_HTML = "html"; + /** Value of the property {@link #KEY_OUTPUT_FORMATS} which select a human-readable table. */ + public final static String VALUE_TEXT = "text"; + /** Value of the property {@link #KEY_OUTPUT_FORMATS} which select a CSV format. */ + public final static String VALUE_CSV = "csv"; + /** Value of the property {@link #KEY_OUTPUT_FORMATS} which select a TSV format. */ + public final static String VALUE_TSV = "tsv"; + /** Value of the property {@link #KEY_OUTPUT_FORMATS} which select a Separated-Value format. + * This value must be parameterized with the separator to use. */ + public final static String VALUE_SV = "sv"; + /** Name/Key of the property specifying the number of result rows that should be returned if none is specified by the user. */ + public final static String KEY_DEFAULT_OUTPUT_LIMIT = "output_default_limit"; + /** Name/Key of the property specifying the maximum number of result rows that can be returned by the TAP service. */ + public final static String KEY_MAX_OUTPUT_LIMIT = "output_max_limit"; + + /* USER IDENTIFICATION */ + /** Name/Key of the property specifying the user identification method to use. + * None is implemented by the library, so a class must be provided as value of this property. */ + public final static String KEY_USER_IDENTIFIER = "user_identifier"; + + /* ADQL RESTRICTIONS */ + /** Name/Key of the property specifying the list of all allowed coordinate systems that can be used in ADQL queries. + * By default, all are allowed, but no conversion is done by the library. */ + public final static String KEY_COORD_SYS = "coordinate_systems"; + /** Name/Key of the property specifying the list of all ADQL geometrical function that can be used in ADQL queries. + * By default, all are allowed. */ + public final static String KEY_GEOMETRIES = "geometries"; + /** Value of {@link #KEY_COORD_SYS} and {@link #KEY_GEOMETRIES} that forbid all possible values. */ + public final static String VALUE_NONE = "NONE"; + /** Name/Key of the property that lets declare all User Defined Functions that must be allowed in ADQL queries. + * By default, all unknown functions are rejected. This default behavior can be totally reversed by using the + * value {@link #VALUE_ANY} */ + public final static String KEY_UDFS = "udfs"; + /** Value of {@link #KEY_UDFS} allowing any unknown function in ADQL queries. Those functions will be considered as UDFs + * and will be translated into SQL exactly as they are written in ADQL. */ + public final static String VALUE_ANY = "ANY"; + + /* ADDITIONAL TAP RESOURCES */ + /** Name/Key of the property specifying a list of resources to add to the TAP service (e.g. a ADQL query validator). + * By default, this list if empty ; only the default TAP resources exist. */ + public final static String KEY_ADD_TAP_RESOURCES = "additional_resources"; + + /* CUSTOM FACTORY */ + /** Name/Key of the property specifying the {@link TAPFactory} class to use instead of the default {@link ConfigurableTAPFactory}. + * Setting a value to this property could disable several properties of the TAP configuration file. */ + public final static String KEY_TAP_FACTORY = "tap_factory"; + + /** No instance of this class should be created. */ + private TAPConfiguration(){} + + /** + *

    Read the asked property from the given Properties object.

    + *
      + *
    • The returned property value is trimmed (no space at the beginning and at the end of the string).
    • + *
    • If the value is empty (length=0), NULL is returned.
    • + *
    + * + * @param prop List of property + * @param key Property whose the value is requested. + * + * @return Return property value. + */ + public final static String getProperty(final Properties prop, final String key){ + if (prop == null) + return null; + + String value = prop.getProperty(key); + if (value != null){ + value = value.trim(); + return (value.length() == 0) ? null : value; + } + + return value; + } + + /** + * Test whether a property value is a class name. + * Expected syntax: a non-empty string surrounded by brackets ('{' and '}'). + * + * Note: The class name itself is not checked! + * + * @param value Property value. + * + * @return true if the given value is formatted as a class name, false otherwise. + */ + public final static boolean isClassName(final String value){ + return (value != null && value.length() > 2 && value.charAt(0) == '{' && value.charAt(value.length() - 1) == '}'); + } + + /** + * Fetch the class object corresponding to the class name provided between brackets in the given value. + * + * @param value Value which is supposed to contain the class name between brackets (see {@link #isClassName(String)} for more details) + * @param propertyName Name of the property associated with the parameter "value". + * @param expectedType Type of the class expected to be returned ; it is also the type which parameterizes this function: C. + * + * @return The corresponding Class object. + * + * @throws TAPException If the class name is incorrect or if its type is not compatible with the parameterized type C (represented by the parameter "expectedType"). + * + * @see #isClassName(String) + */ + @SuppressWarnings("unchecked") + public final static < C > Class fetchClass(final String value, final String propertyName, final Class expectedType) throws TAPException{ + if (!isClassName(value)) + return null; + + String classPath = value.substring(1, value.length() - 1).trim(); + if (classPath.isEmpty()) + return null; + + try{ + Class classObject = (Class)Class.forName(classPath); + if (!expectedType.isAssignableFrom(classObject)) + throw new TAPException("The class specified by the property \"" + propertyName + "\" (" + value + ") is not implementing " + expectedType.getName() + "."); + else + return classObject; + }catch(ClassNotFoundException cnfe){ + throw new TAPException("The class specified by the property \"" + propertyName + "\" (" + value + ") can not be found."); + }catch(ClassCastException cce){ + throw new TAPException("The class specified by the property \"" + propertyName + "\" (" + value + ") is not implementing " + expectedType.getName() + "."); + } + } + + /** + *

    Create an instance of the specified class. The class name is expected to be surrounded by {} in the given value.

    + * + *

    The instance is created using the empty constructor of the specified class.

    + * + * @param propValue Value which is supposed to contain the class name between brackets (see {@link #isClassName(String)} for more details) + * @param propName Name of the property associated with the parameter "value". + * @param expectedType Type of the class expected to be returned ; it is also the type which parameterizes this function: C. + * + * @return The corresponding instance. + * + * @throws TAPException If the class name is incorrect + * or if its type is not compatible with the parameterized type C (represented by the parameter "expectedType") + * or if the specified class has no empty constructor + * or if an error occurred while calling this constructor. + * + * @see #isClassName(String) + * @see #fetchClass(String, String, Class) + */ + public final static < C > C newInstance(final String propValue, final String propName, final Class expectedType) throws TAPException{ + return newInstance(propValue, propName, expectedType, null, null); + } + + /** + *

    Create an instance of the specified class. The class name is expected to be surrounded by {} in the given value.

    + * + *

    IMPORTANT: + * The instance is created using the constructor whose the declaration matches exactly with the given list of parameter types. + * The number and types of given parameters MUST match exactly to the list of parameter types. + *

    + * + * @param propValue Value which is supposed to contain the class name between brackets (see {@link #isClassName(String)} for more details) + * @param propName Name of the property associated with the parameter "value". + * @param expectedType Type of the class expected to be returned ; it is also the type which parameterizes this function: C. + * @param pTypes List of each constructor parameter type. Each type MUST be exactly the type declared in the class constructor to select. NULL or empty array if no parameter. + * @param parameters List of all constructor parameters. The number of object MUST match exactly the number of classes provided in the parameter pTypes. NULL or empty array if no parameter. + * + * @return The corresponding instance. + * + * @throws TAPException If the class name is incorrect + * or if its type is not compatible with the parameterized type C (represented by the parameter "expectedType") + * or if the constructor with the specified parameters can not be found + * or if an error occurred while calling this constructor. + * + * @see #isClassName(String) + * @see #fetchClass(String, String, Class) + */ + public final static < C > C newInstance(final String propValue, final String propName, final Class expectedType, final Class[] pTypes, final Object[] parameters) throws TAPException{ + // Ensure the given name is a class name specification: + if (!isClassName(propValue)) + throw new TAPException("Class name expected for the property \"" + propName + "\" instead of: \"" + propValue + "\"! The specified class must extend/implement " + expectedType.getName() + "."); + + Class classObj = null; + try{ + + // Fetch the class object: + classObj = fetchClass(propValue, propName, expectedType); + + // Get a constructor matching the given parameters list: + Constructor constructor = classObj.getConstructor((pTypes == null) ? new Class[0] : pTypes); + + // Finally create a new instance: + return constructor.newInstance((parameters == null) ? new Object[0] : parameters); + + }catch(NoSuchMethodException e){ + // List parameters' type: + StringBuffer pTypesStr = new StringBuffer(); + for(int i = 0; i < pTypes.length; i++){ + if (pTypesStr.length() > 0) + pTypesStr.append(", "); + if (pTypes[i] == null) + pTypesStr.append("NULL"); + pTypesStr.append(pTypes[i].getName()); + } + // Throw the error: + throw new TAPException("Missing constructor " + classObj.getName() + "(" + pTypesStr.toString() + ")! See the value \"" + propValue + "\" of the property \"" + propName + "\"."); + }catch(InstantiationException ie){ + throw new TAPException("Impossible to create an instance of an abstract class: \"" + classObj.getName() + "\"! See the value \"" + propValue + "\" of the property \"" + propName + "\"."); + }catch(InvocationTargetException ite){ + if (ite.getCause() != null){ + if (ite.getCause() instanceof TAPException) + throw (TAPException)ite.getCause(); + else + throw new TAPException(ite.getCause()); + }else + throw new TAPException(ite); + }catch(TAPException te){ + throw te; + }catch(Exception ex){ + throw new TAPException("Impossible to create an instance of " + expectedType.getName() + " as specified in the property \"" + propName + "\": \"" + propValue + "\"!", ex); + } + } + + /** + *

    Lets parsing a limit (for output, upload, ...) with its numeric value and its unit.

    + *

    + * Here is the expected syntax: num_val[unit]. + * Where unit is optional and should be one of the following values: r or R, B, kB, MB, GB. + * If the unit is not specified, it is set by default to ROWS. + *

    + *

    Note: If the value is strictly less than 0 (whatever is the unit), the returned value will be -1.

    + * + * @param value Property value (must follow the limit syntax: num_val[unit] ; ex: 20kB or 2000 (for 2000 rows)). + * @param propertyName Name of the property which specify the limit. + * @param areBytesAllowed Tells whether the unit BYTES is allowed. If not and a BYTES unit is encountered, then an exception is thrown. + * + * @return An array with always 2 items: [0]=numeric value (of type Integer), [1]=unit (of type {@link LimitUnit}). + * + * @throws TAPException If the syntax is incorrect or if a not allowed unit has been used. + */ + public final static Object[] parseLimit(String value, final String propertyName, final boolean areBytesAllowed) throws TAPException{ + // Remove any whitespace inside or outside the numeric value and its unit: + if (value != null) + value = value.replaceAll("\\s", ""); + + // If empty value, return an infinite limit: + if (value == null || value.length() == 0) + return new Object[]{-1,LimitUnit.rows}; + + // A. Parse the string from the end in order to extract the unit part. + // The final step of the loop is the extraction of the numeric value, when the first digit is encountered. + int numValue = -1; + LimitUnit unit; + StringBuffer buf = new StringBuffer(); + for(int i = value.length() - 1; i >= 0; i--){ + // if a digit, extract the numeric value: + if (value.charAt(i) >= '0' && value.charAt(i) <= '9'){ + try{ + numValue = Integer.parseInt(value.substring(0, i + 1)); + break; + }catch(NumberFormatException nfe){ + throw new TAPException("Integer expected for the property " + propertyName + " for the substring \"" + value.substring(0, i + 1) + "\" of the whole value: \"" + value + "\"!"); + } + } + // if a character, store it for later processing: + else + buf.append(value.charAt(i)); + + } + + // B. Parse the unit. + // if no unit, set ROWS by default: + if (buf.length() == 0) + unit = LimitUnit.rows; + // if the unit is too long, throw an exception: + else if (buf.length() > 2) + throw new TAPException("Unknown limit unit (" + buf.reverse().toString() + ") for the property " + propertyName + ": \"" + value + "\"!"); + // try to identify the unit: + else{ + // the base unit: bytes or rows + switch(buf.charAt(0)){ + case 'B': + if (!areBytesAllowed) + throw new TAPException("BYTES unit is not allowed for the property " + propertyName + " (" + value + ")!"); + unit = LimitUnit.bytes; + break; + case 'r': + case 'R': + unit = LimitUnit.rows; + break; + default: + throw new TAPException("Unknown limit unit (" + buf.reverse().toString() + ") for the property " + propertyName + ": \"" + value + "\"!"); + } + // the 10-power of the base unit, if any: + if (buf.length() > 1){ + if (unit == LimitUnit.bytes){ + switch(buf.charAt(1)){ + case 'k': + unit = LimitUnit.kilobytes; + break; + case 'M': + unit = LimitUnit.megabytes; + break; + case 'G': + unit = LimitUnit.gigabytes; + break; + default: + throw new TAPException("Unknown limit unit (" + buf.reverse().toString() + ") for the property " + propertyName + ": \"" + value + "\"!"); + } + }else + throw new TAPException("Unknown limit unit (" + buf.reverse().toString() + ") for the property " + propertyName + ": \"" + value + "\"!"); + } + } + + return new Object[]{((numValue < 0) ? -1 : numValue),unit}; + } + +} diff --git a/src/tap/config/gums_table.txt b/src/tap/config/gums_table.txt new file mode 100644 index 0000000..9ec4968 --- /dev/null +++ b/src/tap/config/gums_table.txt @@ -0,0 +1,49 @@ +Name|DBType|JDBCType|TAPType|VOTableType +id|character varying(19)|varchar(19)|| +ra2|numeric(14,10)|numeric(14,10)|| +dec2|numeric(14,10)|numeric(14,10)|| +vmag|real|float4|| +gmag|real|float4|| +gbmag|real|float4|| +grmag|real|float4|| +gsmag|real|float4|| +ra|numeric(14,10)|numeric(14,10)|| +deg|numeric(14,10)|numeric(14,10)|| +r|double precision|float8|| +pmra|double precision|float8|| +pmde|double precision|float8|| +rv|double precision|float8|| +v_i|real|float4|| +av|real|float4|| +age|real|float4|| +alphafe|real|float4|| +balb|real|float4|| +e|real|float4|| +feh|real|float4|| +fi|smallint|int2|| +galb|real|float4|| +fm|smallint|int2|| +host|smallint|int2|| +i|real|float4|| +logg|real|float4|| +Omega|real|float4|| +mass|double precision|float8|| +mbol|real|float4|| +nc|smallint|int2|| +nt|smallint|int2|| +p|double precision|float8|| +omega|real|float4|| +t0|double precision|float8|| +phase|real|float4|| +pop|smallint|int2|| +beenv|double precision|float8|| +radius|double precision|float8|| +a|double precision|float8|| +teff|integer|int4|| +vamp|real|float4|| +vper|double precision|float8|| +vphase|real|float4|| +vtype|character varying(4)|varchar(4)|| +vsini|real|float4|| +recno|integer|int4|| +coord|spoint|spoint|| diff --git a/src/tap/config/tap_configuration_file.html b/src/tap/config/tap_configuration_file.html new file mode 100644 index 0000000..0bebdc9 --- /dev/null +++ b/src/tap/config/tap_configuration_file.html @@ -0,0 +1,720 @@ + + + + + TAP configuration file + + + + +

    TAP Configuration File

    +

    + All properties listed in the below table are all the possible TAP configuration properties. + Some of them are mandatory. If one of these properties is missing, the TAP Service will not able to start: + an error will be displayed immediately in the application server log and a HTTP 503 error will be sent when accessing the TAP URL. +

    +

    Besides, you should know that any property key not listed in this table will be ignored without error or warning message.

    +

    + However, any not allowed property value will generate a warning message in the application server log and the default value will be kept. + Thus, the TAP Service will be started and available but the desired configuration value will not be set. So, you should take a look + at the application server log every times you start the TAP Service! +

    + +

    Here is an empty minimum TAP configuration file: tap_min.properties and a complete one: tap_full.properties.

    + + +

    Important note: Any limit value is an integer and so can be at most: 231-1 bytes/rows = 2147483647B/R (or also for the byte unit: = 2147483kB = 2147MB = 2GB). + Otherwise, you should use the null value 0 to raise the limit constraint.

    + +

    Legend: M means that the property is mandatory. If nothing is written for the second column, the property is optional.

    + + / mandatory properties +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    PropertyTypeDescriptionExample
    General
    home_pagetext +

    This property lets set a custom home page. 4 different kinds of value are accepted:

    +
      +
    • nothing (default): the default home page provided by the library (just a simple HTML page displaying a list of all available TAP resources).
    • +
    • name or relative path of a file: this method MUST be chosen if the new home page is a JSP file. This file MUST be inside the directory WebContent of your web application.
    • +
    • a URI starting with file://: in this method the local file pointed by the URI will be merely returned when the home page will be requested.
    • +
    • a URL: here, a redirection toward this URL will be made at each request on the home page
    • +
    • a class name: the class name of an extension of tap.resource.HomePage which must replace the default home page resource. This class MUST have at least one constructor with exactly one parameter not NULL of type tap.resource.TAP.
    • +
    +

    By default, the default home page provided by the library is used.

    +
    • my_tap_homepage.jsp
    • jsp/my_tap_homepage.jsp
    • file:///home/foo/customHomePage.html
    • http://...
    • {aPackage.NewHomePage}
    home_page_mime_typetext +

    MIME type of the service home page.

    +

    This property is used only if the specified "home_page" is a local file path (i.e. if "home_page=file://...").

    +

    If no value is provided "text/html" will be set by default.

    +

    Default: text/html

    +
    • text/html (default)
    • text/plain
    • application/xml
    Provider
    provider_nametextName of the provider of the TAP Service.
    service_descriptiontextDescription of the TAP Service.
    Database (only if tap_factory = ø)
    database_accesstext +

    Method to use in order to create database connections.

    +

    Only two values are supported:

    +
      +
    • jndi: database connections will be supplied by a Datasource whose the JNDI name must be given. This method may propose connection pooling in function of the datasource configuration.
    • +
    • jdbc: the library will create itself connections when they will be needed thanks to the below JDBC parameters. This method does not propose any connection pooling.
    • +
    +
    • jdbc
    • jndi
    sql_translatortext +

    The translator to use in order to translate ADQL to a SQL compatible with the used DBMS and its spatial extension.

    +

    The TAP library supports only Postgresql (without spatial extension) and PgSphere for the moment. But you can provide your own SQL translator + (even if it does not have spatial features), by providing the name of a class (within brackets: {...}) that implements ADQLTranslator and which have at least an empty constructor.

    +
    • postgres
    • pgsphere
    • {apackage.MyADQLTranslator}
    sync_fetch_sizeinteger +

    Size of result blocks to fetch from the database when a ADQL query is executed in Synchronous mode.

    +

    Rather than fetching a query result in a whole, it may be possible to specify to the database + that results may be retrieved by blocks whose the size can be specified with this property. + If supported by the DBMS and the JDBC driver, this feature may help sparing memory and avoid + too much waiting time from the TAP /sync users (and thus, avoiding some HTTP client timeouts).

    +

    A negative or null value means that the default value of the JDBC driver will be used. Generally, + it means that the database must wait to have collected all data before sending them to the library.

    +

    Default: sync_fetch_size=10000

    +
    • 10000 (default)
    • 0 (wait for the the whole result)
    • 100000
    async_fetch_sizeinteger +

    Size of result blocks to fetch from the database when an ADQL query is executed in Asynchronous mode.

    +

    Rather than fetching a query result in a whole, it may be possible to specify to the database + that results may be retrieved by blocks whose the size can be specified with this property. + If supported by the DBMS and the JDBC driver, this feature may help sparing memory.

    +

    A negative or null value means that the default value of the JDBC driver will be used. Generally, + it means that the database must wait to have collected all data before sending them to the library.

    +

    Default: async_fetch_size=100000

    +
    • 100000 (default)
    • 0 (wait for the the whole result)
    • 1000000
    ⤷ JNDI datasource (only if database_access=jndi)
    datasource_jndi_nametext +

    JNDI name of the datasource. It should be defined in the web application (e.g. in the META-INF/context.xml file in tomcat).

    +
    • jdbc/postgres
    • jdbc/mydatasource
    • mydatasource
    ⤷ JDBC parameters (only if database_access=jdbc)
    jdbc_drivertext +

    JDBC driver path. By default, it is guessed in function of the database name provided + in the jdbc_url property. It MUST be provided if another DBMS is used or if the JDBC driver path + does not match the following ones:

    +
      +
    • Oracle : oracle.jdbc.OracleDriver
    • +
    • PostgreSQL: org.postgresql.Driver
    • +
    • MySQL : com.mysql.jdbc.Driver
    • +
    • SQLite : org.sqlite.JDBC
    • +
    +
    oracle.jdbc.driver.OracleDriver
    jdbc_urltext +

    It must be a JDBC driver URL.

    +

    Note: The username, password or other parameters may be included in it, but in this case, the corresponding properties + should leave empty or not provided at all.

    +
    • jdbc:postgresql:mydb
    • jdbc:postgresql://myserver:1234/mydb
    • jdbc:sqlite:Database.db
    db_usernametext +

    Mandatory if the username is not already provided in jdbc_url

    +

    Username used to access to the database.

    +
    db_passwordtext +

    Mandatory if the password is not already provided in jdbc_url

    +

    Password used by db_username to access to the database.

    +

    Warning: No password encryption can be done in this configuration file for the moment.

    +
    Metadata
    metadatatext +

    Define the way the library must get the list of all schemas, tables and columns to publish and all their metadata (e.g. utype, description, type, ...)

    +

    In its current state, the library proposes three methods:

    +
      +
    1. Parse a TableSet XML document and load its content into the database schema TAP_SCHEMA (note: this schema is first erased and rebuilt by the library).
    2. +
    3. Get all metadata from the database schema TAP_SCHEMA.
    4. +
    5. Build yourself the metadata of your service by creating an extension of tap.metadata.TAPMetadata. This extension must have either an empty constructor + or a constructor with exactly 3 parameters of type UWSFileManager, TAPFactory and TAPLog ; if both constructor are provided, only the one with parameters will be used.
    6. +
    +
    • xml
    • db
    • {apackage.MyTAPMetadata}
    +
    metadata_filetext +

    Mandatory if the value of "metadata" is "xml".

    +

    Local file path to the TableSet XML document.

    +

    The XML document must implement the schema TableSet defined by VODataService.

    +

    The file path must be either an absolute local file path or a file path relative to WebContent + (i.e. the web application directory in which there are WEB-INF and META-INF).

    +
    • /home/foo/my_metadata.xml
    • my_metadata.xml
    • WEB-INF/my_metadata.xml
    Files
    file_managertext +

    Type of the file manager.

    +

    Accepted values are: local (to manage files on the local system). + You can also add another way to manage files by providing the name (within brackets: {...}) of a class implementing TAPFileManager and having at least one constructor with only a java.util.Properties parameter.

    +
    • local
    • {apackage.MyTAPFileManager}
    file_root_pathtext +

    Local file path of the directory in which all TAP files (logs, errors, job results, backup, ...) must be.

    +

    The file path must be either an absolute local directory path or a directory path relative to WebContent + (i.e. the web application directory in which there are WEB-INF and META-INF).

    +
    • /home/my_home_dir/tapFiles
    • tapFiles
    • WEB-INF/tapFiles
    directory_per_userboolean +

    Tells whether a directory should be created for each user. If yes, the user directory will be named with the user ID. In this directory, there will be error files, job results + and it may be the backup file of the user.

    +

    Default: true

    +
    • true (default)
    • false
    group_user_directoriesboolean +

    Tells whether user directories must be grouped. If yes, directories are grouped by the first letter found in the user ID.

    +

    Default: false

    +
    • true
    • false (default)
    default_retention_periodinteger +

    The default period (in seconds) to keep query results. The prefix "default" means here that this value is put by default by the TAP Service + if the client does not provide a value for it.

    +

    The default period MUST be less or equals to the maximum retention period. If this rule is not respected, the default retention period is set immediately + to the maximum retention period.

    +

    A negative or null value means there is no restriction on the default retention period: job results will be kept forever. Float values are not allowed.

    +

    By default query results are kept forever: default_retention_period=0.

    86400 (1 day)
    max_retention_periodinteger +

    The maximum period (in seconds) to keep query results. The prefix "max" means here that the client can not set a retention period greater than this one.

    +

    The maximum period MUST be greater or equals to the default retention period. If this rule is not respected, the default retention period is set immediately + to the maximum retention period.

    +

    A negative or null value means there is no restriction on the maximum retention period: the job results will be kept forever. Float values are not allowed.

    +

    Default: max_retention_period=0 (results kept for ever)

    604800 (1 week)
    Log files
    min_log_leveltext +

    Minimum level that a message must have in order to be logged.

    +

    5 possible values:

    p> +
      +
    • DEBUG: every messages are logged.
    • +
    • INFO: every messages EXCEPT DEBUG are logged.
    • +
    • WARNING: every messages EXCEPT DEBUG and INFO are logged.
    • +
    • ERROR: only ERROR and FATAL messages are logged.
    • +
    • FATAL: only FATAL messages are logged.
    • +
    +

    Default: DEBUG (every messages are logged)

    +
    • DEBUG
    • INFO
    • WANRING
    • ERROR
    • FATAL
    log_rotationtext +

    Frequency of the log file rotation. That's to say, logs will be written in a new file after this period. This avoid having too big log files. + Old log files are renamed so that highlighting its logging period.

    +

    The frequency string must respect the following syntax:

    +
      +
    • 'D' hh mm: daily schedule at hh:mm
    • +
    • 'W' dd hh mm: weekly schedule at the given day of the week (1:sunday, 2:monday, ..., 7:saturday) at hh:mm
    • +
    • 'M' dd hh mm: monthly schedule at the given day of the month at hh:mm
    • +
    • 'h' mm: hourly schedule at the given minute
    • +
    • 'm': scheduled every minute (for completness :-))
    • +
    +

    Where: hh = integer between 0 and 23, mm = integer between 0 and 59, dd (for 'W') = integer between 1 and 7 (1:sunday, 2:monday, ..., 7:saturday), + dd (for 'M') = integer between 1 and 31.

    +

    Warning: + The frequency type is case sensitive! Then you should particularly pay attention at the case + when using the frequency types 'M' (monthly) and 'm' (every minute). +

    +

    Default: D 0 0 (daily at midnight)

    +
    • D 6 30
    • W 2 6 30
    • M 2 6 30
    • H 10
    • m
    UWS Backup (only if tap_factory = ø)
    backup_frequencytext or integer +

    Frequency at which the UWS service (that's to say, all its users and jobs) must be backuped.

    +

    Allowed values are: never (no backup will never be done), user_action (each time a user does a writing action, like creating or execution a job), a time (must be positive and not null) in milliseconds.

    +

    The value user_action can be used ONLY IF backup_mode=true.

    +

    Default: backup_frequency=never (no backup)

    +
    • never (default)
    • user_action
    • 3600000 (1 hour)
    backup_by_usertext +

    Tells whether the backup must be one file for every user (false), or one file for each user (true). This second option should be chosen if your TAP Service is organizing its files by user directories ; see the property directory_per_user.

    +

    This option can be enabled ONLY IF a user identification method is provided ; see property user_identifier.

    +

    Default: false

    +
    • false (default)
    • true
    Asynchronous jobs management
    max_async_jobsinteger +

    Maximum number of asynchronous jobs that can run simultaneously.

    +

    A negative or null value means there is no restriction on the number of running asynchronous jobs.

    +

    Default: max_async_jobs=0 (no restriction)

    +
    • 0 (default)
    • 10
    Query Execution
    default_execution_durationinteger +

    Default time (in milliseconds) for query execution. The prefix "default" means here that the execution duration will be this one if the client does not set one.

    +

    The default duration MUST be less or equals to the maximum execution duration. If this rule is not respected, the default execution duration is set immediately + to the maximum execution duration.

    +

    A negative or null value means there is no restriction on the default execution duration: the execution could never end. Float values are not allowed.

    +

    Default: default_execution_duration=0 (no restriction)

    +
    600000 (10 minutes)
    max_execution_durationinteger +

    Maximum time (in milliseconds) for query execution. The prefix "max" means here that the client can not set a time greater than this one.

    +

    The maximum duration MUST be greater or equals to the default execution duration. If this rule is not respected, the default execution duration is set immediately + to the maximum execution duration.

    +

    A negative or null value means there is no restriction on the maximum execution duration: the execution could never end. Float values are not allowed.

    +

    Default: max_execution_duration=0 (no restriction)

    +
    3600000 (1 hour)
    Output
    output_formatstext +

    Comma separated list of output formats for query results.

    +

    Allowed values are: votable (or 'vot'), fits, text, csv, tsv, json and html.

    +

    The special value "ALL" will select all formats provided by the library.

    +

    The VOTable format may be more detailed with the following syntax: (serialization,version):mime_type:short_mime_type. + The MIME type part and the parameters part may be omitted (e.g. votable:application/xml:votable , votable(td,1.3)]). + Empty string values are allowed for each values (e.g. votable():: , votable(td)::votable).

    +

    It is also possible to define a custom Separated Value format, different from CSV and TSV, thanks to the following syntax: sv(separator):mime_type:short_mime_type. + On the contrary to the VOTable syntax, the parameter (i.e. separator) MUST BE provided. The MIME type part may be omitted ; then the MIME type will be set by default to text/plain.

    +

    There is finally a last possible value: a class name of a class implementing OutputFormat and having at least one constructor with exactly one parameter of type tap.ServiceConnection.

    +

    Default: ALL

    +
    • votable
    • vot
    • vot(td,1.2)::votable
    • json,html ,csv, text
    • sv(|):text/psv:psv
    • sv([])
    • {apackage.FooOutputFormat}
    output_default_limittext +

    Default limit for the result output. The prefix "default" means here that this value will be set if the client does not provide one.

    +

    This limit can be expressed in only one unit: rows.

    +

    A negative value means there is no restriction on this limit. Float values are not allowed.

    +

    Obviously this limit MUST be less or equal than output_max_limit.

    +

    Default: output_default_limit=-1 (no restriction)

    +
    • -1 (default)
    • 20
    • 20r
    • 20R
    output_max_limittext +

    Maximum limit for the result output. The prefix "max" means here that the client can not set a limit greater than this one.

    +

    This limit can be expressed in only one unit: rows.

    +

    A negative value means there is no restriction on this limit. Float values are not allowed.

    +

    Obviously this limit MUST be greater or equal than output_default_limit.

    +

    Default: output_max_limit=-1 (no restriction)

    +
    • -1 (default)
    • 1000
    • 10000r
    • 10000R
    Upload
    upload_enabledboolean +

    Tells whether the Upload must be enabled. If enabled, files can be uploaded in the file_root_path, + the corresponding tables can be added inside the UPLOAD_SCHEMA of the database, queried and then deleted.

    +

    By default, the Upload is disabled: upload_enabled=false.

    +
    • false (default)
    • true
    upload_default_db_limittext +

    Default limit for the number of uploaded records that can be inserted inside the database. The prefix "default" means here that this value will be set if the client does not provide one.

    +

    This limit can be expressed with 2 types: rows or bytes. For rows, you just have to suffix the value by a "r" (upper- or lower-case) + or by nothing (by default, nothing will mean "rows"). For bytes, you have to suffix the numeric value by "B", "kB", "MB" or "GB". + Here, unit is case sensitive. No other storage unit is allowed.

    +

    A negative value means there is no restriction on this limit. Float values are not allowed.

    +

    Warning! Obviously this limit MUST be less or equal than upload_max_db_limit, and MUST be of the same type as it. + If the chosen type is rows, this limit MUST also be strictly less than upload_max_file_size.

    +

    Default: upload_default_db_limit=-1 (no restriction)

    +
    • -1 (default)
    • 20
    • 20r
    • 20R
    • 200kB
    upload_max_db_limittext +

    Maximum limit for the number of uploaded records that can be inserted inside the database. The prefix "max" means here that the client can not set a limit greater than this one.

    +

    This limit can be expressed with 2 types: rows or bytes. For rows, you just have to suffix the value by a "r" (upper- or lower-case), + with nothing (by default, nothing will mean "rows"). For bytes, you have to suffix the numeric value by "B", "kB", "MB" or "GB". + Here, unit is case sensitive. No other storage unit is allowed.

    +

    A negative value means there is no restriction on this limit. Float values are not allowed.

    +

    Warning! Obviously this limit MUST be greater or equal than upload_default_db_limit, and MUST be of the same type as it. + If the chosen type is rows, this limit MUST also be strictly less than upload_max_file_size.

    +

    Default: upload_max_db_limit=-1 (no restriction)

    +
    • -1 (default)
    • 10000
    • 10000r
    • 10000R
    • 1MB
    upload_max_file_sizetext +

    Maximum allowed size for the uploaded file.

    +

    This limit MUST be expressed in bytes. Thus, you have to suffix the numeric value by "B", "kB", "MB" or "GB". + Here, unit is case sensitive. No other storage unit is allowed.

    +

    Warning! When the upload is enabled, there must be a maximum file size. Here, no "unlimited" value is possible ; 0 and any negative value are not allowed.

    +

    Warning! In function of the chosen upload_max_db_limit type, upload_max_file_size MUST be greater in order to figure out the file metadata part.

    +

    Default: upload_max_file_size=2147483647B (~2GB ; maximum possible value)

    +
    • 2147483647B (default)
    • 2MB
    User identification
    user_identifiertext +

    Class to use in order to identify a user of the TAP service. The same instance of this class will be used for every request sent to the service.

    +

    + The value of this property MUST be a class name (with brackets: {...}) of a class implementing the interface uws.service.UserIdentifier. + This class MUST have one of its constructors with no parameter. +

    +

    By default, no identification is performed ; all users are then anonymous and their jobs can be seen by everybody.

    +
    {apackage.FooUserIdentifier}
    ADQL restrictions
    coordinate_systemstext +

    Comma-separated list of all allowed coordinate systems.

    +

    + Each item of the list be a kind of regular expression respecting the following syntax: + Frame RefPos Flavor. In other words, it must be a string of exactly + 3 parts. Each of this part is a single value, a list of allowed values or a * meaning all + values. A list of values must be indicated between parenthesis and values must be separated by a pipe. +

    +

    Allowed values for Frame are: ICRS, FK4, + FK5, ECLIPTIC, GALACTIC and UNKNOWNFRAME.

    +

    Allowed values for RefPos are: BARYCENTER, GEOCENTER, + HELIOCENTER, LSR, TOPOCENTER, RELOCATABLE + and UNKNOWNREFPOS.

    +

    Allowed values for Flavor are: CARTESIAN2, CARTESIAN3 and + SPHERICAL2.

    +

    + If the special value NONE is given instead of a list of allowed coordinate systems, + no coordinate system will be allowed. And if the list is empty, any coordinate system will be allowed. +

    +

    By default, any coordinate system is allowed.

    +
    • ø (default)
    • NONE
    • ICRS * *
    • ICRS * *, ECLIPTIC * (CARTESIAN2 | SPHERICAL2)
    geometriestext +

    Comma-separated list of all allowed geometries.

    +

    + Each item of the list must be the name (whatever is the case) of an ADQL geometrical function (e.g. INTERSECTS, COORDSYS, POINT) to allow. + If the list is empty (no item), all functions are allowed. And if the special value NONE is given, no ADQL function will be allowed. +

    +

    By default, all ADQL geometrical functions are allowed.

    +
    • ø (default)
    • NONE
    • CONTAINS, intersects, Point, Box, CIRCLE
    udfstext +

    Comma-separated list of all allowed UDFs (User Defined Functions).

    +

    + Each item of the list must have the following syntax: [fct_signature] or [fct_signature, className]. + fct_function is the function signature. Its syntax is the same as in TAPRegExt. + className is the name of a class extending UserDefinedFunction. An instance of this class will replace any reference of a UDF + written in an ADQL function with the associated signature. A class name must be specified if the function to represent has a signature + (and more particularly a name) different in ADQL and in SQL. +

    +

    + If the list is empty (no item), all unknown functions are forbidden. And if the special value ANY is given, any unknown function is allowed ; + consequently the unknown ADQL functions will be translated into SQL as they are in ADQL. +

    +

    By default, no unknown function is allowed.

    +
    • ø (default)
    • ANY
    • [trim(txt String) -> String], [random() -> DOUBLE]
    • [newFct(x double)->double, {apackage.MyNewFunction}]
    Additional TAP Resources
    additional_resourcestext +

    Comma-separated list of additional TAP resources/end-point.

    +

    + By default, the following standard TAP resources are already existing: /sync, /async, /tables, /capabilities and /availability. + With this property, you can add a custom resource to your TAP service (e.g. /adqlValidator, /admin). +

    +

    + Each item of the list MUST be the name of a class implementing tap.resource.TAPResource. This class MUST have at least one constructor + with exactly one parameter of type tap.resource.TAP. +

    +

    + The string returned by tap.resource.TAPResource.getName() will be the resource name, following the root TAP service URL (e.g. if getName() + returns "foo", then its access URL will "{tapRoot}/foo"). Then, it is possible to replace TAP resources already existing by using the same + name (e.g. if getName() returns "sync", the /sync resource won't be anymore the default Sync resource of this library but your new resource). +

    +

    By default, this list is empty ; only the standard TAP resources exist.

    +
    {aPackage.QuickADQLValidator}
    Custom TAP Factory
    tap_factorytext +

    Class to use in replacement of the default TAPFactory.

    +

    + This property must be a class name (given between {...}). It must reference an implementation of TAPFactory. + This implementation must have at least one constructor with exactly one parameter of type ServiceConnection. +

    +

    + It is recommended to extend an existing implementation such as: + tap.AbstractTAPFactory or tap.config.ConfigurableTAPFactory. +

    +

    By default, the default TAPFactory (tap.config.ConfigurableTAPFactory) is used and may use all properties related to the backup management, + the database access and the ADQL translation.

    +
    {aPackage.MyTAPFactory}
    + + + \ No newline at end of file diff --git a/src/tap/config/tap_full.properties b/src/tap/config/tap_full.properties new file mode 100644 index 0000000..b223d59 --- /dev/null +++ b/src/tap/config/tap_full.properties @@ -0,0 +1,536 @@ +########################################################## +# FULL TAP CONFIGURATION FILE # +# # +# TAP Version: 2.0 # +# Date: 13 April 2015 # +# Author: Gregory Mantelet (ARI) # +# # +########################################################## + +########### +# GENERAL # +########### + +# [OPTIONAL] +# This property lets set a custom home page. +# +# 4 different kinds of value are accepted: +# * nothing (default): the default home page provided by the library (just a simple HTML page displaying a list of all available TAP resources). +# * name or relative path of a file: this method MUST be chosen if the new home page is a JSP file. This file MUST be inside the directory WebContent of your web application. +# * a URI starting with file://: in this method the local file pointed by the URI will be merely returned when the home page will be requested. +# * a URL: here, a redirection toward this URL will be made at each request on the home page +# * a class name: the class name of an extension of tap.resource.HomePage which must replace the default home page resource. +# This class MUST have at least one constructor with exactly one parameter not NULL of type tap.resource.TAP. +home_page = + +# [OPTIONAL] +# MIME type of the service home page. +# +# This property is used only if the specified "home_page" is a local file path (i.e. if "home_page=file://..."). +# +# If no value is provided "text/html" will be set by default. +# +# Default: text/html +home_page_mime_type = + +############ +# PROVIDER # +############ + +# [OPTIONAL] +# Name of the provider of the TAP Service. +provider_name = ARI + +# [OPTIONAL] +# Description of the TAP Service. +service_description = My TAP Service is so amazing! You should use it with your favorite TAP client. + +############ +# DATABASE # +############ + +# [MANDATORY] +# Method to use in order to create database connections. +# +# Only two values are supported: +# * jndi: database connections will be supplied by a Datasource whose the JNDI name must be given. This method may propose connection pooling in function of the datasource configuration. +# * jdbc: the library will create itself connections when they will be needed thanks to the below JDBC parameters. This method does not propose any connection pooling. +# +# Allowed values: jndi, jdbc. +database_access = + +# [MANDATORY] +# The translator to use in order to translate ADQL to a SQL compatible with the used DBMS and its spatial extension. +# +# The TAP library supports only Postgresql (without spatial extension) and PgSphere for the moment. But you can provide your own SQL translator +# (even if it does not have spatial features), by providing the name of a class (within brackets: {...}) that implements ADQLTranslator (for instance: {apackage.MyADQLTranslator}) +# and which have at least an empty constructor. +# +# Allowed values: postgres, pgsphere, a class name +sql_translator = postgres + +# [OPTIONAL] +# Size of result blocks to fetch from the database when a ADQL query is executed in Synchronous mode. +# +# Rather than fetching a query result in a whole, it may be possible to specify to the database that +# results may be retrieved by blocks whose the size can be specified with this property. If supported by +# the DBMS and the JDBC driver, this feature may help sparing memory and avoid too much waiting time from +# the TAP /sync users (and thus, avoiding some HTTP client timeouts). +# +# A negative or null value means that the default value of the JDBC driver will be used. Generally, it means +# that the database must wait to have collected all data before sending them to the library. +# +# Default: sync_fetch_size=10000 +sync_fetch_size = 10000 + +# [OPTIONAL] +# Size of result blocks to fetch from the database when an ADQL query is executed in Asynchronous mode. +# +# Rather than fetching a query result in a whole, it may be possible to specify to the database that +# results may be retrieved by blocks whose the size can be specified with this property. If supported by +# the DBMS and the JDBC driver, this feature may help sparing memory. +# +# A negative or null value means that the default value of the JDBC driver will be used. Generally, it means +# that the database must wait to have collected all data before sending them to the library. +# +# Default: async_fetch_size=100000 +async_fetch_size=100000 + +############################# +# IF DATABASE ACCESS = JNDI # +############################# + +# [MANDATORY] +# JNDI name of the datasource pointing toward the database to use. +# It should be defined in the web application (e.g. in the META-INF/context.xml file in tomcat). +datasource_jndi_name = + +############################# +# IF DATABASE ACCESS = JDBC # +############################# + +# [MANDATORY] +# JDBC driver URL pointing toward the database to use. +# +# Note: The username, password or other parameters may be included in it, but in this case, the corresponding properties should leave empty or not provided at all. +jdbc_url = + +# [OPTIONAL] +# JDBC driver path. +# +# By default, it is guessed in function of the database name provided in the jdbc_url property. It MUST be provided if another DBMS is used or if the JDBC driver path does not match the following ones: +# * Oracle : oracle.jdbc.OracleDriver +# * PostgreSQL: org.postgresql.Driver +# * MySQL : com.mysql.jdbc.Driver +# * SQLite : org.sqlite.JDBC +#jdbc_driver = + +# [MANDATORY] +# Mandatory if the username is not already provided in jdbc_url +# Username used to access to the database. +db_username = + +# [MANDATORY] +# Mandatory if the password is not already provided in jdbc_url +# Password used by db_username to access to the database. +# +# Note: No password encryption can be done in this configuration file for the moment. +db_password = + +############ +# METADATA # +############ + +# [MANDATORY] +# Metadata fetching method. +# +# The value of this key defines the way the library will get the list of all schemas, tables and columns to publish and all their metadata (e.g. utype, description, type, ...). +# +# In its current state, the library proposes three methods: +# 1/ Parse a TableSet XML document and load its content into the database schema TAP_SCHEMA (note: this schema is first erased and rebuilt by the library). +# 2/ Get all metadata from the database schema TAP_SCHEMA. +# 3/ Build yourself the metadata of your service by creating an extension of tap.metadata.TAPMetadata. This extension must have either an empty constructor +# or a constructor with exactly 3 parameters of type UWSFileManager, TAPFactory and TAPLog ; if both constructor are provided, only the one with parameters will be used. +# +# Allowed values: xml, db or a full class name (between {}). +metadata = + +# [MANDATORY] +# Mandatory if the value of "metadata" is "xml". +# Local file path to the TableSet XML document. +# The XML document must implement the schema TableSet defined by VODataService. +# The file path must be either an absolute local file path or a file path relative to WebContent (i.e. the web application directory in which there are WEB-INF and META-INF). +metadata_file = + +######### +# FILES # +######### + +# [MANDATORY] +# Type of the file manager. +# +# Accepted values are: local (to manage files on the local system). You can also add another way to manage files by providing +# the name (within brackets: {...}) of a class implementing TAPFileManager and having at least one constructor with only a +# java.util.Properties parameter. +# +# Allowed values: local, a class name. +file_manager = local + +# [MANDATORY] +# Local file path of the directory in which all TAP files (logs, errors, job results, backup, ...) must be. +# The file path must be either an absolute local directory path or a directory path relative to WebContent (i.e. the web application directory in which there are WEB-INF and META-INF). +file_root_path = + +# [OPTIONAL] +# Tells whether a directory should be created for each user. +# +# If yes, the user directory will be named with the user ID. In this directory, there will be error files, job results and it may be the backup file of the user. +# +# Allowed values: true (default), false. +directory_per_user = true + +# [OPTIONAL] +# Tells whether user directories must be grouped. +# +# If yes, directories are grouped by the first letter found in the user ID. +# +# Allowed values: true (default), false. +group_user_dir = true + +# [OPTIONAL] +# The default period (in seconds) to keep query results. +# +# The prefix "default" means here that this value is put by default by the TAP Service if the client does not provide a value for it. +# +# The default period MUST be less or equals to the maximum retention period. If this rule is not respected, the default retention period is set +# immediately to the maximum retention period. +# +# A negative or null value means there is no restriction on the default retention period: job results will be kept forever. Float values are not allowed. +# +# Default: 0 (results kept forever). +default_retention_period = 0 + +# [OPTIONAL] +# The maximum period (in seconds) to keep query results. +# +# The prefix "max" means here that the client can not set a retention period greater than this one. +# +# The maximum period MUST be greater or equals to the default retention period. If this rule is not respected, the default retention period is set +# immediately to the maximum retention period. +# +# A negative or null value means there is no restriction on the maximum retention period: the job results will be kept forever. Float values are not allowed. +# +# Default: 0 (results kept forever). +max_retention_period = 0 + +############# +# LOG FILES # +############# + +# [OPTIONAL] +# Minimum level that a message must have in order to be logged. +# +# 5 possible values: +# * DEBUG: every messages are logged. +# * INFO: every messages EXCEPT DEBUG are logged. +# * WARNING: every messages EXCEPT DEBUG and INFO are logged. +# * ERROR: only ERROR and FATAL messages are logged. +# * FATAL: only FATAL messages are logged. +# +# Default: DEBUG (every messages are logged) +min_log_level = + +# [OPTIONAL] +# Frequency of the log file rotation. That's to say, logs will be written in a new file after this period. This avoid having too big log files. +# Old log files are renamed so that highlighting its logging period. +# +# The frequency string must respect the following syntax: +# 'D' hh mm: daily schedule at hh:mm +# 'W' dd hh mm: weekly schedule at the given day of the week (1:sunday, 2:monday, ..., 7:saturday) at hh:mm +# 'M' dd hh mm: monthly schedule at the given day of the month at hh:mm +# 'h' mm: hourly schedule at the given minute +# 'm': scheduled every minute (for completness :-)) +# Where: hh = integer between 0 and 23, mm = integer between 0 and 59, dd (for 'W') = integer between 1 and 7 (1:sunday, 2:monday, ..., 7:saturday), dd (for 'M') = integer between 1 and 31. +# +# Warning: The frequency type is case sensitive! Then you should particularly pay attention at the case when using the frequency types 'M' (monthly) and 'm' (every minute). +# +# Note: this property is ignored if the file manager is not any more an extension of uws.service.file.LocalUWSFileManager. +# +# Default: D 0 0 (daily at midnight) +log_rotation = + +############## +# UWS_BACKUP # +############## + +# [OPTIONAL] +# Frequency at which the UWS service (that's to say, all its users and jobs) must be backuped. +# +# Allowed values: never (no backup will never be done ; default), user_action (each time a user does a writing action, like creating or execution a job), +# a time (must be positive and not null) in milliseconds. +# +# The value user_action can be used ONLY IF backup_mode=true. +# +# Default: never +backup_frequency = never + +# [OPTIONAL] +# Tells whether the backup must be one file for every user (false), or one file for each user (true). +# This second option should be chosen if your TAP Service is organizing its files by user directories ; +# see the property directory_per_user. +# +# This option can be enabled ONLY IF a user identification method is provided ; see property user_identifier. +# +# Default: false +backup_by_user = false + +##################### +# ASYNCHRONOUS JOBS # +##################### + +# [OPTIONAL] +# Maximum number of asynchronous jobs that can run simultaneously. +# +# A negative or null value means there is no restriction on the number of running asynchronous jobs. +# +# Default: there is no restriction => max_async_jobs=0. +max_async_jobs = 0 + +################### +# QUERY_EXECUTION # +################### + +# [OPTIONAL] +# Default time (in milliseconds) for query execution. +# +# The prefix "default" means here that the execution duration will be this one if the client does not set one. +# +# The default duration MUST be less or equals to the maximum execution duration. If this rule is not respected, the default execution duration is set +# immediately to the maximum execution duration. +# +# A negative or null value means there is no restriction on the default execution duration: the execution could never end. Float values are not allowed. +# +# Default: there is no restriction => default_execution_duration=0. +default_execution_duration = 0 + +# [OPTIONAL] +# Maximum time (in milliseconds) for query execution. +# +# The prefix "max" means here that the client can not set a time greater than this one. +# +# The maximum duration MUST be greater or equals to the default execution duration. If this rule is not respected, the default execution duration is set +# immediately to the maximum execution duration. +# +# A negative or null value means there is no restriction on the maximum execution duration: the execution could never end. Float values are not allowed. +# +# Default: there is no restriction => max_execution_duration=0. +max_execution_duration = 0 + +########## +# OUTPUT # +########## + +# [OPTIONAL] +# Comma separated list of output formats for query results. +# +# Allowed values are: votable (or 'vot'), fits, text, csv, tsv, json and html. +# +# The special value "ALL" will select all formats provided by the library. +# +# The VOTable format may be more detailed with the following syntax: (serialization,version):mime_type:short_mime_type. +# The MIME type part and the parameters part may be omitted (e.g. votable:application/xml:votable , votable(td,1.3)]). +# Empty string values are allowed for each values (e.g. votable():: , votable(td)::votable). +# +# It is also possible to define a custom Separated Value format, different from CSV and TSV, thanks to the following syntax: sv(separator):mime_type:short_mime_type. +# On the contrary to the VOTable syntax, the parameter (i.e. separator) MUST BE provided. +# The MIME type part may be omitted ; then the MIME type will be set by default to text/plain. +# +# There is finally a last possible value: a class name of a class implementing OutputFormat and having at least one constructor with exactly one parameter of type +# tap.ServiceConnection. +# +# Default: ALL +output_formats = ALL + +# [OPTIONAL] +# Default limit for the result output. +# +# The prefix "default" means here that this value will be set if the client does not provide one. +# +# This limit can be expressed in only one unit: rows. +# +# A negative value means there is no restriction on this limit. Float values are not allowed. +# +# Obviously this limit MUST be less or equal than output_max_limit. +# +# Default: there is no restriction: output_default_limit=-1 +output_default_limit = -1 + +# [OPTIONAL] +# Maximum limit for the result output. The prefix "max" means here that the client can not set a limit greater than this one. +# +# This limit can be expressed in only one unit: rows. +# +# A negative value means there is no restriction on this limit. Float values are not allowed. +# +# Obviously this limit MUST be greater or equal than output_default_limit. +# +# Default: there is no restriction => output_max_limit=-1 +output_max_limit = -1 + +########## +# UPLOAD # +########## + +# [OPTIONAL] +# Tells whether the Upload must be enabled. +# +# If enabled, files can be uploaded in the file_root_path, the corresponding tables can be added inside the UPLOAD_SCHEMA +# of the database, queried and then deleted. +# +# Allowed values: true, false (default). +upload_enabled = false + +# [OPTIONAL] +# Default limit for the number of uploaded records that can be inserted inside the database. +# +# The prefix "default" means here that this value will be set if the client does not provide one. +# +# This limit can be expressed with 2 types: rows or bytes. For rows, you just have to suffix the value by a "r" (upper- or lower-case), +# with nothing (by default, nothing will mean "rows"). For bytes, you have to suffix the numeric value by "b", "kb", "Mb" or "Gb". Here, +# unit is case sensitive (except for the last character: "b"). No other storage unit is allowed. +# +# A negative value means there is no restriction on this limit. Float values are not allowed. +# +# Obviously this limit MUST be less or equal than upload_max_db_limit. +# +# Default: there is no restriction: upload_default_db_limit=-1 +upload_default_db_limit = -1 + +# [OPTIONAL] +# Maximum limit for the number of uploaded records that can be inserted inside the database. +# +# The prefix "max" means here that the client can not set a limit greater than this one. +# +# This limit can be expressed with 2 types: rows or bytes. For rows, you just have to suffix the value by a "r" (upper- or lower-case), +# with nothing (by default, nothing will mean "rows"). For bytes, you have to suffix the numeric value by "b", "kb", "Mb" or "Gb". Here, +# unit is case sensitive (except for the last character: "b"). No other storage unit is allowed. +# +# A negative value means there is no restriction on this limit. Float values are not allowed. +# +# Obviously this limit MUST be greater or equal than upload_default_db_limit. +# +# Default: there is no restriction: upload_max_db_limit=-1 +upload_max_db_limit = -1 + +# [OPTIONAL] +# Maximum allowed size for the uploaded file. +# +# This limit MUST be expressed in bytes. Thus, you have to suffix the numeric value by "B", "kB", "MB" or "GB". Here, unit is case sensitive. No other storage unit is allowed. +# +# Warning! When the upload is enabled, there must be a maximum file size. Here, no "unlimited" value is possible ; 0 and any negative value are not allowed. +# +# Warning! In function of the chosen upload_max_db_limit type, upload_max_file_size MUST be greater in order to figure out the file metadata part. +# +# Default: upload_max_file_size=2147483647B (~2GB ; maximum possible value) +upload_max_file_size = 2147483647B + +####################### +# USER IDENTIFICATION # +####################### + +# [OPTIONAL] +# Class to use in order to identify a user of the TAP service. +# +# The same instance of this class will be used for every request sent to the service. +# +# The value of this property MUST be a class name (with brackets: {...}) of a class implementing the interface uws.service.UserIdentifier. +# This class MUST have one of its constructors with no parameter. +# +# Default: no identification is performed => all users are then anonymous and their jobs can be seen by everybody. +user_identifier = + +###################### +# COORDINATE SYSTEMS # +###################### + +# [OPTIONAL] +# Comma-separated list of all allowed coordinate systems. +# +# Each item of the list be a kind of regular expression respecting the following syntax: Frame RefPos Flavor. In other words, it must be a string of exactly 3 parts. Each of this part is a single value, a list of allowed values or a * meaning all values. A list of values must be indicated between parenthesis and values must be separated by a pipe. +# +# Allowed values for Frame are: ICRS, FK4, FK5, ECLIPTIC, GALACTIC and UNKNOWNFRAME. +# Allowed values for RefPos are: BARYCENTER, GEOCENTER, HELIOCENTER, LSR, TOPOCENTER, RELOCATABLE and UNKNOWNREFPOS. +# Allowed values for Flavor are: CARTESIAN2, CARTESIAN3 and SPHERICAL2. +# +# If the special value NONE is given instead of a list of allowed coordinate systems, no coordinate system will be allowed. And if the list is empty, any coordinate system will be allowed. +# +# By default, any coordinate system is allowed. +coordinate_systems = + +############## +# GEOMETRIES # +############## + +# [OPTIONAL] +# Comma-separated list of all allowed geometries. +# +# Each item of the list must be the name (whatever is the case) of an ADQL geometrical function (e.g. INTERSECTS, COORDSYS, POINT) to allow. +# If the list is empty (no item), all functions are allowed. And if the special value NONE is given, no ADQL function will be allowed. +# +# Default: all ADQL geometrical functions are allowed. +geometries = + +################################# +# USER DEFINED FUNCTIONS (UDFs) # +################################# + +# [OPTIONAL] +# Comma-separated list of all allowed UDFs (User Defined Functions). +# +# Each item of the list must have the following syntax: [fct_signature] or [fct_signature, className]. fct_function is the function signature. +# Its syntax is the same as in TAPRegExt. className is the name of a class extending UserDefinedFunction. An instance of this class will replace +# any reference of a UDF written in an ADQL function with the associated signature. A class name must be specified if the function to represent +# has a signature (and more particularly a name) different in ADQL and in SQL. +# +# If the list is empty (no item), all unknown functions are forbidden. And if the special value ANY is given, any unknown function is allowed ; +# consequently the unknown ADQL functions will be translated into SQL as they are in ADQL. +# +# Default: no unknown function is allowed. +udfs = + +######################## +# ADDITIONAL RESOURCES # +######################## + +# [OPTIONAL] +# Comma-separated list of additional TAP resources/end-point. +# +# By default, the following standard TAP resources are already existing: /sync, /async, /tables, /capabilities and /availability. +# With this property, you can add a custom resource to your TAP service (e.g. /adqlValidator, /admin). +# +# Each item of the list MUST be the name of a class implementing tap.resource.TAPResource. This class MUST have at least one constructor with +# exactly one parameter of type tap.resource.TAP. +# +# The string returned by tap.resource.TAPResource.getName() will be the resource name, following the root TAP service URL (e.g. if getName() +# returns "foo", then its access URL will "{tapRoot}/foo"). Then, it is possible to replace TAP resources already existing by using the same +# name (e.g. if getName() returns "sync", the /sync resource won't be anymore the default Sync resource of this library but your new resource). +# +# By default, this list is empty ; only the standard TAP resources exist. +additional_resources = + +###################### +# CUSTOM TAP_FACTORY # +###################### + +# [OPTIONAL] +# Class to use in replacement of the default TAPFactory. +# +# This property must be a class name (given between {...}). It must reference an implementation of TAPFactory. +# This implementation must have at least one constructor with exactly one parameter of type ServiceConnection. +# +# It is recommended to extend an existing implementation such as: +# tap.AbstractTAPFactory or tap.config.ConfigurableTAPFactory. +# +# By default, the default TAPFactory (tap.config.ConfigurableTAPFactory) is used and may use all properties related to the backup management, +# the database access and the ADQL translation. +tap_factory = diff --git a/src/tap/config/tap_min.properties b/src/tap/config/tap_min.properties new file mode 100644 index 0000000..2bfd5af --- /dev/null +++ b/src/tap/config/tap_min.properties @@ -0,0 +1,107 @@ +########################################################## +# MINIMUM TAP CONFIGURATION FILE # +# # +# TAP Version: 2.0 # +# Date: 27 Feb. 2015 # +# Author: Gregory Mantelet (ARI) # +# # +########################################################## + +############ +# DATABASE # +############ + +# Method to use in order to create database connections. +# +# Only two values are supported: +# * jndi: database connections will be supplied by a Datasource whose the JNDI name must be given. This method may propose connection pooling in function of the datasource configuration. +# * jdbc: the library will create itself connections when they will be needed thanks to the below JDBC parameters. This method does not propose any connection pooling. +# +# Allowed values: jndi, jdbc. +database_access = + +# The translator to use in order to translate ADQL to a SQL compatible with the used DBMS and its spatial extension. +# +# The TAP library supports only Postgresql (without spatial extension) and PgSphere for the moment. But you can provide your own SQL translator +# (even if it does not have spatial features), by providing the name of a class (within brackets: {...}) that implements ADQLTranslator (for instance: {apackage.MyADQLTranslator}) +# and which have at least an empty constructor. +# +# Allowed values: postgres, pgsphere, a class name +sql_translator = postgres + +############################# +# IF DATABASE ACCESS = JNDI # +############################# + +# JNDI name of the datasource. +# +# It should be defined in the web application (e.g. in the META-INF/context.xml file in tomcat). +datasource_jndi_name = + +############################# +# IF DATABASE ACCESS = JDBC # +############################# + +# It must be a JDBC driver URL. +# +# Note: The username, password or other parameters may be included in it, but in this case, the corresponding properties should leave empty or not provided at all. +jdbc_url = + +# JDBC driver path. +# +# By default, it is guessed in function of the database name provided in the jdbc_url property. It MUST be provided if another DBMS is used or if the JDBC driver path does not match the following ones: +# * Oracle : oracle.jdbc.OracleDriver +# * PostgreSQL: org.postgresql.Driver +# * MySQL : com.mysql.jdbc.Driver +# * SQLite : org.sqlite.JDBC +jdbc_driver = + +# Mandatory if the username is not already provided in jdbc_url +# Username used to access to the database. +db_user = + +# Mandatory if the password is not already provided in jdbc_url +# Password used by db_username to access to the database. +# +# Note: No password encryption can be done in this configuration file for the moment. +db_password = + +############ +# METADATA # +############ + +# Metadata fetching method. +# +# The value of this key defines the way the library will get the list of all schemas, tables and columns to publish and all their metadata (e.g. utype, description, type, ...). +# +# In its current state, the library proposes three methods: +# 1/ Parse a TableSet XML document and load its content into the database schema TAP_SCHEMA (note: this schema is first erased and rebuilt by the library). +# 2/ Get all metadata from the database schema TAP_SCHEMA. +# 3/ Build yourself the metadata of your service by creating an extension of tap.metadata.TAPMetadata. This extension must have either an empty constructor +# or a constructor with exactly 3 parameters of type UWSFileManager, TAPFactory and TAPLog ; if both constructor are provided, only the one with parameters will be used. +# +# Allowed values: xml, db or a full class name (between {}). +metadata = + +# Mandatory if the value of "metadata" is "xml". +# Local file path to the TableSet XML document. +# The XML document must implement the schema TableSet defined by VODataService. +# The file path must be either an absolute local file path or a file path relative to WebContent (i.e. the web application directory in which there are WEB-INF and META-INF). +metadata_file = + +######### +# FILES # +######### + +# Type of the file manager. +# +# Accepted values are: local (to manage files on the local system). You can also add another way to manage files by providing +# the name (within brackets: {...}) of a class implementing TAPFileManager and having at least one constructor with only a +# java.util.Properties parameter. +# +# Allowed values: local, a class name. +file_manager = local + +# Local file path of the directory in which all TAP files (logs, errors, job results, backup, ...) must be. +# The file path must be either an absolute local directory path or a directory path relative to WebContent (i.e. the web application directory in which there are WEB-INF and META-INF). +file_root_path = diff --git a/src/tap/formatter/ResultSetFormatter.java b/src/tap/data/DataReadException.java similarity index 53% rename from src/tap/formatter/ResultSetFormatter.java rename to src/tap/data/DataReadException.java index bd4608d..d75e19c 100644 --- a/src/tap/formatter/ResultSetFormatter.java +++ b/src/tap/data/DataReadException.java @@ -1,4 +1,4 @@ -package tap.formatter; +package tap.data; /* * This file is part of TAPLibrary. @@ -16,17 +16,33 @@ package tap.formatter; * You should have received a copy of the GNU Lesser General Public License * along with TAPLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2014 - Astronomisches Rechen Institut (ARI) */ -import java.sql.ResultSet; - import tap.TAPException; -import adql.db.DBColumn; +/** + * Exception that occurs when reading a data input (can be an InputStream, a ResultSet, a SavotTable, ...). + * + * @author Grégory Mantelet (ARI) - gmantele@ari.uni-heidelberg.de + * @version 2.0 (06/2014) + * @since 2.0 + * + * @see TableIterator + */ +public class DataReadException extends TAPException { + private static final long serialVersionUID = 1L; + + public DataReadException(final String message){ + super(message); + } -public interface ResultSetFormatter extends OutputFormat { + public DataReadException(Throwable cause){ + super(cause); + } - public Object formatValue(final Object value, final DBColumn colMeta) throws TAPException; + public DataReadException(String message, Throwable cause){ + super(message, cause); + } } diff --git a/src/tap/data/LimitedTableIterator.java b/src/tap/data/LimitedTableIterator.java new file mode 100644 index 0000000..9a98d94 --- /dev/null +++ b/src/tap/data/LimitedTableIterator.java @@ -0,0 +1,252 @@ +package tap.data; + +/* + * This file is part of TAPLibrary. + * + * ADQLLibrary is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ADQLLibrary is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ADQLLibrary. If not, see . + * + * Copyright 2014 - Astronomisches Rechen Institut (ARI) + */ + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.NoSuchElementException; + +import tap.ServiceConnection.LimitUnit; +import tap.metadata.TAPColumn; +import tap.upload.LimitedSizeInputStream; +import adql.db.DBType; + +import com.oreilly.servlet.multipart.ExceededSizeException; + +/** + *

    Wrap a {@link TableIterator} in order to limit its reading to a fixed number of rows.

    + * + *

    + * This wrapper can be "mixed" with a {@link LimitedSizeInputStream}, by wrapping the original input stream by a {@link LimitedSizeInputStream} + * and then by wrapping the {@link TableIterator} based on this wrapped input stream by {@link LimitedTableIterator}. + * Thus, this wrapper will be able to detect embedded {@link ExceededSizeException} thrown by a {@link LimitedSizeInputStream} through another {@link TableIterator}. + * If a such exception is detected, it will declare this wrapper as overflowed as it would be if a rows limit is reached. + *

    + * + *

    Warning: + * To work together with a {@link LimitedSizeInputStream}, this wrapper relies on the hypothesis that any {@link IOException} (including {@link ExceededSizeException}) + * will be embedded in a {@link DataReadException} as cause of this exception (using {@link DataReadException#DataReadException(Throwable)} + * or {@link DataReadException#DataReadException(String, Throwable)}). If it is not the case, no overflow detection could be done and the exception will just be forwarded. + *

    + * + *

    + * If a limit - either of rows or of bytes - is reached, a flag "overflow" is set to true. This flag can be got with {@link #isOverflow()}. + * Thus, when a {@link DataReadException} is caught, it will be easy to detect whether the error occurred because of an overflow + * or of another problem. + *

    + * + * @author Grégory Mantelet (ARI) + * @version 2.0 (01/2015) + * @since 2.0 + */ +public class LimitedTableIterator implements TableIterator { + + /** The wrapped {@link TableIterator}. */ + private final TableIterator innerIt; + + /** Limit on the number of rows to read. note: a negative value means "no limit". */ + private final int maxNbRows; + + /** The number of rows already read. */ + private int countRow = 0; + + /** Indicate whether a limit (rows or bytes) has been reached or not. */ + private boolean overflow = false; + + /** + * Wrap the given {@link TableIterator} so that limiting the number of rows to read. + * + * @param it The iterator to wrap. MUST NOT be NULL + * @param nbMaxRows Maximum number of rows that can be read. There is overflow if more than this number of rows is asked. A negative value means "no limit". + */ + public LimitedTableIterator(final TableIterator it, final int nbMaxRows) throws DataReadException{ + if (it == null) + throw new NullPointerException("Missing TableIterator to wrap!"); + innerIt = it; + this.maxNbRows = nbMaxRows; + } + + /** + *

    Build the specified {@link TableIterator} instance and wrap it so that limiting the number of rows OR bytes to read.

    + * + *

    + * If the limit is on the number of bytes, the given input stream will be first wrapped inside a {@link LimitedSizeInputStream}. + * Then, it will be given as only parameter of the constructor of the specified {@link TableIterator} instance. + *

    + * + *

    If the limit is on the number of rows, this {@link LimitedTableIterator} will count and limit itself the number of rows.

    + * + *

    IMPORTANT: The specified class must:

    + *
      + *
    • extend {@link TableIterator},
    • + *
    • be a concrete class,
    • + *
    • have at least one constructor with only one parameter of type {@link InputStream}.
    • + *
    + * + *

    Note: + * If the given limit type is NULL (or different from ROWS and BYTES), or the limit value is <=0, no limit will be set. + * All rows and bytes will be read until the end of input is reached. + *

    + * + * @param classIt Class of the {@link TableIterator} implementation to create and whose the output must be limited. + * @param input Input stream toward the table to read. + * @param type Type of the limit: ROWS or BYTES. MAY be NULL + * @param limit Limit in rows or bytes, depending of the "type" parameter. MAY BE <=0 + * + * @throws DataReadException If no instance of the given class can be created, + * or if the {@link TableIterator} instance can not be initialized, + * or if the limit (in rows or bytes) has been reached. + */ + public < T extends TableIterator > LimitedTableIterator(final Class classIt, final InputStream input, final LimitUnit type, final int limit) throws DataReadException{ + try{ + Constructor construct = classIt.getConstructor(InputStream.class); + if (LimitUnit.bytes.isCompatibleWith(type) && limit > 0){ + maxNbRows = -1; + innerIt = construct.newInstance(new LimitedSizeInputStream(input, limit * type.bytesFactor())); + }else{ + innerIt = construct.newInstance(input); + maxNbRows = (type == null || type != LimitUnit.rows) ? -1 : limit; + } + }catch(InvocationTargetException ite){ + Throwable t = ite.getCause(); + if (t != null && t instanceof DataReadException){ + ExceededSizeException exceedEx = getExceededSizeException(t); + // if an error caused by an ExceedSizeException occurs, set this iterator as overflowed and throw the exception: + if (exceedEx != null) + throw new DataReadException(exceedEx.getMessage(), exceedEx); + else + throw (DataReadException)t; + }else + throw new DataReadException("Can not create a LimitedTableIterator!", ite); + }catch(Exception ex){ + throw new DataReadException("Can not create a LimitedTableIterator!", ex); + } + } + + /** + * Get the iterator wrapped by this {@link TableIterator} instance. + * + * @return The wrapped iterator. + */ + public final TableIterator getWrappedIterator(){ + return innerIt; + } + + /** + *

    Tell whether a limit (in rows or bytes) has been reached.

    + * + *

    Note: + * If true is returned (that's to say, if a limit has been reached) no more rows or column values + * can be read ; an {@link IllegalStateException} would then be thrown. + *

    + * + * @return true if a limit has been reached, false otherwise. + */ + public final boolean isOverflow(){ + return overflow; + } + + @Override + public void close() throws DataReadException{ + innerIt.close(); + } + + @Override + public TAPColumn[] getMetadata() throws DataReadException{ + return innerIt.getMetadata(); + } + + @Override + public boolean nextRow() throws DataReadException{ + // Test the overflow flag and proceed only if not overflowed: + if (overflow) + throw new DataReadException("Data read overflow: the limit has already been reached! No more data can be read."); + + // Read the next row: + boolean nextRow; + try{ + nextRow = innerIt.nextRow(); + countRow++; + }catch(DataReadException ex){ + ExceededSizeException exceedEx = getExceededSizeException(ex); + // if an error caused by an ExceedSizeException occurs, set this iterator as overflowed and throw the exception: + if (exceedEx != null){ + overflow = true; + throw new DataReadException(exceedEx.getMessage()); + }else + throw ex; + } + + // If, counting this one, the number of rows exceeds the limit, set this iterator as overflowed and throw an exception: + if (nextRow && maxNbRows >= 0 && countRow > maxNbRows){ + overflow = true; + throw new DataReadException("Data read overflow: the limit of " + maxNbRows + " rows has been reached!"); + } + + // Send back the value returned by the inner iterator: + return nextRow; + } + + @Override + public boolean hasNextCol() throws IllegalStateException, DataReadException{ + testOverflow(); + return innerIt.hasNextCol(); + } + + @Override + public Object nextCol() throws NoSuchElementException, IllegalStateException, DataReadException{ + testOverflow(); + return innerIt.nextCol(); + } + + @Override + public DBType getColType() throws IllegalStateException, DataReadException{ + testOverflow(); + return innerIt.getColType(); + } + + /** + * Test the overflow flag and throw an {@link IllegalStateException} if true. + * + * @throws IllegalStateException If this iterator is overflowed (because of either a bytes limit or a rows limit). + */ + private void testOverflow() throws IllegalStateException{ + if (overflow) + throw new IllegalStateException("Data read overflow: the limit has already been reached! No more data can be read."); + } + + /** + * Get the first {@link ExceededSizeException} found in the given {@link Throwable} trace. + * + * @param ex A {@link Throwable} + * + * @return The first {@link ExceededSizeException} encountered, or NULL if none has been found. + */ + private ExceededSizeException getExceededSizeException(Throwable ex){ + if (ex == null) + return null; + while(!(ex instanceof ExceededSizeException) && ex.getCause() != null) + ex = ex.getCause(); + return (ex instanceof ExceededSizeException) ? (ExceededSizeException)ex : null; + } + +} diff --git a/src/tap/data/ResultSetTableIterator.java b/src/tap/data/ResultSetTableIterator.java new file mode 100644 index 0000000..9c906a1 --- /dev/null +++ b/src/tap/data/ResultSetTableIterator.java @@ -0,0 +1,544 @@ +package tap.data; + +/* + * This file is part of TAPLibrary. + * + * TAPLibrary is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * TAPLibrary is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with TAPLibrary. If not, see . + * + * Copyright 2014 - Astronomisches Rechen Institut (ARI) + */ + +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.util.NoSuchElementException; + +import tap.metadata.TAPColumn; +import uws.ISO8601Format; +import adql.db.DBColumn; +import adql.db.DBType; +import adql.db.DBType.DBDatatype; +import adql.db.STCS.Region; +import adql.parser.ParseException; +import adql.translator.JDBCTranslator; + +/** + *

    {@link TableIterator} which lets iterate over a SQL {@link ResultSet}.

    + * + *

    Note: + * {@link #getColType()} will return a TAP type based on the one declared in the {@link ResultSetMetaData} object. + *

    + * + * @author Grégory Mantelet (ARI) + * @version 2.0 (11/2014) + * @since 2.0 + */ +public class ResultSetTableIterator implements TableIterator { + + /** ResultSet/Dataset to read. */ + private final ResultSet data; + + /** Object which has the knowledge of the specific JDBC column types + * and which knows how to deal with geometrical values between the + * library and the database. */ + private final JDBCTranslator translator; + + /** Number of columns to read. */ + private final int nbColumns; + /** Metadata of all columns identified before the iteration. */ + private final TAPColumn[] colMeta; + + /** Indicate whether the row iteration has already started. */ + private boolean iterationStarted = false; + /** Indicate whether the last row has already been reached. */ + private boolean endReached = false; + /** Index of the last read column (=0 just after {@link #nextRow()} and before {@link #nextCol()}, ={@link #nbColumns} after the last column has been read). */ + private int colIndex; + + /** + *

    Build a TableIterator able to read rows and columns of the given ResultSet.

    + * + *

    + * In order to provide the metadata through {@link #getMetadata()}, this constructor is trying to guess the datatype + * from the DBMS column datatype (using {@link #convertType(int, String, String)}). + *

    + * + *

    Type guessing

    + * + *

    + * In order to guess a TAP type from a DBMS type, this constructor will call {@link #convertType(int, String, String)} + * which deals with the most common standard datatypes known in Postgres, SQLite, MySQL, Oracle and JavaDB/Derby. + * This conversion is therefore not as precise as the one expected by a translator. That's why it is recommended + * to use one of the constructor having a {@link JDBCTranslator} in parameter. + *

    + * + * @param dataSet Dataset over which this iterator must iterate. + * + * @throws NullPointerException If NULL is given in parameter. + * @throws DataReadException If the given ResultSet is closed or if the metadata (columns count and types) can not be fetched. + * + * @see #convertType(int, String, String) + * @see #ResultSetTableIterator(ResultSet, JDBCTranslator, String, DBColumn[]) + */ + public ResultSetTableIterator(final ResultSet dataSet) throws NullPointerException, DataReadException{ + this(dataSet, null, null, null); + } + + /** + *

    Build a TableIterator able to read rows and columns of the given ResultSet.

    + * + *

    + * In order to provide the metadata through {@link #getMetadata()}, this constructor is trying to guess the datatype + * from the DBMS column datatype (using {@link #convertType(int, String, String)}). + *

    + * + *

    Type guessing

    + * + *

    + * In order to guess a TAP type from a DBMS type, this constructor will call {@link #convertType(int, String, String)} + * which deals with the most common standard datatypes known in Postgres, SQLite, MySQL, Oracle and JavaDB/Derby. + * This conversion is therefore not as precise as the one expected by a translator. That's why it is recommended + * to use one of the constructor having a {@link JDBCTranslator} in parameter. + *

    + * + *

    Important: + * The second parameter of this constructor is given as second parameter of {@link #convertType(int, String, String)}. + * This parameter is really used ONLY when the DBMS is SQLite ("sqlite"). + * Indeed, SQLite has so many datatype restrictions that it is absolutely needed to know it is the DBMS from which the + * ResultSet is coming. Without this information, type guessing will be unpredictable! + *

    + * + * @param dataSet Dataset over which this iterator must iterate. + * @param dbms Lower-case string which indicates from which DBMS the given ResultSet is coming. note: MAY be NULL. + * + * @throws NullPointerException If NULL is given in parameter. + * @throws DataReadException If the given ResultSet is closed or if the metadata (columns count and types) can not be fetched. + * + * @see #convertType(int, String, String) + * @see ResultSetTableIterator#ResultSetTableIterator(ResultSet, JDBCTranslator, String, DBColumn[]) + */ + public ResultSetTableIterator(final ResultSet dataSet, final String dbms) throws NullPointerException, DataReadException{ + this(dataSet, null, dbms, null); + } + + /** + *

    Build a TableIterator able to read rows and columns of the given ResultSet.

    + * + *

    + * In order to provide the metadata through {@link #getMetadata()}, this constructor is trying to guess the datatype + * from the DBMS column datatype (using {@link #convertType(int, String, String)}). + *

    + * + *

    Type guessing

    + * + *

    + * In order to guess a TAP type from a DBMS type, this constructor will call {@link #convertType(int, String, String)} + * which will ask to the given translator ({@link JDBCTranslator#convertTypeFromDB(int, String, String, String[])}) + * if not NULL. However if no translator is provided, this function will proceed to a default conversion + * using the most common standard datatypes known in Postgres, SQLite, MySQL, Oracle and JavaDB/Derby. + * This conversion is therefore not as precise as the one expected by the translator. + *

    + * + * @param dataSet Dataset over which this iterator must iterate. + * @param translator The {@link JDBCTranslator} used to transform the ADQL query into SQL query. This translator is also able to convert + * JDBC types and to parse geometrical values. note: MAY be NULL + * + * @throws NullPointerException If NULL is given in parameter. + * @throws DataReadException If the given ResultSet is closed or if the metadata (columns count and types) can not be fetched. + * + * @see #convertType(int, String, String) + * @see ResultSetTableIterator#ResultSetTableIterator(ResultSet, JDBCTranslator, String, DBColumn[]) + */ + public ResultSetTableIterator(final ResultSet dataSet, final JDBCTranslator translator) throws NullPointerException, DataReadException{ + this(dataSet, translator, null, null); + } + + /** + *

    Build a TableIterator able to read rows and columns of the given ResultSet.

    + * + *

    + * In order to provide the metadata through {@link #getMetadata()}, this constructor is trying to guess the datatype + * from the DBMS column datatype (using {@link #convertType(int, String, String)}). + *

    + * + *

    Type guessing

    + * + *

    + * In order to guess a TAP type from a DBMS type, this constructor will call {@link #convertType(int, String, String)} + * which will ask to the given translator ({@link JDBCTranslator#convertTypeFromDB(int, String, String, String[])}) + * if not NULL. However if no translator is provided, this function will proceed to a default conversion + * using the most common standard datatypes known in Postgres, SQLite, MySQL, Oracle and JavaDB/Derby. + * This conversion is therefore not as precise as the one expected by the translator. + *

    + * + *

    Important: + * The third parameter of this constructor is given as second parameter of {@link #convertType(int, String, String)}. + * This parameter is really used ONLY when the translator conversion failed and when the DBMS is SQLite ("sqlite"). + * Indeed, SQLite has so many datatype restrictions that it is absolutely needed to know it is the DBMS from which the + * ResultSet is coming. Without this information, type guessing will be unpredictable! + *

    + * + * @param dataSet Dataset over which this iterator must iterate. + * @param translator The {@link JDBCTranslator} used to transform the ADQL query into SQL query. This translator is also able to convert + * JDBC types and to parse geometrical values. note: MAY be NULL + * @param dbms Lower-case string which indicates from which DBMS the given ResultSet is coming. note: MAY be NULL. + * + * @throws NullPointerException If NULL is given in parameter. + * @throws DataReadException If the given ResultSet is closed or if the metadata (columns count and types) can not be fetched. + * + * @see #convertType(int, String, String) + * @see ResultSetTableIterator#ResultSetTableIterator(ResultSet, JDBCTranslator, String, DBColumn[]) + */ + public ResultSetTableIterator(final ResultSet dataSet, final JDBCTranslator translator, final String dbms) throws NullPointerException, DataReadException{ + this(dataSet, translator, dbms, null); + } + + /** + *

    Build a TableIterator able to read rows and columns of the given ResultSet.

    + * + *

    + * In order to provide the metadata through {@link #getMetadata()}, this constructor is reading first the given metadata (if any), + * and then, try to guess the datatype from the DBMS column datatype (using {@link #convertType(int, String, String)}). + *

    + * + *

    Provided metadata

    + * + *

    The third parameter of this constructor aims to provide the metadata expected for each column of the ResultSet.

    + * + *

    + * For that, it is expected that all these metadata are {@link TAPColumn} objects. Indeed, simple {@link DBColumn} + * instances do not have the type information. If just {@link DBColumn}s are provided, the ADQL name it provides will be kept + * but the type will be guessed from the type provide by the ResultSetMetadata. + *

    + * + *

    Note: + * If this parameter is incomplete (array length less than the column count returned by the ResultSet or some array items are NULL), + * column metadata will be associated in the same order as the ResultSet columns. Missing metadata will be built from the + * {@link ResultSetMetaData} and so the types will be guessed. + *

    + * + *

    Type guessing

    + * + *

    + * In order to guess a TAP type from a DBMS type, this constructor will call {@link #convertType(int, String, String)} + * which will ask to the given translator ({@link JDBCTranslator#convertTypeFromDB(int, String, String, String[])}) + * if not NULL. However if no translator is provided, this function will proceed to a default conversion + * using the most common standard datatypes known in Postgres, SQLite, MySQL, Oracle and JavaDB/Derby. + * This conversion is therefore not as precise as the one expected by the translator. + *

    + * + *

    Important: + * The third parameter of this constructor is given as second parameter of {@link #convertType(int, String, String)}. + * This parameter is really used ONLY when the translator conversion failed and when the DBMS is SQLite ("sqlite"). + * Indeed, SQLite has so many datatype restrictions that it is absolutely needed to know it is the DBMS from which the + * ResultSet is coming. Without this information, type guessing will be unpredictable! + *

    + * + * @param dataSet Dataset over which this iterator must iterate. + * @param translator The {@link JDBCTranslator} used to transform the ADQL query into SQL query. This translator is also able to convert + * JDBC types and to parse geometrical values. note: MAY be NULL + * @param dbms Lower-case string which indicates from which DBMS the given ResultSet is coming. note: MAY be NULL. + * @param resultMeta List of expected columns. note: these metadata are expected to be really {@link TAPColumn} objects ; MAY be NULL. + * + * @throws NullPointerException If NULL is given in parameter. + * @throws DataReadException If the metadata (columns count and types) can not be fetched. + * + * @see #convertType(int, String, String) + */ + public ResultSetTableIterator(final ResultSet dataSet, final JDBCTranslator translator, final String dbms, final DBColumn[] resultMeta) throws NullPointerException, DataReadException{ + // A dataset MUST BE provided: + if (dataSet == null) + throw new NullPointerException("Missing ResultSet object over which to iterate!"); + + // Keep a reference to the ResultSet: + data = dataSet; + + // Set the translator to use (if needed): + this.translator = translator; + + // Count columns and determine their type: + try{ + // get the metadata: + ResultSetMetaData metadata = data.getMetaData(); + // count columns: + nbColumns = metadata.getColumnCount(); + // determine their type: + colMeta = new TAPColumn[nbColumns]; + for(int i = 1; i <= nbColumns; i++){ + if (resultMeta != null && (i - 1) < resultMeta.length && resultMeta[i - 1] != null){ + try{ + colMeta[i - 1] = (TAPColumn)resultMeta[i - 1]; + }catch(ClassCastException cce){ + DBType datatype = convertType(metadata.getColumnType(i), metadata.getColumnTypeName(i), dbms); + colMeta[i - 1] = new TAPColumn(resultMeta[i - 1].getADQLName(), datatype); + } + }else{ + DBType datatype = convertType(metadata.getColumnType(i), metadata.getColumnTypeName(i), dbms); + colMeta[i - 1] = new TAPColumn(metadata.getColumnLabel(i), datatype); + } + } + }catch(SQLException se){ + throw new DataReadException("Can not get the column types of the given ResultSet!", se); + } + } + + @Override + public void close() throws DataReadException{ + try{ + data.close(); + }catch(SQLException se){ + throw new DataReadException("Can not close the iterated ResultSet!", se); + } + } + + @Override + public TAPColumn[] getMetadata(){ + return colMeta; + } + + @Override + public boolean nextRow() throws DataReadException{ + try{ + // go to the next row: + boolean rowFetched = data.next(); + endReached = !rowFetched; + // prepare the iteration over its columns: + colIndex = 0; + iterationStarted = true; + return rowFetched; + }catch(SQLException e){ + throw new DataReadException("Unable to read a result set row!", e); + } + } + + /** + *

    Check the row iteration state. That's to say whether:

    + *
      + *
    • the row iteration has started = the first row has been read = a first call of {@link #nextRow()} has been done
    • + *
    • AND the row iteration is not finished = the last row has been read.
    • + *
    + * + * @throws IllegalStateException + */ + private void checkReadState() throws IllegalStateException{ + if (!iterationStarted) + throw new IllegalStateException("No row has yet been read!"); + else if (endReached) + throw new IllegalStateException("End of ResultSet already reached!"); + } + + @Override + public boolean hasNextCol() throws IllegalStateException, DataReadException{ + // Check the read state: + checkReadState(); + + // Determine whether the last column has been reached or not: + return (colIndex < nbColumns); + } + + @Override + public Object nextCol() throws NoSuchElementException, IllegalStateException, DataReadException{ + // Check the read state and ensure there is still at least one column to read: + if (!hasNextCol()) + throw new NoSuchElementException("No more column to read!"); + + // Get the column value: + try{ + Object o = data.getObject(++colIndex); + if (o != null){ + DBType colType = getColType(); + // if the column value is a Timestamp object, format it in ISO8601: + if (o instanceof Timestamp) + o = ISO8601Format.format(((Timestamp)o).getTime()); + // if the column value is a geometrical object, it must be serialized in STC-S: + else if (translator != null && colType.isGeometry()){ + Region region = translator.translateGeometryFromDB(o); + if (region != null) + o = region.toSTCS(); + } + } + return o; + }catch(SQLException se){ + throw new DataReadException("Can not read the value of the " + colIndex + "-th column!", se); + }catch(ParseException pe){ + throw new DataReadException(pe.getMessage()); + } + } + + @Override + public DBType getColType() throws IllegalStateException, DataReadException{ + // Basically check the read state (for rows iteration): + checkReadState(); + + // Check deeper the read state (for columns iteration): + if (colIndex <= 0) + throw new IllegalStateException("No column has yet been read!"); + else if (colIndex > nbColumns) + throw new IllegalStateException("All columns have already been read!"); + + // Return the column type: + return colMeta[colIndex - 1].getDatatype(); + } + + /** + *

    Convert the given DBMS type into the corresponding {@link DBType} instance.

    + * + *

    + * This function first tries the conversion using the translator ({@link JDBCTranslator#convertTypeFromDB(int, String, String, String[])}). + * If the translator fails, a default conversion is done. + *

    + * + *

    Warning: + * It is not recommended to rely on the default conversion. + * This conversion is just a matter of guessing the better matching {@link DBType} + * considering the types of the following DBMS: PostgreSQL, SQLite, MySQL, Oracle and Java/DB/Derby. + *

    + * + * @param dbmsType DBMS column data-type name. + * @param dbms Lower-case string which indicates which DBMS the ResultSet is coming from. note: MAY be NULL. + * + * @return The best suited {@link DBType} object. + * + * @see JDBCTranslator#convertTypeFromDB(int, String, String, String[]) + * @see #defaultTypeConversion(String, String[], String) + */ + protected DBType convertType(final int dbmsType, String dbmsTypeName, final String dbms) throws DataReadException{ + // If no type is provided return VARCHAR: + if (dbmsTypeName == null || dbmsTypeName.trim().length() == 0) + return new DBType(DBDatatype.VARCHAR, DBType.NO_LENGTH); + + // Extract the type prefix and lower-case it: + int startParamIndex = dbmsTypeName.indexOf('('), endParamIndex = dbmsTypeName.indexOf(')'); + String dbmsTypePrefix = (startParamIndex <= 0) ? dbmsTypeName : dbmsTypeName.substring(0, endParamIndex); + dbmsTypePrefix = dbmsTypePrefix.trim().toLowerCase(); + String[] typeParams = (startParamIndex <= 0) ? null : dbmsTypeName.substring(startParamIndex + 1, endParamIndex).split(","); + + // Ask first to the translator: + DBType dbType = null; + if (translator != null) + dbType = translator.convertTypeFromDB(dbmsType, dbmsTypeName, dbmsTypePrefix, typeParams); + + // And if unsuccessful, apply a default conversion: + if (dbType == null) + dbType = defaultTypeConversion(dbmsTypePrefix, typeParams, dbms); + + return dbType; + } + + /** + *

    Convert the given DBMS type into the better matching {@link DBType} instance. + * This function is used to guess the TAP type of a column when it is not provided in the constructor. + * It aims not to be exhaustive, but just to provide a type when the given TAP metadata are incomplete.

    + * + *

    Note: + * Any unknown DBMS data-type will be considered and translated as a VARCHAR. + * This latter will be also returned if the given parameter is an empty string or NULL. + *

    + * + *

    Note: + * This type conversion function has been designed to work with all standard data-types of the following DBMS: + * PostgreSQL, SQLite, MySQL, Oracle and JavaDB/Derby. + *

    + * + *

    Important: + * The third parameter is REALLY NEEDED when the DBMS is SQLite ("sqlite")! + * Indeed, SQLite has a so restrictive list of data-types that this function can reliably convert + * only if it knows the DBMS is SQLite. Otherwise, the conversion result would be unpredictable. + * In this default implementation of this function, all other DBMS values are ignored. + *

    + * + *

    Warning: + * This function is not translating the geometrical data-types. If a such data-type is encountered, + * it will considered as unknown and so, a VARCHAR TAP type will be returned. + *

    + * + * @param dbmsTypeName Name of type, without the eventual parameters. + * @param params The eventual type parameters (e.g. char string length). + * @param dbms The targeted DBMS. + * + * @return The corresponding ADQL/TAP type. NEVER NULL + */ + protected final DBType defaultTypeConversion(final String dbmsTypeName, final String[] params, final String dbms){ + // Get the length parameter (always in first position): + int lengthParam = DBType.NO_LENGTH; + if (params != null && params.length > 0){ + try{ + lengthParam = Integer.parseInt(params[0]); + }catch(NumberFormatException nfe){} + } + + // CASE: SQLITE + if (dbms != null && dbms.equals("sqlite")){ + // INTEGER -> SMALLINT, INTEGER, BIGINT + if (dbmsTypeName.equals("integer")) + return new DBType(DBDatatype.BIGINT); + // REAL -> REAL, DOUBLE + else if (dbmsTypeName.equals("real")) + return new DBType(DBDatatype.DOUBLE); + // TEXT -> CHAR, VARCHAR, CLOB, TIMESTAMP + else if (dbmsTypeName.equals("text")) + return new DBType(DBDatatype.VARCHAR); + // BLOB -> BINARY, VARBINARY, BLOB + else if (dbmsTypeName.equals("blob")) + return new DBType(DBDatatype.BLOB); + // Default: + else + return new DBType(DBDatatype.VARCHAR, DBType.NO_LENGTH); + } + // CASE: OTHER DBMS + else{ + // SMALLINT + if (dbmsTypeName.equals("smallint") || dbmsTypeName.equals("int2") || dbmsTypeName.equals("smallserial") || dbmsTypeName.equals("serial2") || dbmsTypeName.equals("boolean") || dbmsTypeName.equals("bool")) + return new DBType(DBDatatype.SMALLINT); + // INTEGER + else if (dbmsTypeName.equals("integer") || dbmsTypeName.equals("int") || dbmsTypeName.equals("int4") || dbmsTypeName.equals("serial") || dbmsTypeName.equals("serial4")) + return new DBType(DBDatatype.INTEGER); + // BIGINT + else if (dbmsTypeName.equals("bigint") || dbmsTypeName.equals("int8") || dbmsTypeName.equals("bigserial") || dbmsTypeName.equals("bigserial8") || dbmsTypeName.equals("number")) + return new DBType(DBDatatype.BIGINT); + // REAL + else if (dbmsTypeName.equals("real") || dbmsTypeName.equals("float4") || (dbmsTypeName.equals("float") && lengthParam <= 63)) + return new DBType(DBDatatype.REAL); + // DOUBLE + else if (dbmsTypeName.equals("double") || dbmsTypeName.equals("double precision") || dbmsTypeName.equals("float8") || (dbmsTypeName.equals("float") && lengthParam > 63)) + return new DBType(DBDatatype.DOUBLE); + // BINARY + else if (dbmsTypeName.equals("bit") || dbmsTypeName.equals("binary") || dbmsTypeName.equals("raw") || ((dbmsTypeName.equals("char") || dbmsTypeName.equals("character")) && dbmsTypeName.endsWith(" for bit data"))) + return new DBType(DBDatatype.BINARY, lengthParam); + // VARBINARY + else if (dbmsTypeName.equals("bit varying") || dbmsTypeName.equals("varbit") || dbmsTypeName.equals("varbinary") || dbmsTypeName.equals("long raw") || ((dbmsTypeName.equals("varchar") || dbmsTypeName.equals("character varying")) && dbmsTypeName.endsWith(" for bit data"))) + return new DBType(DBDatatype.VARBINARY, lengthParam); + // CHAR + else if (dbmsTypeName.equals("char") || dbmsTypeName.equals("character")) + return new DBType(DBDatatype.CHAR, lengthParam); + // VARCHAR + else if (dbmsTypeName.equals("varchar") || dbmsTypeName.equals("varchar2") || dbmsTypeName.equals("character varying")) + return new DBType(DBDatatype.VARCHAR, lengthParam); + // BLOB + else if (dbmsTypeName.equals("bytea") || dbmsTypeName.equals("blob") || dbmsTypeName.equals("binary large object")) + return new DBType(DBDatatype.BLOB); + // CLOB + else if (dbmsTypeName.equals("text") || dbmsTypeName.equals("clob") || dbmsTypeName.equals("character large object")) + return new DBType(DBDatatype.CLOB); + // TIMESTAMP + else if (dbmsTypeName.equals("timestamp") || dbmsTypeName.equals("timestamptz") || dbmsTypeName.equals("time") || dbmsTypeName.equals("timetz") || dbmsTypeName.equals("date")) + return new DBType(DBDatatype.TIMESTAMP); + // Default: + else + return new DBType(DBDatatype.VARCHAR, DBType.NO_LENGTH); + } + } + +} diff --git a/src/tap/data/TableIterator.java b/src/tap/data/TableIterator.java new file mode 100644 index 0000000..cdc16de --- /dev/null +++ b/src/tap/data/TableIterator.java @@ -0,0 +1,138 @@ +package tap.data; + +/* + * This file is part of TAPLibrary. + * + * TAPLibrary is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * TAPLibrary is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with TAPLibrary. If not, see . + * + * Copyright 2014 - Astronomisches Rechen Institut (ARI) + */ + +import java.util.NoSuchElementException; + +import tap.metadata.TAPColumn; +import adql.db.DBType; + +/** + *

    Let's iterate on each row and then on each column over a table dataset.

    + * + *

    Initially, no rows are loaded and the "cursor" inside the dataset is set before the first row. + * Thus, a first call to {@link #nextRow()} is required to read each of the column values of the first row.

    + * + *

    Example of an expected usage:

    + *
    + * 	TableIterator it = ...;
    + * 	try{
    + * 		while(it.nextRow()){
    + * 			while(it.hasNextCol()){
    + * 				Object colValue = it.nextCol();
    + * 				String colType = it.getColType();
    + * 				...
    + * 			}
    + * 		}
    + * 	}catch(DataReadException dre){
    + * 		...
    + * 	}finally{
    + * 		try{
    + * 			it.close();
    + * 		}catch(DataReadException dre){ ... }
    + * 	}
    + * 
    + * + * @author Grégory Mantelet (ARI) + * @version 2.0 (12/2014) + * @since 2.0 + */ +public interface TableIterator { + /** + *

    Get all the metadata column that have been successfully extracted at the creation of this iterator.

    + * + *

    Important: This function should be callable at any moment from the creation of the iterator until the end of the table dataset has been reached.

    + * + *

    Note: This function MAY BE NOT IMPLEMENTED or the metadata can not be fetched. In this case, NULL will be returned.

    + * + *

    Warning: If the metadata part of the original document is corrupted (i.e. false number of columns), + * the column type information should be fetched thanks to {@link #getColType()} while iterating over rows and columns.

    + * + * @return An array of {@link TAPColumn} objects (each for a column of any row), + * or NULL if this function is not implemented OR if it was not possible to get these metadata. + * + * @see #getColType() + */ + public TAPColumn[] getMetadata() throws DataReadException; + + /** + *

    Go to the next row if there is one.

    + * + *

    Note: After a call to this function the columns must be fetched individually using {@link #nextCol()} + * IF this function returned true.

    + * + * @return true if the next row has been successfully reached, + * false if no more rows can be read. + * + * @throws DataReadException If an error occurs while reading the table dataset. + */ + public boolean nextRow() throws DataReadException; + + /** + * Tell whether another column is available. + * + * @return true if {@link #nextCol()} will return the value of the next column with no error, + * false otherwise. + * + * @throws IllegalStateException If {@link #nextRow()} has not yet been called. + * @throws DataReadException If an error occurs while reading the table dataset. + */ + public boolean hasNextCol() throws IllegalStateException, DataReadException; + + /** + *

    Return the value of the next column.

    + * + *

    Note: The column type can be fetched using {@link #getColType()} after a call to {@link #nextCol()}.

    + * + * @return Get the value of the next column. + * + * @throws NoSuchElementException If no more column value is available. + * @throws IllegalStateException If {@link #nextRow()} has not yet been called. + * @throws DataReadException If an error occurs while reading the table dataset. + */ + public Object nextCol() throws NoSuchElementException, IllegalStateException, DataReadException; + + /** + *

    Get the type of the current column value.

    + * + *

    Note 1: "Current column value" means here "the value last returned by {@link #nextCol()}".

    + * + *

    Note 2: This function MAY BE NOT IMPLEMENTED or the type information can not be fetched. If this is the case, NULL will be returned.

    + * + *

    Warning: In some cases, the metadata part of the original document does not match with the data + * it should have represented. In such case, the types returned here and by {@link #getMetadata()} would be different. + * In case of such mismatch, the type returned by {@link #getColType()} should be considered as more correct/accurate.

    + * + * @return Type of the current column value, + * or NULL if this information is not available or if this function is not implemented. + * + * @throws IllegalStateException If {@link #nextCol()} has not yet been called. + * @throws DataReadException If an error occurs while reading the table dataset. + */ + public DBType getColType() throws IllegalStateException, DataReadException; + + /** + * Close the stream or input over which this class iterates. + * + * @throws DataReadException If any error occurs while closing it. + */ + public void close() throws DataReadException; + +} diff --git a/src/tap/data/VOTableIterator.java b/src/tap/data/VOTableIterator.java new file mode 100644 index 0000000..95d2d56 --- /dev/null +++ b/src/tap/data/VOTableIterator.java @@ -0,0 +1,484 @@ +package tap.data; + +/* + * This file is part of TAPLibrary. + * + * TAPLibrary is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * TAPLibrary is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with TAPLibrary. If not, see . + * + * Copyright 2015 - Astronomisches Rechen Institut (ARI) + */ + +import java.io.IOException; +import java.io.InputStream; +import java.util.NoSuchElementException; + +import tap.TAPException; +import tap.metadata.TAPColumn; +import tap.metadata.VotType; +import tap.metadata.VotType.VotDatatype; +import uk.ac.starlink.table.ColumnInfo; +import uk.ac.starlink.table.DescribedValue; +import uk.ac.starlink.table.StarTable; +import uk.ac.starlink.table.StarTableFactory; +import uk.ac.starlink.table.TableBuilder; +import uk.ac.starlink.table.TableFormatException; +import uk.ac.starlink.table.TableSink; +import adql.db.DBType; + +/** + *

    {@link TableIterator} which lets iterate over a VOTable input stream using STIL.

    + * + *

    {@link #getColType()} will return TAP type based on the type declared in the VOTable metadata part.

    + * + * @author Grégory Mantelet (ARI) + * @version 2.0 (04/2015) + * @since 2.0 + */ +public class VOTableIterator implements TableIterator { + + /** Message of the IOException sent when the streaming is aborted. */ + protected static final String STREAM_ABORTED_MESSAGE = "Streaming aborted!"; + + /** + *

    This class lets consume the metadata and rows of a VOTable document.

    + * + *

    + * On the contrary to a usual TableSink, this one will stop after each row until this row has been fetched by {@link VOTableIterator}. + *

    + * + *

    + * Besides, the metadata returned by StarTable are immediately converted into TAP metadata. If this conversion fails, the error is kept + * in metaError, so that the VOTable reading can continue if the fact that metadata are missing is not a problem for the class using the + * {@link VOTableIterator}. + *

    + * + * @author Grégory Mantelet (ARI) + * @version 2.0 (04/2015) + * @since 2.0 + */ + protected static class StreamVOTableSink implements TableSink { + + /**

    The accepted VOTable metadata, after conversion from StarTable metadata.

    + *

    Note: this may be NULL after the metadata has been read if an error occurred while performing the conversion. + * In this case, metaError contains this error. */ + private TAPColumn[] meta = null; + + /** The error which happened while converting the StarTable metadata into TAP metadata. */ + private DataReadException metaError = null; + + /** The last accepted row. */ + private Object[] pendingRow = null; + + /** Flag meaning that the end of the stream has been reached + * OR if the VOTable reading should be stopped before reading more rows. */ + private boolean endReached = false; + + /** + *

    Stop nicely reading the VOTable.

    + * + *

    + * An exception will be thrown to the STILTS class using this TableSink, + * but no exception should be thrown to VOTableIterator. + *

    + */ + public synchronized void stop(){ + endReached = true; + notifyAll(); + } + + @Override + public synchronized void acceptMetadata(final StarTable metaTable) throws TableFormatException{ + try{ + // Convert the StartTable metadata into TAP metadata: + meta = extractColMeta(metaTable); + + }catch(DataReadException dre){ + // Save the error ; this error will be throw when a call to getMetadata() will be done: + metaError = dre; + + }finally{ + // Free all waiting threads: + notifyAll(); + } + } + + @Override + public synchronized void acceptRow(final Object[] row) throws IOException{ + try{ + // Wait until the last accepted row has been consumed: + while(!endReached && pendingRow != null) + wait(); + + /* If the end has been reached, this is not normal + * (because endRows() is always called after acceptRow()...so, it means the iteration has been aborted before the end) + * and so the stream reading should be interrupted: */ + if (endReached) + throw new IOException(STREAM_ABORTED_MESSAGE); + + // Otherwise, keep the given row: + pendingRow = row; + + /* Security for the cases where a row to accept is NULL. + * In such case, pendingRow will be set to NULL and the function getRow() will wait for ever. + * This case is not supposed to happen because the caller of acceptRow(...) should not give a NULL row... + * ...which should then mean that the end of the stream has been reached. */ + if (pendingRow == null) + endReached = true; + + }catch(InterruptedException ie){ + /* If the thread has been interrupted, set this TableSink in a state similar to + * when the end of the stream has been reached: */ + pendingRow = null; + endReached = true; + + }finally{ + // In all cases, all waiting threads must be freed: + notifyAll(); + } + } + + @Override + public synchronized void endRows() throws IOException{ + try{ + // Wait until the last accepted row has been consumed: + while(!endReached && pendingRow != null) + wait(); + }catch(InterruptedException ie){ + /* Nothing to do in particular ; the end of the stream will be set anyway. */ + }finally{ + // No more rows are available: + pendingRow = null; + // Set the END flag: + endReached = true; + // Notify all waiting threads that the end has been reached: + notifyAll(); + } + } + + /** + *

    Get the metadata found in the VOTable.

    + * + *

    Note: + * This method is blocking until metadata are fully available by this TableSink + * or if an error occurred while converting them in TAP metadata. + * A Thread interruption will also make this function returning. + *

    + * + * @return The metadata found in the VOTable header. + * + * @throws DataReadException If the metadata can not be interpreted correctly. + */ + public synchronized TAPColumn[] getMeta() throws DataReadException{ + try{ + // Wait until metadata are available, or if an error has occurred while accepting them: + while(metaError == null && meta == null) + wait(); + + // If there was an error while interpreting the accepted metadata, throw it: + if (metaError != null) + throw metaError; + + // Otherwise, just return the metadata: + return meta; + + }catch(InterruptedException ie){ + /* If the thread has been interrupted, set this TableSink in a state similar to + * when the end of the stream has been reached: */ + endReached = true; + /* Return the metadata ; + * NULL will be returned if the interruption has occurred before the real reading of the VOTable metadata: */ + return meta; + + }finally{ + // In all cases, the waiting threads must be freed: + notifyAll(); + } + } + + /** + *

    Get the last accepted row.

    + * + *

    Note: + * This function is blocking until a row has been accepted or the end of the stream has been reached. + * A Thread interruption will also make this function returning. + *

    + * + * @return The last accepted row, + * or NULL if no more rows are available or if the iteration has been interrupted/canceled. + */ + public synchronized Object[] getRow(){ + try{ + // Wait until a row has been accepted or the end has been reached: + while(!endReached && pendingRow == null) + wait(); + + // If there is no more rows, just return NULL (meaning for the called "end of stream"): + if (endReached && pendingRow == null) + return null; + + /* Otherwise, reset pendingRow to NULL in order to enable the reading of the next row, + * and finally return the last accepted row: */ + Object[] row = pendingRow; + pendingRow = null; + return row; + + }catch(InterruptedException ie){ + /* If the thread has been interrupted, set this TableSink in a state similar to + * when the end of the stream has been reached: */ + endReached = true; + // Return NULL, meaning the end of the stream has been reached: + return null; + + }finally{ + // In all cases, the waiting threads must be freed: + notifyAll(); + } + } + + /** + * Extract an array of {@link TAPColumn} objects. Each corresponds to one of the columns listed in the given table, + * and so corresponds to the metadata of a column. + * + * @param table {@link StarTable} which contains only the columns' information. + * + * @return The corresponding list of {@link TAPColumn} objects. + * + * @throws DataReadException If there is a problem while resolving the field datatype (for instance: unknown datatype, a multi-dimensional array is provided, a bad number format for the arraysize). + */ + protected TAPColumn[] extractColMeta(final StarTable table) throws DataReadException{ + // Count the number columns and initialize the array: + TAPColumn[] columns = new TAPColumn[table.getColumnCount()]; + + // Add all columns meta: + for(int i = 0; i < columns.length; i++){ + // get the field: + ColumnInfo colInfo = table.getColumnInfo(i); + + // get the datatype: + String datatype = getAuxDatumValue(colInfo, "Datatype"); + + // get the arraysize: + String arraysize = ColumnInfo.formatShape(colInfo.getShape()); + + // get the xtype: + String xtype = getAuxDatumValue(colInfo, "xtype"); + + // Resolve the field type: + DBType type; + try{ + type = resolveVotType(datatype, arraysize, xtype).toTAPType(); + }catch(TAPException te){ + if (te instanceof DataReadException) + throw (DataReadException)te; + else + throw new DataReadException(te.getMessage(), te); + } + + // build the TAPColumn object: + TAPColumn col = new TAPColumn(colInfo.getName(), type, colInfo.getDescription(), colInfo.getUnitString(), colInfo.getUCD(), colInfo.getUtype()); + col.setPrincipal(false); + col.setIndexed(false); + col.setStd(false); + + // append it to the array: + columns[i] = col; + } + + return columns; + } + + /** + * Extract the specified auxiliary datum value from the given {@link ColumnInfo}. + * + * @param colInfo {@link ColumnInfo} from which the auxiliary datum must be extracted. + * @param auxDatumName The name of the datum to extract. + * + * @return The extracted value as String. + */ + protected String getAuxDatumValue(final ColumnInfo colInfo, final String auxDatumName){ + DescribedValue value = colInfo.getAuxDatumByName(auxDatumName); + return (value != null) ? value.getValue().toString() : null; + } + + } + + /** Stream containing the VOTable on which this {@link TableIterator} is iterating. */ + protected final InputStream input; + /** The StarTable consumer which is used to iterate on each row. */ + protected final StreamVOTableSink sink; + + /** Indicate whether the row iteration has already started. */ + protected boolean iterationStarted = false; + /** Indicate whether the last row has already been reached. */ + protected boolean endReached = false; + + /** The last read row. Column iteration is done on this array. */ + protected Object[] row; + /** Index of the last read column (=0 just after {@link #nextRow()} and before {@link #nextCol()}, ={@link #nbCol} after the last column has been read). */ + protected int indCol = -1; + /** Number of columns available according to the metadata. */ + protected int nbCol = 0; + + /** + * Build a TableIterator able to read rows and columns inside the given VOTable input stream. + * + * @param input Input stream over a VOTable document. + * + * @throws NullPointerException If NULL is given in parameter. + * @throws DataReadException If the given VOTable can not be parsed. + */ + public VOTableIterator(final InputStream input) throws DataReadException{ + // An input stream MUST BE provided: + if (input == null) + throw new NullPointerException("Missing VOTable document input stream over which to iterate!"); + this.input = input; + + try{ + + // Set the VOTable builder/interpreter: + final TableBuilder tb = (new StarTableFactory()).getTableBuilder("votable"); + + // Build the TableSink to use: + sink = new StreamVOTableSink(); + + // Initiate the stream process: + Thread streamThread = new Thread(){ + @Override + public void run(){ + try{ + tb.streamStarTable(input, sink, null); + }catch(IOException e){ + if (e.getMessage() != null && !e.getMessage().equals(STREAM_ABORTED_MESSAGE)) + e.printStackTrace(); + } + } + }; + streamThread.start(); + + }catch(Exception ex){ + throw new DataReadException("Unable to parse/read the given VOTable input stream!", ex); + } + } + + @Override + public TAPColumn[] getMetadata() throws DataReadException{ + return sink.getMeta(); + } + + @Override + public boolean nextRow() throws DataReadException{ + // If no more rows, return false directly: + if (endReached) + return false; + + // Fetch the row: + row = sink.getRow(); + + // Reset the column iteration: + if (!iterationStarted){ + iterationStarted = true; + nbCol = sink.getMeta().length; + } + indCol = 0; + + // Tells whether there is more rows or not: + endReached = (row == null); + return !endReached; + } + + @Override + public boolean hasNextCol() throws IllegalStateException, DataReadException{ + // Check the read state: + checkReadState(); + + // Determine whether the last column has been reached or not: + return (indCol < nbCol); + } + + @Override + public Object nextCol() throws NoSuchElementException, IllegalStateException, DataReadException{ + // Check the read state and ensure there is still at least one column to read: + if (!hasNextCol()) + throw new NoSuchElementException("No more field to read!"); + + // Get the column value: + return row[indCol++]; + } + + @Override + public DBType getColType() throws IllegalStateException, DataReadException{ + // Basically check the read state (for rows iteration): + checkReadState(); + + // Check deeper the read state (for columns iteration): + if (indCol <= 0) + throw new IllegalStateException("No field has yet been read!"); + else if (indCol > nbCol) + throw new IllegalStateException("All fields have already been read!"); + + // Return the column type: + return sink.getMeta()[indCol - 1].getDatatype(); + } + + @Override + public void close() throws DataReadException{ + endReached = true; + sink.stop(); + // input.close(); // in case sink.stop() is not enough to stop the VOTable reading! + } + + /** + *

    Check the row iteration state. That's to say whether:

    + *
      + *
    • the row iteration has started = the first row has been read = a first call of {@link #nextRow()} has been done
    • + *
    • AND the row iteration is not finished = the last row has been read.
    • + *
    + * @throws IllegalStateException + */ + protected void checkReadState() throws IllegalStateException{ + if (!iterationStarted) + throw new IllegalStateException("No row has yet been read!"); + else if (endReached) + throw new IllegalStateException("End of VOTable file already reached!"); + } + + /** + * Resolve a VOTable field type by using the datatype, arraysize and xtype strings as specified in a VOTable document. + * + * @param datatype Attribute value of VOTable corresponding to the datatype. + * @param arraysize Attribute value of VOTable corresponding to the arraysize. + * @param xtype Attribute value of VOTable corresponding to the xtype. + * + * @return The resolved VOTable field type, or a CHAR(*) type if the specified type can not be resolved. + * + * @throws DataReadException If a field datatype is unknown. + */ + public static VotType resolveVotType(final String datatype, final String arraysize, final String xtype) throws DataReadException{ + // If no datatype is specified, return immediately a CHAR(*) type: + if (datatype == null || datatype.trim().length() == 0) + return new VotType(VotDatatype.CHAR, "*"); + + // Identify the specified datatype: + VotDatatype votdatatype; + try{ + votdatatype = VotDatatype.valueOf(datatype.toUpperCase()); + }catch(IllegalArgumentException iae){ + throw new DataReadException("unknown field datatype: \"" + datatype + "\""); + } + + // Build the VOTable type: + return new VotType(votdatatype, arraysize, xtype); + } + +} diff --git a/src/tap/db/DBConnection.java b/src/tap/db/DBConnection.java index 695cf14..e836211 100644 --- a/src/tap/db/DBConnection.java +++ b/src/tap/db/DBConnection.java @@ -16,47 +16,256 @@ package tap.db; * You should have received a copy of the GNU Lesser General Public License * along with TAPLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ -import cds.savot.model.SavotTR; +import tap.TAPFactory; +import tap.data.DataReadException; +import tap.data.TableIterator; +import tap.metadata.TAPColumn; +import tap.metadata.TAPMetadata; import tap.metadata.TAPTable; -import uws.service.log.UWSLogType; - import adql.query.ADQLQuery; /** - * TODO + *

    Connection to the "database" (whatever is the type or whether it is linked to a true DBMS connection).

    + * + *

    It lets executing ADQL queries and updating the TAP datamodel (with the list of schemas, tables and columns published in TAP, + * or with uploaded tables).

    * - * @author Grégory Mantelet (CDS) - * @version 06/2012 + *

    IMPORTANT: + * This connection aims only to provide a common and known interface for any kind of database connection. + * A connection MUST be opened/created and closed/freed ONLY by the {@link TAPFactory}, which will usually merely wrap + * the real database connection by a {@link DBConnection} object. That's why this interface does not provide anymore + * a close() function. + *

    * - * @param Result type of the execution of a query (see {@link #executeQuery(String, ADQLQuery)}. + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (03/2015) */ -public interface DBConnection< R > { - - public final static UWSLogType LOG_TYPE_DB_ACTIVITY = UWSLogType.createCustomLogType("DBActivity"); +public interface DBConnection { + /** + *

    Get any identifier for this connection.

    + * + *

    note: it is used only for logging purpose.

    + * + * @return ID of this connection. + */ public String getID(); - public void startTransaction() throws DBException; - - public void cancelTransaction() throws DBException; - - public void endTransaction() throws DBException; - - public R executeQuery(final String sqlQuery, final ADQLQuery adqlQuery) throws DBException; - - public void createSchema(final String schemaName) throws DBException; + /** + *

    Fetch the whole content of TAP_SCHEMA.

    + * + *

    + * This function SHOULD be used only once: at the starting of the TAP service. It is an alternative way + * to get the published schemas, tables and columns. The other way is to build a {@link TAPMetadata} object + * yourself in function of the schemas/tables/columns you want to publish (i.e. which can be done by reading + * metadata from a XML document - following the same schema - XSD- as for the TAP resource tables) + * and then to load them in the DB (see {@link #setTAPSchema(TAPMetadata)} for more details). + *

    + * + *

    CAUTION: + * This function MUST NOT be used if the tables to publish or the standard TAP_SCHEMA tables have names in DB different from the + * ones defined by the TAP standard. So, if DB names are different from the ADQL names, you have to write yourself a way to get + * the metadata from the DB. + *

    + * + *

    Important note: + * If the schema or some standard tables or columns are missing, TAP_SCHEMA will be considered as incomplete + * and an exception will be thrown. + *

    + * + *

    Note: + * This function MUST be able to read the standard tables and columns described by the IVOA. All other tables/columns + * will be merely ignored. + *

    + * + * @return Content of TAP_SCHEMA inside the DB. + * + * @throws DBException If TAP_SCHEMA can not be found, is incomplete or if some important metadata can not be retrieved. + * + * @since 2.0 + */ + public TAPMetadata getTAPSchema() throws DBException; - public void dropSchema(final String schemaName) throws DBException; + /** + *

    Empty and then fill all the TAP_SCHEMA tables with the given list of metadata.

    + * + *

    + * This function SHOULD be used only once: at the starting of the TAP service, + * when metadata are loaded from a XML document (following the same schema - XSD- + * as for the TAP resource tables). + *

    + * + *

    + * THIS FUNCTION IS MANIPULATING THE SCHEMAS AND TABLES OF YOUR DATABASE. + * SO IT SHOULD HAVE A SPECIFIC BEHAVIOR DESCRIBED BELOW. + * SO PLEASE READ THE FOLLOWINGS AND TRY TO RESPECT IT AS MUCH AS POSSIBLE IN THE IMPLEMENTATIONS + *

    + * + *

    TAP_SCHEMA CREATION

    + *

    + * This function is MAY drop and then re-create the schema TAP_SCHEMA and all + * its tables listed in the TAP standard (TAP_SCHEMA.schemas, .tables, .columns, .keys and .key_columns). + * All other tables inside TAP_SCHEMA SHOULD NOT be modified! + *

    + * + *

    + * The schema and the tables MUST be created using either the standard definition or the + * definition provided in the {@link TAPMetadata} object (if provided). Indeed, if your definition of these TAP tables + * is different from the standard (the standard + new elements), you MUST provide your modifications in parameter + * through the {@link TAPMetadata} object so that they can be applied and taken into account in TAP_SCHEMA. + *

    + * + *

    Note: + * DB names provided in the given TAPMetadata (see {@link TAPTable#getDBSchemaName()}, {@link TAPTable#getDBName()} and {@link TAPColumn#getDBName()}) + * are used for the creation and filling of the tables. + * + * Whether these requests must be case sensitive or not SHOULD be managed by ADQLTranslator. + *

    + * + *

    TAPMetadata PARAMETER

    + *

    + * This object MUST contain all schemas, tables and columns that MUST be published. All its content will be + * used in order to fill the TAP_SCHEMA tables. + *

    + *

    + * Of course, TAP_SCHEMA and its tables MAY be provided in this object. But: + *

    + *
      + *
    • (a) if TAP_SCHEMA tables are NOT provided: + * this function SHOULD consider their definition as exactly the one provided by + * the TAP standard/protocol. If so, the standard definition MUST be automatically added + * into the {@link TAPMetadata} object AND into TAP_SCHEMA. + *
    • + *
    • (b) if TAP_SCHEMA tables ARE provided: + * the definition of all given elements will be taken into account while updating the TAP_SCHEMA. + * Each element definition not provided MUST be considered as exactly the same as the standard one + * and MUST be added into the {@link TAPMetadata} object AND into TAP_SCHEMA. + *
    • + *
    + * + *

    Note: By default, all implementations of this interface in the TAP library will fill only standard columns and tables of TAP_SCHEMA. + * To fill your own, you MUST implement yourself this interface or to extend an existing implementation.

    + * + *

    WARNING: + * (b) lets consider a TAP_SCHEMA different from the standard one. BUT, these differences MUST be only additions, + * NOT modifications or deletion of the standard definition! This function MUST be able to work AT LEAST on a + * standard definition of TAP_SCHEMA. + *

    + * + *

    FILLING BEHAVIOUR

    + *

    + * The TAP_SCHEMA tables SHOULD be completely emptied (in SQL: "DELETE FROM <table_name>;" or merely "DROP TABLE <table_name>") before insertions can be processed. + *

    + * + *

    ERRORS MANAGEMENT

    + *

    + * If any error occurs while executing any "DB" queries (in SQL: DROP, DELETE, INSERT, CREATE, ...), all queries executed + * before in this function MUST be canceled (in SQL: ROLLBACK). + *

    + * + * @param metadata List of all schemas, tables, columns and foreign keys to insert in the TAP_SCHEMA. + * + * @throws DBException If any error occurs while updating the database. + * + * @since 2.0 + */ + public void setTAPSchema(final TAPMetadata metadata) throws DBException; - public void createTable(final TAPTable table) throws DBException; + /** + * Add the defined and given table inside the TAP_UPLOAD schema. + * + *

    If the TAP_UPLOAD schema does not already exist, it will be created.

    + * + *

    note: A table of TAP_UPLOAD MUST be transient and persistent only for the lifetime of the query. + * So, this function should always be used with {@link #dropUploadedTable(TAPTable)}, which is called at + * the end of each query execution.

    + * + * @param tableDef Definition of the table to upload (list of all columns and of their type). + * @param data Rows and columns of the table to upload. + * + * @return true if the given table has been successfully added, false otherwise. + * + * @throws DBException If any error occurs while adding the table. + * @throws DataReadException If any error occurs while reading the given data (particularly if any limit - in byte or row - set in the TableIterator is reached). + * + * @since 2.0 + */ + public boolean addUploadedTable(final TAPTable tableDef, final TableIterator data) throws DBException, DataReadException; - public void insertRow(final SavotTR row, final TAPTable table) throws DBException; + /** + *

    Drop the specified uploaded table from the database. + * More precisely, it means dropping a table from the TAP_UPLOAD schema.

    + * + *

    Note: + * This function SHOULD drop only one table. So, if more than one table match in the "database" to the given one, an exception MAY be thrown. + * This behavior is implementation-dependent. + *

    + * + * @param tableDef Definition of the uploaded table to drop (the whole object is needed in order to get the DB schema and tables names). + * + * @return true if the specified table has been successfully dropped, false otherwise. + * + * @throws DBException If any error occurs while dropping the specified uploaded table. + * + * @since 2.0 + */ + public boolean dropUploadedTable(final TAPTable tableDef) throws DBException; - public void dropTable(final TAPTable table) throws DBException; + /** + *

    Let executing the given ADQL query.

    + * + *

    The result of this query must be formatted as a table, and so must be iterable using a {@link TableIterator}.

    + * + *

    note: the interpretation of the ADQL query is up to the implementation. In most of the case, it is just needed + * to translate this ADQL query into an SQL query (understandable by the chosen DBMS).

    + * + * @param adqlQuery ADQL query to execute. + * + * @return The table result. + * + * @throws DBException If any errors occurs while executing the query. + * + * @since 2.0 + */ + public TableIterator executeQuery(final ADQLQuery adqlQuery) throws DBException; - public void close() throws DBException; + /** + *

    Set the number of rows to fetch before searching/getting the following. + * Thus, rows are fetched by block whose the size is set by this function.

    + * + *

    + * This feature may not be supported. In such case or if an exception occurs while setting the fetch size, + * this function must not send any exception and the connection stays with its default fetch size. A message may be however + * logged. + *

    + * + *

    Note: + * The "fetch size" should be taken into account only for SELECT queries executed by {@link #executeQuery(ADQLQuery)}. + *

    + * + *

    + * This feature is generally implemented by JDBC drivers using the V3 protocol. Thus, here is how the PostgreSQL JDBC documentation + * (https://jdbc.postgresql.org/documentation/head/query.html#query-with-cursor) describes this feature: + *

    + *
    + *

    + * By default the driver collects all the results for the query at once. This can be inconvenient for large data sets + * so the JDBC driver provides a means of basing a ResultSet on a database cursor and only fetching a small number of rows. + *

    + *

    + * A small number of rows are cached on the client side of the connection and when exhausted the next block of rows + * is retrieved by repositioning the cursor. + *

    + *
    + * + * @param size Blocks size (in number of rows) to fetch. + * + * @since 2.0 + */ + public void setFetchSize(final int size); } diff --git a/src/tap/db/JDBCConnection.java b/src/tap/db/JDBCConnection.java index ee79567..c596f26 100644 --- a/src/tap/db/JDBCConnection.java +++ b/src/tap/db/JDBCConnection.java @@ -16,81 +16,366 @@ package tap.db; * You should have received a copy of the GNU Lesser General Public License * along with TAPLibrary. If not, see . * - * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomishes Rechen Institute (ARI) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.Driver; import java.sql.DriverManager; +import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; +import java.sql.Timestamp; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.HashMap; import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import tap.data.DataReadException; +import tap.data.ResultSetTableIterator; +import tap.data.TableIterator; import tap.log.TAPLog; import tap.metadata.TAPColumn; +import tap.metadata.TAPForeignKey; +import tap.metadata.TAPMetadata; +import tap.metadata.TAPMetadata.STDSchema; +import tap.metadata.TAPMetadata.STDTable; +import tap.metadata.TAPSchema; import tap.metadata.TAPTable; +import tap.metadata.TAPTable.TableType; +import uws.ISO8601Format; +import uws.service.log.UWSLog.LogLevel; +import adql.db.DBColumn; +import adql.db.DBType; +import adql.db.DBType.DBDatatype; +import adql.db.STCS; +import adql.db.STCS.Region; import adql.query.ADQLQuery; -import cds.savot.model.SavotTR; -import cds.savot.model.TDSet; +import adql.query.IdentifierField; +import adql.translator.ADQLTranslator; +import adql.translator.JDBCTranslator; +import adql.translator.TranslationException; /** - * Simple implementation of the {@link DBConnection} interface. - * It creates and manages a JDBC connection to a specified database. - * Thus results of any executed SQL query will be a {@link ResultSet}. + *

    This {@link DBConnection} implementation is theoretically able to deal with any DBMS JDBC connection.

    + * + *

    Note: + * "Theoretically", because its design has been done using information about Postgres, SQLite, Oracle, MySQL and Java DB (Derby). + * Then it has been really tested successfully with Postgres and SQLite. + *

    + * + *

    Deal with different DBMS features

    + * + *

    Update queries are taking into account whether the following features are supported by the DBMS:

    + *
      + *
    • data definition: when not supported, no update operation will be possible. + * All corresponding functions will then throw a {@link DBException} ; + * only {@link #executeQuery(ADQLQuery)} will be possibly called.
    • + * + *
    • transactions: when not supported, no transaction is started or merely used. + * It means that in case of update failure, no rollback will be possible + * and that already done modification will remain in the database.
    • + * + *
    • schemas: when the DBMS does not have the notion of schema (like SQLite), no schema creation or dropping will be obviously processed. + * Besides, if not already done, database name of all tables will be prefixed by the schema name.
    • + * + *
    • batch updates: when not supported, updates will just be done, "normally, one by one. + * In one word, there will be merely no optimization. + * Anyway, this feature concerns only the insertions into tables.
    • + * + *
    • case sensitivity of identifiers: the case sensitivity of quoted identifier varies from the used DBMS. This {@link DBConnection} + * implementation is able to adapt itself in function of the way identifiers are stored and + * researched in the database. How the case sensitivity is managed by the DBMS is the problem + * of only one function (which can be overwritten if needed): {@link #equals(String, String, boolean)}.
    • + *
    + * + *

    Warning: + * All these features have no impact at all on ADQL query executions ({@link #executeQuery(ADQLQuery)}). + *

    + * + *

    Datatypes

    + * + *

    + * All datatype conversions done while fetching a query result (via a {@link ResultSet}) + * are done exclusively by the returned {@link TableIterator} (so, here {@link ResultSetTableIterator}). + *

    + * + *

    + * However, datatype conversions done while uploading a table are done here by the function + * {@link #convertTypeToDB(DBType)}. This function uses first the conversion function of the translator + * ({@link JDBCTranslator#convertTypeToDB(DBType)}), and then {@link #defaultTypeConversion(DBType)} + * if it fails. + *

    + * + *

    + * In this default conversion, all typical DBMS datatypes are taken into account, EXCEPT the geometrical types + * (POINT and REGION). That's why it is recommended to use a translator in which the geometrical types are supported + * and managed. + *

    + * + *

    Fetch size

    + * + *

    + * The possibility to specify a "fetch size" to the JDBC driver (and more exactly to a {@link Statement}) may reveal + * very helpful when dealing with large datasets. Thus, it is possible to fetch rows by block of a size represented + * by this "fetch size". This is also possible with this {@link DBConnection} thanks to the function {@link #setFetchSize(int)}. + *

    + * + *

    + * However, some JDBC driver or DBMS may not support this feature. In such case, it is then automatically disabled by + * {@link JDBCConnection} so that any subsequent queries do not attempt to use it again. The {@link #supportsFetchSize} + * is however reset to true when {@link #setFetchSize(int)} is called. + *

    + * + *

    Note 1: + * The "fetch size" feature is used only for SELECT queries executed by {@link #executeQuery(ADQLQuery)}. In all other functions, + * results of SELECT queries are fetched with the default parameter of the JDBC driver and its {@link Statement} implementation. + *

    + * + *

    Note 2: + * By default, this feature is disabled. So the default value of the JDBC driver is used. + * To enable it, a simple call to {@link #setFetchSize(int)} is enough, whatever is the given value. + *

    * * @author Grégory Mantelet (CDS;ARI) - * @version 1.1 (04/2014) + * @version 2.0 (04/2015) + * @since 2.0 */ -public class JDBCConnection implements DBConnection { +public class JDBCConnection implements DBConnection { - /** JDBC prefix of any database URL (for instance: jdbc:postgresql://127.0.0.1/myDB or jdbc:postgresql:myDB). */ - public final static String JDBC_PREFIX = "jdbc"; + /** DBMS name of PostgreSQL used in the database URL. */ + protected final static String DBMS_POSTGRES = "postgresql"; + + /** DBMS name of SQLite used in the database URL. */ + protected final static String DBMS_SQLITE = "sqlite"; - /** Connection ID (typically, the job ID). */ + /** DBMS name of MySQL used in the database URL. */ + protected final static String DBMS_MYSQL = "mysql"; + + /** DBMS name of Oracle used in the database URL. */ + protected final static String DBMS_ORACLE = "oracle"; + + /** Name of the database column giving the database name of a TAP column, table or schema. */ + protected final static String DB_NAME_COLUMN = "dbname"; + + /** Connection ID (typically, the job ID). It lets identify the DB errors linked to the Job execution in the logs. */ protected final String ID; /** JDBC connection (created and initialized at the creation of this {@link JDBCConnection} instance). */ protected final Connection connection; - /** Logger to use if any message needs to be printed to the server manager. */ + /** The translator this connection must use to translate ADQL into SQL. It is also used to get information about the case sensitivity of all types of identifier (schema, table, column). */ + protected final JDBCTranslator translator; + + /** Object to use if any message needs to be logged. note: this logger may be NULL. If NULL, messages will never be printed. */ protected final TAPLog logger; + /* JDBC URL MANAGEMENT */ + + /** JDBC prefix of any database URL (for instance: jdbc:postgresql://127.0.0.1/myDB or jdbc:postgresql:myDB). */ + public final static String JDBC_PREFIX = "jdbc"; + + /** Name (in lower-case) of the DBMS with which the connection is linked. */ + protected final String dbms; + + /* DBMS SUPPORTED FEATURES */ + + /** Indicate whether the DBMS supports transactions (start, commit, rollback and end). note: If no transaction is possible, no transaction will be used, but then, it will never possible to cancel modifications in case of error. */ + protected boolean supportsTransaction; + + /** Indicate whether the DBMS supports the definition of data (create, update, drop, insert into schemas and tables). note: If not supported, it will never possible to create TAP_SCHEMA from given metadata (see {@link #setTAPSchema(TAPMetadata)}) and to upload/drop tables (see {@link #addUploadedTable(TAPTable, TableIterator)} and {@link #dropUploadedTable(TAPTable)}). */ + protected boolean supportsDataDefinition; + + /** Indicate whether the DBMS supports several updates in once (using {@link Statement#addBatch(String)} and {@link Statement#executeBatch()}). note: If not supported, every updates will be done one by one. So it is not really a problem, but just a loss of optimization. */ + protected boolean supportsBatchUpdates; + + /** Indicate whether the DBMS has the notion of SCHEMA. Most of the DBMS has it, but not SQLite for instance. note: If not supported, the DB table name will be prefixed by the DB schema name followed by the character "_". Nevertheless, if the DB schema name is NULL, the DB table name will never be prefixed. */ + protected boolean supportsSchema; + + /* CASE SENSITIVITY SUPPORT */ + + /** Indicate whether UNquoted identifiers will be considered as case INsensitive and stored in mixed case by the DBMS. note: If FALSE, unquoted identifiers will still be considered as case insensitive for the researches, but will be stored in lower or upper case (in function of {@link #lowerCaseUnquoted} and {@link #upperCaseUnquoted}). If none of these two flags is TRUE, the storage case will be though considered as mixed. */ + protected boolean supportsMixedCaseUnquotedIdentifier; + /** Indicate whether the unquoted identifiers are stored in lower case in the DBMS. */ + protected boolean lowerCaseUnquoted; + /** Indicate whether the unquoted identifiers are stored in upper case in the DBMS. */ + protected boolean upperCaseUnquoted; + + /** Indicate whether quoted identifiers will be considered as case INsensitive and stored in mixed case by the DBMS. note: If FALSE, quoted identifiers will be considered as case sensitive and will be stored either in lower, upper or in mixed case (in function of {@link #lowerCaseQuoted}, {@link #upperCaseQuoted} and {@link #mixedCaseQuoted}). If none of these three flags is TRUE, the storage case will be mixed case. */ + protected boolean supportsMixedCaseQuotedIdentifier; + /** Indicate whether the quoted identifiers are stored in lower case in the DBMS. */ + protected boolean lowerCaseQuoted; + /** Indicate whether the quoted identifiers are stored in mixed case in the DBMS. */ + protected boolean mixedCaseQuoted; + /** Indicate whether the quoted identifiers are stored in upper case in the DBMS. */ + protected boolean upperCaseQuoted; + + /* FETCH SIZE */ + + /** Special fetch size meaning that the JDBC driver is free to set its own guess for this value. */ + public final static int IGNORE_FETCH_SIZE = 0; + /** Default fetch size. + * Note 1: this value may be however ignored if the JDBC driver does not support this feature. + * Note 2: by default set to {@link #IGNORE_FETCH_SIZE}. */ + public final static int DEFAULT_FETCH_SIZE = IGNORE_FETCH_SIZE; + + /**

    Indicate whether the last fetch size operation works.

    + *

    By default, this attribute is set to false, meaning that the "fetch size" feature is + * disabled. To enable it, a simple call to {@link #setFetchSize(int)} is enough, whatever is the given value.

    + *

    If just once this operation fails, the fetch size feature will be always considered as unsupported in this {@link JDBCConnection} + * until the next call of {@link #setFetchSize(int)}.

    */ + protected boolean supportsFetchSize = false; + + /**

    Fetch size to set in the {@link Statement} in charge of executing a SELECT query.

    + *

    Note 1: this value must always be positive. If negative or null, it will be ignored and the {@link Statement} will keep its default behavior.

    + *

    Note 2: if this feature is enabled (i.e. has a value > 0), the AutoCommit will be disabled.

    */ + protected int fetchSize = DEFAULT_FETCH_SIZE; + /** - *

    - * Creates a JDBC connection to the specified database and with the specified JDBC driver. - * This connection is established using the given user name and password. - *

    - *

    note: the JDBC driver is loaded using

    Class.forName(driverPath)
    .

    + *

    Creates a JDBC connection to the specified database and with the specified JDBC driver. + * This connection is established using the given user name and password.

    + * + *

    note: the JDBC driver is loaded using

    Class.forName(driverPath)
    and the connection is created with
    DriverManager.getConnection(dbUrl, dbUser, dbPassword)
    .

    + * + *

    Warning: + * This constructor really creates a new SQL connection. Creating a SQL connection is time consuming! + * That's why it is recommended to use a pool of connections. When doing so, you should use the other constructor of this class + * ({@link #JDBCConnection(Connection, JDBCTranslator, String, TAPLog)}). + *

    * * @param driverPath Full class name of the JDBC driver. * @param dbUrl URL to the database. note This URL may not be prefixed by "jdbc:". If not, the prefix will be automatically added. - * @param dbUser Name of the database user (supposed to be the database owner). + * @param dbUser Name of the database user. * @param dbPassword Password of the given database user. - * @param logger Logger to use if any message needs to be printed to the server admin. + * @param translator {@link ADQLTranslator} to use in order to get SQL from an ADQL query and to get qualified DB table names. + * @param connID ID of this connection. note: may be NULL ; but in this case, logs concerning this connection will be more difficult to localize. + * @param logger Logger to use in case of need. note: may be NULL ; in this case, error will never be logged, but sometimes DBException may be raised. + * + * @throws DBException If the driver can not be found or if the connection can not merely be created (usually because DB parameters are wrong). + */ + public JDBCConnection(final String driverPath, final String dbUrl, final String dbUser, final String dbPassword, final JDBCTranslator translator, final String connID, final TAPLog logger) throws DBException{ + this(createConnection(driverPath, dbUrl, dbUser, dbPassword), translator, connID, logger); + } + + /** + * Create a JDBC connection by wrapping the given connection. * - * @throws DBException If the specified driver can not be found, or if the database URL or user is incorrect. + * @param conn Connection to wrap. + * @param translator {@link ADQLTranslator} to use in order to get SQL from an ADQL query and to get qualified DB table names. + * @param connID ID of this connection. note: may be NULL ; but in this case, logs concerning this connection will be more difficult to localize. + * @param logger Logger to use in case of need. note: may be NULL ; in this case, error will never be logged, but sometimes DBException may be raised. */ - public JDBCConnection(final String ID, final String driverPath, final String dbUrl, final String dbUser, final String dbPassword, final TAPLog logger) throws DBException{ + public JDBCConnection(final Connection conn, final JDBCTranslator translator, final String connID, final TAPLog logger) throws DBException{ + if (conn == null) + throw new NullPointerException("Missing SQL connection! => can not create a JDBCConnection object."); + if (translator == null) + throw new NullPointerException("Missing ADQL translator! => can not create a JDBCConnection object."); + + this.connection = conn; + this.translator = translator; + this.ID = connID; this.logger = logger; - this.ID = ID; - // Load the specified JDBC driver: + // Set the supporting features' flags + DBMS type: try{ - Class.forName(driverPath); - }catch(ClassNotFoundException cnfe){ - logger.dbError("Impossible to find the JDBC driver \"" + driverPath + "\" !", cnfe); - throw new DBException("Impossible to find the JDBC driver \"" + driverPath + "\" !", cnfe); + DatabaseMetaData dbMeta = connection.getMetaData(); + dbms = getDBMSName(dbMeta.getURL()); + supportsTransaction = dbMeta.supportsTransactions(); + supportsBatchUpdates = dbMeta.supportsBatchUpdates(); + supportsDataDefinition = dbMeta.supportsDataDefinitionAndDataManipulationTransactions(); + supportsSchema = dbMeta.supportsSchemasInTableDefinitions(); + lowerCaseUnquoted = dbMeta.storesLowerCaseIdentifiers(); + upperCaseUnquoted = dbMeta.storesUpperCaseIdentifiers(); + supportsMixedCaseUnquotedIdentifier = dbMeta.supportsMixedCaseIdentifiers(); + lowerCaseQuoted = dbMeta.storesLowerCaseQuotedIdentifiers(); + mixedCaseQuoted = dbMeta.storesMixedCaseQuotedIdentifiers(); + upperCaseQuoted = dbMeta.storesUpperCaseQuotedIdentifiers(); + supportsMixedCaseQuotedIdentifier = dbMeta.supportsMixedCaseQuotedIdentifiers(); + }catch(SQLException se){ + throw new DBException("Unable to access to one or several DB metadata (url, supportsTransaction, supportsBatchUpdates, supportsDataDefinitionAndDataManipulationTransactions, supportsSchemasInTableDefinitions, storesLowerCaseIdentifiers, storesUpperCaseIdentifiers, supportsMixedCaseIdentifiers, storesLowerCaseQuotedIdentifiers, storesMixedCaseQuotedIdentifiers, storesUpperCaseQuotedIdentifiers and supportsMixedCaseQuotedIdentifiers) from the given Connection!"); } + } - // Build a connection to the specified database: + /** + * Extract the DBMS name from the given database URL. + * + * @param dbUrl JDBC URL to access the database. This URL must start with "jdbc:" ; otherwise an exception will be thrown. + * + * @return The DBMS name as found in the given URL. + * + * @throws DBException If NULL has been given, if the URL is not a JDBC one (starting with "jdbc:") or if the DBMS name is missing. + */ + protected static final String getDBMSName(String dbUrl) throws DBException{ + if (dbUrl == null) + throw new DBException("Missing database URL!"); + + if (!dbUrl.startsWith(JDBC_PREFIX + ":")) + throw new DBException("This DBConnection implementation is only able to deal with JDBC connection! (the DB URL must start with \"" + JDBC_PREFIX + ":\" ; given url: " + dbUrl + ")"); + + dbUrl = dbUrl.substring(5); + int indSep = dbUrl.indexOf(':'); + if (indSep <= 0) + throw new DBException("Incorrect database URL: " + dbUrl); + + return dbUrl.substring(0, indSep).toLowerCase(); + } + + /** + * Create a {@link Connection} instance using the given database parameters. + * The path of the JDBC driver will be used to load the adequate driver if none is found by default. + * + * @param driverPath Path to the JDBC driver. + * @param dbUrl JDBC URL to connect to the database. note This URL may not be prefixed by "jdbc:". If not, the prefix will be automatically added. + * @param dbUser Name of the user to use to connect to the database. + * @param dbPassword Password of the user to use to connect to the database. + * + * @return A new DB connection. + * + * @throws DBException If the driver can not be found or if the connection can not merely be created (usually because DB parameters are wrong). + * + * @see DriverManager#getDriver(String) + * @see Driver#connect(String, Properties) + */ + private final static Connection createConnection(final String driverPath, final String dbUrl, final String dbUser, final String dbPassword) throws DBException{ + // Normalize the DB URL: String url = dbUrl.startsWith(JDBC_PREFIX) ? dbUrl : (JDBC_PREFIX + dbUrl); + + // Select the JDBDC driver: + Driver d; + try{ + d = DriverManager.getDriver(dbUrl); + }catch(SQLException e){ + try{ + // ...load it, if necessary: + if (driverPath == null) + throw new DBException("Missing JDBC driver path! Since the required JDBC driver is not yet loaded, this path is needed to load it."); + Class.forName(driverPath); + // ...and try again: + d = DriverManager.getDriver(dbUrl); + }catch(ClassNotFoundException cnfe){ + throw new DBException("Impossible to find the JDBC driver \"" + driverPath + "\" !", cnfe); + }catch(SQLException se){ + throw new DBException("No suitable JDBC driver found for the database URL \"" + dbUrl + "\" and the driver path \"" + driverPath + "\"!", se); + } + } + + // Build a connection to the specified database: try{ - connection = DriverManager.getConnection(url, dbUser, dbPassword); - logger.connectionOpened(this, (dbUrl.lastIndexOf('/') > 0 ? dbUrl.substring(dbUrl.lastIndexOf('/')) : dbUrl.substring(dbUrl.lastIndexOf(':')))); + Properties p = new Properties(); + if (dbUser != null) + p.setProperty("user", dbUser); + if (dbPassword != null) + p.setProperty("password", dbPassword); + Connection con = d.connect(url, p); + return con; }catch(SQLException se){ - logger.dbError("Impossible to establish a connection to the database \"" + url + "\" !", se); - throw new DBException("Impossible to establish a connection to the database \"" + url + "\" !", se); + throw new DBException("Impossible to establish a connection to the database \"" + url + "\"!", se); } } @@ -99,214 +384,2395 @@ public class JDBCConnection implements DBConnection { return ID; } + /** + *

    Get the JDBC connection wrapped by this {@link JDBCConnection} object.

    + * + *

    Note: + * This is the best way to get the JDBC connection in order to properly close it. + *

    + * + * @return The wrapped JDBC connection. + */ + public final Connection getInnerConnection(){ + return connection; + } + + /* ********************* */ + /* INTERROGATION METHODS */ + /* ********************* */ @Override - public void startTransaction() throws DBException{ + public TableIterator executeQuery(final ADQLQuery adqlQuery) throws DBException{ + String sql = null; + ResultSet result = null; try{ - Statement st = connection.createStatement(); - st.execute("begin"); - logger.transactionStarted(this); + // 1. Translate the ADQL query into SQL: + if (logger != null) + logger.logDB(LogLevel.INFO, this, "TRANSLATE", "Translating ADQL: " + adqlQuery.toADQL().replaceAll("(\t|\r?\n)+", " "), null); + sql = translator.translate(adqlQuery); + + // 2. Create the statement and if needed, configure it for the given fetch size: + if (supportsFetchSize && fetchSize > 0){ + try{ + connection.setAutoCommit(false); + }catch(SQLException se){ + supportsFetchSize = false; + if (logger != null) + logger.logDB(LogLevel.WARNING, this, "RESULT", "Fetch size unsupported!", null); + } + } + Statement stmt = connection.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); + if (supportsFetchSize){ + try{ + stmt.setFetchSize(fetchSize); + }catch(SQLException se){ + supportsFetchSize = false; + if (logger != null) + logger.logDB(LogLevel.WARNING, this, "RESULT", "Fetch size unsupported!", null); + } + } + + // 3. Execute the SQL query: + result = stmt.executeQuery(sql); + if (logger != null) + logger.logDB(LogLevel.INFO, this, "EXECUTE", "SQL query: " + sql.replaceAll("(\t|\r?\n)+", " "), null); + + // 4. Return the result through a TableIterator object: + if (logger != null) + logger.logDB(LogLevel.INFO, this, "RESULT", "Returning result (" + (supportsFetchSize ? "fetch size = " + fetchSize : "all in once") + ").", null); + return createTableIterator(result, adqlQuery.getResultingColumns()); + }catch(SQLException se){ - logger.dbError("Impossible to begin a transaction !", se); - throw new DBException("Impossible to begin a transaction !", se); + close(result); + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "EXECUTE", "Unexpected error while EXECUTING SQL query!", null); + throw new DBException("Unexpected error while executing a SQL query: " + se.getMessage(), se); + }catch(TranslationException te){ + close(result); + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "TRANSLATE", "Unexpected error while TRANSLATING ADQL into SQL!", null); + throw new DBException("Unexpected error while translating ADQL into SQL: " + te.getMessage(), te); + }catch(DataReadException dre){ + close(result); + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "RESULT", "Unexpected error while reading the query result!", null); + throw new DBException("Impossible to read the query result, because: " + dre.getMessage(), dre); } } - @Override - public void cancelTransaction() throws DBException{ - try{ - connection.rollback(); - logger.transactionCancelled(this); - }catch(SQLException se){ - logger.dbError("Impossible to cancel/rollback a transaction !", se); - throw new DBException("Impossible to cancel (rollback) the transaction !", se); + /** + * Create a {@link TableIterator} instance which lets reading the given result table. + * + * @param rs Result of an SQL query. + * @param resultingColumns Metadata corresponding to each columns of the result. + * + * @return A {@link TableIterator} instance. + * + * @throws DataReadException If the metadata (columns count and types) can not be fetched + * or if any other error occurs. + */ + protected TableIterator createTableIterator(final ResultSet rs, final DBColumn[] resultingColumns) throws DataReadException{ + return new ResultSetTableIterator(rs, translator, dbms, resultingColumns); + } + + /* *********************** */ + /* TAP_SCHEMA MANIPULATION */ + /* *********************** */ + + /** + * Tell when, compared to the other TAP standard tables, a given standard TAP table should be created. + * + * @param table Standard TAP table. + * + * @return An index between 0 and 4 (included) - 0 meaning the first table to create whereas 4 is the last one. + * -1 is returned if NULL is given in parameter of if the standard table is not taken into account here. + */ + protected int getCreationOrder(final STDTable table){ + if (table == null) + return -1; + + switch(table){ + case SCHEMAS: + return 0; + case TABLES: + return 1; + case COLUMNS: + return 2; + case KEYS: + return 3; + case KEY_COLUMNS: + return 4; + default: + return -1; } } + /* ************************************ */ + /* GETTING TAP_SCHEMA FROM THE DATABASE */ + /* ************************************ */ + + /** + *

    In this implementation, this function is first creating a virgin {@link TAPMetadata} object + * that will be filled progressively by calling the following functions:

    + *
      + *
    1. {@link #loadSchemas(TAPTable, TAPMetadata, Statement)}
    2. + *
    3. {@link #loadTables(TAPTable, TAPMetadata, Statement)}
    4. + *
    5. {@link #loadColumns(TAPTable, List, Statement)}
    6. + *
    7. {@link #loadKeys(TAPTable, TAPTable, List, Statement)}
    8. + *
    + * + *

    Note: + * If schemas are not supported by this DBMS connection, the DB name of all tables will be set to NULL + * and the DB name of all tables will be prefixed by the ADQL name of their respective schema. + *

    + * + * @see tap.db.DBConnection#getTAPSchema() + */ @Override - public void endTransaction() throws DBException{ + public TAPMetadata getTAPSchema() throws DBException{ + // Build a virgin TAP metadata: + TAPMetadata metadata = new TAPMetadata(); + + // Get the definition of the standard TAP_SCHEMA tables: + TAPSchema tap_schema = TAPMetadata.getStdSchema(supportsSchema); + + // LOAD ALL METADATA FROM THE STANDARD TAP TABLES: + Statement stmt = null; try{ - connection.commit(); - logger.transactionEnded(this); + // create a common statement for all loading functions: + stmt = connection.createStatement(); + + // load all schemas from TAP_SCHEMA.schemas: + if (logger != null) + logger.logDB(LogLevel.INFO, this, "LOAD_TAP_SCHEMA", "Loading TAP_SCHEMA.schemas.", null); + loadSchemas(tap_schema.getTable(STDTable.SCHEMAS.label), metadata, stmt); + + // load all tables from TAP_SCHEMA.tables: + if (logger != null) + logger.logDB(LogLevel.INFO, this, "LOAD_TAP_SCHEMA", "Loading TAP_SCHEMA.tables.", null); + List lstTables = loadTables(tap_schema.getTable(STDTable.TABLES.label), metadata, stmt); + + // load all columns from TAP_SCHEMA.columns: + if (logger != null) + logger.logDB(LogLevel.INFO, this, "LOAD_TAP_SCHEMA", "Loading TAP_SCHEMA.columns.", null); + loadColumns(tap_schema.getTable(STDTable.COLUMNS.label), lstTables, stmt); + + // load all foreign keys from TAP_SCHEMA.keys and TAP_SCHEMA.key_columns: + if (logger != null) + logger.logDB(LogLevel.INFO, this, "LOAD_TAP_SCHEMA", "Loading TAP_SCHEMA.keys and TAP_SCHEMA.key_columns.", null); + loadKeys(tap_schema.getTable(STDTable.KEYS.label), tap_schema.getTable(STDTable.KEY_COLUMNS.label), lstTables, stmt); + }catch(SQLException se){ - logger.dbError("Impossible to end/commit a transaction !", se); - throw new DBException("Impossible to end/commit the transaction !", se); + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "LOAD_TAP_SCHEMA", "Impossible to create a Statement!", se); + throw new DBException("Can not create a Statement!", se); + }finally{ + close(stmt); } + + return metadata; } - @Override - public void close() throws DBException{ + /** + *

    Load into the given metadata all schemas listed in TAP_SCHEMA.schemas.

    + * + *

    Note: + * If schemas are not supported by this DBMS connection, the DB name of the loaded schemas is set to NULL. + *

    + * + * @param tableDef Definition of the table TAP_SCHEMA.schemas. + * @param metadata Metadata to fill with all found schemas. + * @param stmt Statement to use in order to interact with the database. + * + * @throws DBException If any error occurs while interacting with the database. + */ + protected void loadSchemas(final TAPTable tableDef, final TAPMetadata metadata, final Statement stmt) throws DBException{ + ResultSet rs = null; try{ - connection.close(); - logger.connectionClosed(this); + // Determine whether the dbName column exists: + /* note: if the schema notion is not supported by this DBMS, the column "dbname" is ignored. */ + boolean hasDBName = supportsSchema && isColumnExisting(tableDef.getDBSchemaName(), tableDef.getDBName(), DB_NAME_COLUMN, connection.getMetaData()); + + // Build the SQL query: + StringBuffer sqlBuf = new StringBuffer("SELECT "); + sqlBuf.append(translator.getColumnName(tableDef.getColumn("schema_name"))); + sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("description"))); + sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("utype"))); + if (hasDBName) + sqlBuf.append(", ").append(DB_NAME_COLUMN); + sqlBuf.append(" FROM ").append(translator.getTableName(tableDef, supportsSchema)).append(';'); + + // Execute the query: + rs = stmt.executeQuery(sqlBuf.toString()); + + // Create all schemas: + while(rs.next()){ + String schemaName = rs.getString(1), description = rs.getString(2), utype = rs.getString(3), dbName = (hasDBName ? rs.getString(4) : null); + + // create the new schema: + TAPSchema newSchema = new TAPSchema(schemaName, nullifyIfNeeded(description), nullifyIfNeeded(utype)); + if (dbName != null && dbName.trim().length() > 0) + newSchema.setDBName(dbName); + + // add the new schema inside the given metadata: + metadata.addSchema(newSchema); + } }catch(SQLException se){ - logger.dbError("Impossible to close a database transaction !", se); - throw new DBException("Impossible to close the database transaction !", se); + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "LOAD_TAP_SCHEMA", "Impossible to load schemas from TAP_SCHEMA.schemas!", se); + throw new DBException("Impossible to load schemas from TAP_SCHEMA.schemas!", se); + }finally{ + close(rs); } } - /* ********************* */ - /* INTERROGATION METHODS */ - /* ********************* */ - @Override - public ResultSet executeQuery(final String sqlQuery, final ADQLQuery adqlQuery) throws DBException{ + /** + *

    Load into the corresponding metadata all tables listed in TAP_SCHEMA.tables.

    + * + *

    Note: + * Schemas are searched in the given metadata by their ADQL name and case sensitively. + * If they can not be found a {@link DBException} is thrown. + *

    + * + *

    Note: + * If schemas are not supported by this DBMS connection, the DB name of the loaded + * {@link TAPTable}s is prefixed by the ADQL name of their respective schema. + *

    + * + * @param tableDef Definition of the table TAP_SCHEMA.tables. + * @param metadata Metadata (containing already all schemas listed in TAP_SCHEMA.schemas). + * @param stmt Statement to use in order to interact with the database. + * + * @return The complete list of all loaded tables. note: this list is required by {@link #loadColumns(TAPTable, List, Statement)}. + * + * @throws DBException If a schema can not be found, or if any other error occurs while interacting with the database. + */ + protected List loadTables(final TAPTable tableDef, final TAPMetadata metadata, final Statement stmt) throws DBException{ + ResultSet rs = null; try{ - Statement stmt = connection.createStatement(); - logger.sqlQueryExecuting(this, sqlQuery); - ResultSet result = stmt.executeQuery(sqlQuery); - logger.sqlQueryExecuted(this, sqlQuery); - return result; + // Determine whether the dbName column exists: + boolean hasDBName = isColumnExisting(tableDef.getDBSchemaName(), tableDef.getDBName(), DB_NAME_COLUMN, connection.getMetaData()); + + // Build the SQL query: + StringBuffer sqlBuf = new StringBuffer("SELECT "); + sqlBuf.append(translator.getColumnName(tableDef.getColumn("schema_name"))); + sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("table_name"))); + sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("table_type"))); + sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("description"))); + sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("utype"))); + if (hasDBName) + sqlBuf.append(", ").append(DB_NAME_COLUMN); + sqlBuf.append(" FROM ").append(translator.getTableName(tableDef, supportsSchema)).append(';'); + + // Execute the query: + rs = stmt.executeQuery(sqlBuf.toString()); + + // Create all tables: + ArrayList lstTables = new ArrayList(); + while(rs.next()){ + String schemaName = rs.getString(1), tableName = rs.getString(2), typeStr = rs.getString(3), description = rs.getString(4), utype = rs.getString(5), dbName = (hasDBName ? rs.getString(6) : null); + + // get the schema: + TAPSchema schema = metadata.getSchema(schemaName); + if (schema == null){ + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "LOAD_TAP_SCHEMA", "Impossible to find the schema of the table \"" + tableName + "\": \"" + schemaName + "\"!", null); + throw new DBException("Impossible to find the schema of the table \"" + tableName + "\": \"" + schemaName + "\"!"); + } + + // If the table name is qualified, check its prefix (it must match to the schema name): + int endPrefix = tableName.indexOf('.'); + if (endPrefix >= 0){ + if (endPrefix == 0) + throw new DBException("Incorrect table name syntax: \"" + tableName + "\"! Missing schema name (before '.')."); + else if (endPrefix == tableName.length() - 1) + throw new DBException("Incorrect table name syntax: \"" + tableName + "\"! Missing table name (after '.')."); + else if (schemaName == null) + throw new DBException("Incorrect schema prefix for the table \"" + tableName.substring(endPrefix + 1) + "\": this table is not in a schema, according to the column \"schema_name\" of TAP_SCHEMA.tables!"); + else if (!tableName.substring(0, endPrefix).trim().equalsIgnoreCase(schemaName)) + throw new DBException("Incorrect schema prefix for the table \"" + schemaName + "." + tableName.substring(tableName.indexOf('.') + 1) + "\": " + tableName + "! Mismatch between the schema specified in prefix of the column \"table_name\" and in the column \"schema_name\"."); + } + + // resolve the table type (if any) ; by default, it will be "table": + TableType type = TableType.table; + if (typeStr != null){ + try{ + type = TableType.valueOf(typeStr.toLowerCase()); + }catch(IllegalArgumentException iae){} + } + + // create the new table: + TAPTable newTable = new TAPTable(tableName, type, nullifyIfNeeded(description), nullifyIfNeeded(utype)); + newTable.setDBName(dbName); + + // add the new table inside its corresponding schema: + schema.addTable(newTable); + lstTables.add(newTable); + } + + return lstTables; }catch(SQLException se){ - logger.sqlQueryError(this, sqlQuery, se); - throw new DBException("Unexpected error while executing a SQL query: " + se.getMessage(), se); + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "LOAD_TAP_SCHEMA", "Impossible to load tables from TAP_SCHEMA.tables!", se); + throw new DBException("Impossible to load tables from TAP_SCHEMA.tables!", se); + }finally{ + close(rs); } } - /* ************** */ - /* UPLOAD METHODS */ - /* ************** */ - @Override - public void createSchema(final String schemaName) throws DBException{ - String sql = "CREATE SCHEMA " + schemaName + ";"; + /** + *

    Load into the corresponding tables all columns listed in TAP_SCHEMA.columns.

    + * + *

    Note: + * Tables are searched in the given list by their ADQL name and case sensitively. + * If they can not be found a {@link DBException} is thrown. + *

    + * + * @param tableDef Definition of the table TAP_SCHEMA.columns. + * @param lstTables List of all published tables (= all tables listed in TAP_SCHEMA.tables). + * @param stmt Statement to use in order to interact with the database. + * + * @throws DBException If a table can not be found, or if any other error occurs while interacting with the database. + */ + protected void loadColumns(final TAPTable tableDef, final List lstTables, final Statement stmt) throws DBException{ + ResultSet rs = null; try{ - Statement stmt = connection.createStatement(); - stmt.executeUpdate(sql); - logger.schemaCreated(this, schemaName); + // Determine whether the dbName column exists: + boolean hasDBName = isColumnExisting(tableDef.getDBSchemaName(), tableDef.getDBName(), DB_NAME_COLUMN, connection.getMetaData()); + + // Build the SQL query: + StringBuffer sqlBuf = new StringBuffer("SELECT "); + sqlBuf.append(translator.getColumnName(tableDef.getColumn("table_name"))); + sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("column_name"))); + sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("description"))); + sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("unit"))); + sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("ucd"))); + sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("utype"))); + sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("datatype"))); + sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("size"))); + sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("principal"))); + sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("indexed"))); + sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("std"))); + if (hasDBName) + sqlBuf.append(", ").append(DB_NAME_COLUMN); + sqlBuf.append(" FROM ").append(translator.getTableName(tableDef, supportsSchema)).append(';'); + + // Execute the query: + rs = stmt.executeQuery(sqlBuf.toString()); + + // Create all tables: + while(rs.next()){ + String tableName = rs.getString(1), columnName = rs.getString(2), description = rs.getString(3), unit = rs.getString(4), ucd = rs.getString(5), utype = rs.getString(6), datatype = rs.getString(7), dbName = (hasDBName ? rs.getString(12) : null); + int size = rs.getInt(8); + boolean principal = toBoolean(rs.getObject(9)), indexed = toBoolean(rs.getObject(10)), std = toBoolean(rs.getObject(11)); + + // get the table: + TAPTable table = searchTable(tableName, lstTables.iterator()); + if (table == null){ + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "LOAD_TAP_SCHEMA", "Impossible to find the table of the column \"" + columnName + "\": \"" + tableName + "\"!", null); + throw new DBException("Impossible to find the table of the column \"" + columnName + "\": \"" + tableName + "\"!"); + } + + // resolve the column type (if any) ; by default, it will be "VARCHAR" if unknown or missing: + DBDatatype tapDatatype = null; + // ...try to resolve the datatype in function of all datatypes declared by the TAP standard. + if (datatype != null){ + try{ + tapDatatype = DBDatatype.valueOf(datatype.toUpperCase()); + }catch(IllegalArgumentException iae){} + } + // ...build the column type: + DBType type; + if (tapDatatype == null) + type = new DBType(DBDatatype.VARCHAR); + else + type = new DBType(tapDatatype, size); + + // create the new column: + TAPColumn newColumn = new TAPColumn(columnName, type, nullifyIfNeeded(description), nullifyIfNeeded(unit), nullifyIfNeeded(ucd), nullifyIfNeeded(utype)); + newColumn.setPrincipal(principal); + newColumn.setIndexed(indexed); + newColumn.setStd(std); + newColumn.setDBName(dbName); + + // add the new column inside its corresponding table: + table.addColumn(newColumn); + } }catch(SQLException se){ - logger.dbError("Impossible to create the schema \"" + schemaName + "\" !", se); - throw new DBException("Impossible to create the schema \"" + schemaName + "\" !", se); + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "LOAD_TAP_SCHEMA", "Impossible to load columns from TAP_SCHEMA.columns!", se); + throw new DBException("Impossible to load columns from TAP_SCHEMA.columns!", se); + }finally{ + close(rs); } } - @Override - public void dropSchema(final String schemaName) throws DBException{ - String sql = "DROP SCHEMA IF EXISTS " + schemaName + " CASCADE;"; + /** + *

    Load into the corresponding tables all keys listed in TAP_SCHEMA.keys and detailed in TAP_SCHEMA.key_columns.

    + * + *

    Note: + * Tables and columns are searched in the given list by their ADQL name and case sensitively. + * If they can not be found a {@link DBException} is thrown. + *

    + * + * @param keysDef Definition of the table TAP_SCHEMA.keys. + * @param keyColumnsDef Definition of the table TAP_SCHEMA.key_columns. + * @param lstTables List of all published tables (= all tables listed in TAP_SCHEMA.tables). + * @param stmt Statement to use in order to interact with the database. + * + * @throws DBException If a table or a column can not be found, or if any other error occurs while interacting with the database. + */ + protected void loadKeys(final TAPTable keysDef, final TAPTable keyColumnsDef, final List lstTables, final Statement stmt) throws DBException{ + ResultSet rs = null; + PreparedStatement keyColumnsStmt = null; try{ - Statement stmt = connection.createStatement(); - stmt.executeUpdate(sql); - logger.schemaDropped(this, schemaName); + // Prepare the query to get the columns of each key: + StringBuffer sqlBuf = new StringBuffer("SELECT "); + sqlBuf.append(translator.getColumnName(keyColumnsDef.getColumn("key_id"))); + sqlBuf.append(", ").append(translator.getColumnName(keyColumnsDef.getColumn("from_column"))); + sqlBuf.append(", ").append(translator.getColumnName(keyColumnsDef.getColumn("target_column"))); + sqlBuf.append(" FROM ").append(translator.getTableName(keyColumnsDef, supportsSchema)); + sqlBuf.append(" WHERE ").append(translator.getColumnName(keyColumnsDef.getColumn("key_id"))).append(" = ?").append(';'); + keyColumnsStmt = connection.prepareStatement(sqlBuf.toString()); + + // Build the SQL query to get the keys: + sqlBuf.delete(0, sqlBuf.length()); + sqlBuf.append("SELECT ").append(translator.getColumnName(keysDef.getColumn("key_id"))); + sqlBuf.append(", ").append(translator.getColumnName(keysDef.getColumn("from_table"))); + sqlBuf.append(", ").append(translator.getColumnName(keysDef.getColumn("target_table"))); + sqlBuf.append(", ").append(translator.getColumnName(keysDef.getColumn("description"))); + sqlBuf.append(", ").append(translator.getColumnName(keysDef.getColumn("utype"))); + sqlBuf.append(" FROM ").append(translator.getTableName(keysDef, supportsSchema)).append(';'); + + // Execute the query: + rs = stmt.executeQuery(sqlBuf.toString()); + + // Create all foreign keys: + while(rs.next()){ + String key_id = rs.getString(1), from_table = rs.getString(2), target_table = rs.getString(3), description = rs.getString(4), utype = rs.getString(5); + + // get the two tables (source and target): + TAPTable sourceTable = searchTable(from_table, lstTables.iterator()); + if (sourceTable == null){ + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "LOAD_TAP_SCHEMA", "Impossible to find the source table of the foreign key \"" + key_id + "\": \"" + from_table + "\"!", null); + throw new DBException("Impossible to find the source table of the foreign key \"" + key_id + "\": \"" + from_table + "\"!"); + } + TAPTable targetTable = searchTable(target_table, lstTables.iterator()); + if (targetTable == null){ + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "LOAD_TAP_SCHEMA", "Impossible to find the target table of the foreign key \"" + key_id + "\": \"" + target_table + "\"!", null); + throw new DBException("Impossible to find the target table of the foreign key \"" + key_id + "\": \"" + target_table + "\"!"); + } + + // get the list of columns joining the two tables of the foreign key: + HashMap columns = new HashMap(); + ResultSet rsKeyCols = null; + try{ + keyColumnsStmt.setString(1, key_id); + rsKeyCols = keyColumnsStmt.executeQuery(); + while(rsKeyCols.next()) + columns.put(rsKeyCols.getString(1), rsKeyCols.getString(2)); + }catch(SQLException se){ + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "LOAD_TAP_SCHEMA", "Impossible to load key columns from TAP_SCHEMA.key_columns for the foreign key: \"" + key_id + "\"!", se); + throw new DBException("Impossible to load key columns from TAP_SCHEMA.key_columns for the foreign key: \"" + key_id + "\"!", se); + }finally{ + close(rsKeyCols); + } + + // create and add the new foreign key inside the source table: + try{ + sourceTable.addForeignKey(key_id, targetTable, columns, nullifyIfNeeded(description), nullifyIfNeeded(utype)); + }catch(Exception ex){ + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "LOAD_TAP_SCHEMA", "Impossible to create the foreign key \"" + key_id + "\" because: " + ex.getMessage(), ex); + throw new DBException("Impossible to create the foreign key \"" + key_id + "\" because: " + ex.getMessage(), ex); + } + } }catch(SQLException se){ - logger.dbError("Impossible to drop the schema \"" + schemaName + "\" !", se); - throw new DBException("Impossible to drop the schema \"" + schemaName + "\" !", se); + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "LOAD_TAP_SCHEMA", "Impossible to load columns from TAP_SCHEMA.columns!", se); + throw new DBException("Impossible to load columns from TAP_SCHEMA.columns!", se); + }finally{ + close(rs); + close(keyColumnsStmt); } } + /* ********************************** */ + /* SETTING TAP_SCHEMA IN THE DATABASE */ + /* ********************************** */ + + /** + *

    This function is just calling the following functions:

    + *
      + *
    1. {@link #mergeTAPSchemaDefs(TAPMetadata)}
    2. + *
    3. {@link #startTransaction()}
    4. + *
    5. {@link #resetTAPSchema(Statement, TAPTable[])}
    6. + *
    7. {@link #createTAPSchemaTable(TAPTable, Statement)} for each standard TAP_SCHEMA table
    8. + *
    9. {@link #fillTAPSchema(TAPMetadata)}
    10. + *
    11. {@link #createTAPTableIndexes(TAPTable, Statement)} for each standard TA_SCHEMA table
    12. + *
    13. {@link #commit()} or {@link #rollback()}
    14. + *
    15. {@link #endTransaction()}
    16. + *
    + * + *

    Important note: + * If the connection does not support transactions, then there will be merely no transaction. + * Consequently, any failure (exception/error) will not clean the partial modifications done by this function. + *

    + * + * @see tap.db.DBConnection#setTAPSchema(tap.metadata.TAPMetadata) + */ @Override - public void createTable(final TAPTable table) throws DBException{ - // Build the SQL query: - StringBuffer sqlBuf = new StringBuffer(); - sqlBuf.append("CREATE TABLE ").append(table.getDBSchemaName()).append('.').append(table.getDBName()).append("("); - Iterator it = table.getColumns(); - while(it.hasNext()){ - TAPColumn col = it.next(); - sqlBuf.append('"').append(col.getDBName()).append("\" ").append(' ').append(getDBType(col.getDatatype(), col.getArraySize(), logger)); - if (it.hasNext()) - sqlBuf.append(','); - } - sqlBuf.append(");"); + public void setTAPSchema(final TAPMetadata metadata) throws DBException{ + Statement stmt = null; - // Execute the creation query: - String sql = sqlBuf.toString(); try{ - Statement stmt = connection.createStatement(); - stmt.executeUpdate(sql); - logger.tableCreated(this, table); + // A. GET THE DEFINITION OF ALL STANDARD TAP TABLES: + TAPTable[] stdTables = mergeTAPSchemaDefs(metadata); + + startTransaction(); + + // B. RE-CREATE THE STANDARD TAP_SCHEMA TABLES: + stmt = connection.createStatement(); + + // 1. Ensure TAP_SCHEMA exists and drop all its standard TAP tables: + if (logger != null) + logger.logDB(LogLevel.INFO, this, "CLEAN_TAP_SCHEMA", "Cleaning TAP_SCHEMA.", null); + resetTAPSchema(stmt, stdTables); + + // 2. Create all standard TAP tables: + if (logger != null) + logger.logDB(LogLevel.INFO, this, "CREATE_TAP_SCHEMA", "Creating TAP_SCHEMA tables.", null); + for(TAPTable table : stdTables) + createTAPSchemaTable(table, stmt); + + // C. FILL THE NEW TABLE USING THE GIVEN DATA ITERATOR: + if (logger != null) + logger.logDB(LogLevel.INFO, this, "CREATE_TAP_SCHEMA", "Filling TAP_SCHEMA tables.", null); + fillTAPSchema(metadata); + + // D. CREATE THE INDEXES OF ALL STANDARD TAP TABLES: + if (logger != null) + logger.logDB(LogLevel.INFO, this, "CREATE_TAP_SCHEMA", "Creating TAP_SCHEMA tables' indexes.", null); + for(TAPTable table : stdTables) + createTAPTableIndexes(table, stmt); + + commit(); }catch(SQLException se){ - logger.dbError("Impossible to create the table \"" + table.getFullName() + "\" !", se); - throw new DBException("Impossible to create the table \"" + table.getFullName() + "\" !", se); + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "CREATE_TAP_SCHEMA", "Impossible to SET TAP_SCHEMA in DB!", se); + rollback(); + throw new DBException("Impossible to SET TAP_SCHEMA in DB!", se); + }finally{ + close(stmt); + endTransaction(); } } /** - * Gets the database type corresponding to the given {@link TAPColumn} type. + *

    Merge the definition of TAP_SCHEMA tables given in parameter with the definition provided in the TAP standard.

    + * + *

    + * The goal is to get in output the list of all standard TAP_SCHEMA tables. But it must take into account the customized + * definition given in parameter if there is one. Indeed, if a part of TAP_SCHEMA is not provided, it will be completed here by the + * definition provided in the TAP standard. And so, if the whole TAP_SCHEMA is not provided at all, the returned tables will be those + * of the IVOA standard. + *

    * - * @param datatype Column datatype (short, int, long, float, double, boolea, char or unsignedByte). - * @param arraysize Size of the array type (1 if not an array, a value > 1 for an array). - * @param logger Object to use to print warnings (for instance, if a given datatype is unknown). + *

    Important note: + * If the TAP_SCHEMA definition is missing or incomplete in the given metadata, it will be added or completed automatically + * by this function with the definition provided in the IVOA TAP standard. + *

    * - * @return The corresponding database type or the given datatype if unknown. + *

    Note: + * Only the standard tables of TAP_SCHEMA are considered. The others are skipped (that's to say: never returned by this function ; + * however, they will stay in the given metadata). + *

    + * + *

    Note: + * If schemas are not supported by this DBMS connection, the DB name of schemas is set to NULL and + * the DB name of tables is prefixed by the schema name. + *

    + * + * @param metadata Metadata (with or without TAP_SCHEMA schema or some of its table). Must not be NULL + * + * @return The list of all standard TAP_SCHEMA tables, ordered by creation order (see {@link #getCreationOrder(tap.metadata.TAPMetadata.STDTable)}). + * + * @see TAPMetadata#resolveStdTable(String) + * @see TAPMetadata#getStdSchema(boolean) + * @see TAPMetadata#getStdTable(STDTable) */ - public static String getDBType(String datatype, final int arraysize, final TAPLog logger){ - datatype = (datatype == null) ? null : datatype.trim().toLowerCase(); + protected TAPTable[] mergeTAPSchemaDefs(final TAPMetadata metadata){ + // 1. Get the TAP_SCHEMA schema from the given metadata: + TAPSchema tapSchema = null; + Iterator itSchema = metadata.iterator(); + while(tapSchema == null && itSchema.hasNext()){ + TAPSchema schema = itSchema.next(); + if (schema.getADQLName().equalsIgnoreCase(STDSchema.TAPSCHEMA.label)) + tapSchema = schema; + } - if (datatype == null || datatype.isEmpty()){ - if (logger != null) - logger.warning("undefined datatype => considered as VARCHAR !"); - return "VARCHAR"; - } - - if (datatype.equals("short")) - return (arraysize == 1) ? "INT2" : "BYTEA"; - else if (datatype.equals("int")) - return (arraysize == 1) ? "INT4" : "BYTEA"; - else if (datatype.equals("long")) - return (arraysize == 1) ? "INT8" : "BYTEA"; - else if (datatype.equals("float")) - return (arraysize == 1) ? "FLOAT4" : "BYTEA"; - else if (datatype.equals("double")) - return (arraysize == 1) ? "FLOAT8" : "BYTEA"; - else if (datatype.equals("boolean")) - return (arraysize == 1) ? "BOOL" : "BYTEA"; - else if (datatype.equals("char")) - return (arraysize == 1) ? "CHAR(1)" : ((arraysize <= 0) ? "VARCHAR" : ("VARCHAR(" + arraysize + ")")); - else if (datatype.equals("unsignedbyte")) - return "BYTEA"; - else{ - if (logger != null) - logger.dbInfo("Warning: unknown datatype: \"" + datatype + "\" => considered as \"" + datatype + "\" !"); - return datatype; + // 2. Get the provided definition of the standard TAP tables: + TAPTable[] customStdTables = new TAPTable[5]; + if (tapSchema != null){ + + /* if the schemas are not supported with this DBMS, + * remove its DB name: */ + if (!supportsSchema) + tapSchema.setDBName(null); + + // retrieve only the standard TAP tables: + Iterator itTable = tapSchema.iterator(); + while(itTable.hasNext()){ + TAPTable table = itTable.next(); + int indStdTable = getCreationOrder(TAPMetadata.resolveStdTable(table.getADQLName())); + if (indStdTable > -1) + customStdTables[indStdTable] = table; + } } - } - @Override - public void dropTable(final TAPTable table) throws DBException{ - String sql = "DROP TABLE " + table.getDBSchemaName() + "." + table.getDBName() + ";"; - try{ - Statement stmt = connection.createStatement(); - stmt.executeUpdate(sql); - logger.tableDropped(this, table); - }catch(SQLException se){ - logger.dbError("Impossible to drop the table \"" + table.getFullName() + "\" !", se); - throw new DBException("Impossible to drop the table \"" + table.getFullName() + "\" !", se); + // 3. Build a common TAPSchema, if needed: + if (tapSchema == null){ + + // build a new TAP_SCHEMA definition based on the standard definition: + tapSchema = TAPMetadata.getStdSchema(supportsSchema); + + // add the new TAP_SCHEMA definition in the given metadata object: + metadata.addSchema(tapSchema); } - } - @Override - public void insertRow(final SavotTR row, final TAPTable table) throws DBException{ - StringBuffer sql = new StringBuffer("INSERT INTO "); - sql.append(table.getDBSchemaName()).append('.').append(table.getDBName()).append(" VALUES ("); + // 4. Finally, build the join between the standard tables and the custom ones: + TAPTable[] stdTables = new TAPTable[]{TAPMetadata.getStdTable(STDTable.SCHEMAS),TAPMetadata.getStdTable(STDTable.TABLES),TAPMetadata.getStdTable(STDTable.COLUMNS),TAPMetadata.getStdTable(STDTable.KEYS),TAPMetadata.getStdTable(STDTable.KEY_COLUMNS)}; + for(int i = 0; i < stdTables.length; i++){ - TDSet cells = row.getTDs(); - Iterator it = table.getColumns(); - String datatype, value; - TAPColumn col; - int i = 0; - while(it.hasNext()){ - col = it.next(); - if (i > 0) - sql.append(','); - datatype = col.getDatatype(); - value = cells.getContent(i); - if (value == null || value.isEmpty()) - sql.append("NULL"); - else if (datatype.equalsIgnoreCase("char") || datatype.equalsIgnoreCase("varchar") || datatype.equalsIgnoreCase("unsignedByte")) - sql.append('\'').append(value.replaceAll("'", "''").replaceAll("\0", "")).append('\''); - else{ - if (value.equalsIgnoreCase("nan")) - sql.append("'NaN'"); - else - sql.append(value.replaceAll("\0", "")); + // CASE: no custom definition: + if (customStdTables[i] == null){ + if (!supportsSchema) + stdTables[i].setDBName(STDSchema.TAPSCHEMA.label + "_" + stdTables[i].getADQLName()); + // add the table to the fetched or built-in schema: + tapSchema.addTable(stdTables[i]); } - i++; + // CASE: custom definition + else + stdTables[i] = customStdTables[i]; } - sql.append(");"); - try{ - Statement stmt = connection.createStatement(); - int nbInsertedRows = stmt.executeUpdate(sql.toString()); - logger.rowsInserted(this, table, nbInsertedRows); - }catch(SQLException se){ - logger.dbError("Impossible to insert a row into the table \"" + table.getFullName() + "\" !", se); - throw new DBException("Impossible to insert a row in the table \"" + table.getFullName() + "\" !", se); - } + return stdTables; + } + + /** + *

    Ensure the TAP_SCHEMA schema exists in the database AND it must especially drop all of its standard tables + * (schemas, tables, columns, keys and key_columns), if they exist.

    + * + *

    Important note: + * If TAP_SCHEMA already exists and contains other tables than the standard ones, they will not be dropped and they will stay in place. + *

    + * + * @param stmt The statement to use in order to interact with the database. + * @param stdTables List of all standard tables that must be (re-)created. + * They will be used just to know the name of the standard tables that should be dropped here. + * + * @throws SQLException If any error occurs while querying or updating the database. + */ + protected void resetTAPSchema(final Statement stmt, final TAPTable[] stdTables) throws SQLException{ + DatabaseMetaData dbMeta = connection.getMetaData(); + + // 1. Get the qualified DB schema name: + String dbSchemaName = (supportsSchema ? stdTables[0].getDBSchemaName() : null); + + /* 2. Test whether the schema TAP_SCHEMA exists + * and if it does not, create it: */ + if (dbSchemaName != null){ + // test whether the schema TAP_SCHEMA exists: + boolean hasTAPSchema = isSchemaExisting(dbSchemaName, dbMeta); + + // create TAP_SCHEMA if it does not exist: + if (!hasTAPSchema) + stmt.executeUpdate("CREATE SCHEMA " + translator.getQualifiedSchemaName(stdTables[0]) + ";"); + } + + // 2-bis. Drop all its standard tables: + dropTAPSchemaTables(stdTables, stmt, dbMeta); + } + + /** + *

    Remove/Drop all standard TAP_SCHEMA tables given in parameter.

    + * + *

    Note: + * To test the existence of tables to drop, {@link DatabaseMetaData#getTables(String, String, String, String[])} is called. + * Then the schema and table names are compared with the case sensitivity defined by the translator. + * Only tables matching with these comparisons will be dropped. + *

    + * + * @param stdTables Tables to drop. (they should be provided ordered by their creation order (see {@link #getCreationOrder(STDTable)})). + * @param stmt Statement to use in order to interact with the database. + * @param dbMeta Database metadata. Used to list all existing tables. + * + * @throws SQLException If any error occurs while querying or updating the database. + * + * @see JDBCTranslator#isCaseSensitive(IdentifierField) + */ + private void dropTAPSchemaTables(final TAPTable[] stdTables, final Statement stmt, final DatabaseMetaData dbMeta) throws SQLException{ + String[] stdTablesToDrop = new String[]{null,null,null,null,null}; + + ResultSet rs = null; + try{ + // Retrieve only the schema name and determine whether the search should be case sensitive: + String tapSchemaName = stdTables[0].getDBSchemaName(); + boolean schemaCaseSensitive = translator.isCaseSensitive(IdentifierField.SCHEMA); + boolean tableCaseSensitive = translator.isCaseSensitive(IdentifierField.TABLE); + + // Identify which standard TAP tables must be dropped: + rs = dbMeta.getTables(null, null, null, null); + while(rs.next()){ + String rsSchema = nullifyIfNeeded(rs.getString(2)), rsTable = rs.getString(3); + if (!supportsSchema || (tapSchemaName == null && rsSchema == null) || equals(rsSchema, tapSchemaName, schemaCaseSensitive)){ + int indStdTable; + indStdTable = getCreationOrder(isStdTable(rsTable, stdTables, tableCaseSensitive)); + if (indStdTable > -1){ + stdTablesToDrop[indStdTable] = (rsSchema != null ? "\"" + rsSchema + "\"." : "") + "\"" + rsTable + "\""; + } + } + } + }finally{ + close(rs); + } + + // Drop the existing tables (in the reverse order of creation): + for(int i = stdTablesToDrop.length - 1; i >= 0; i--){ + if (stdTablesToDrop[i] != null) + stmt.executeUpdate("DROP TABLE " + stdTablesToDrop[i] + ";"); + } + } + + /** + *

    Create the specified standard TAP_SCHEMA tables into the database.

    + * + *

    Important note: + * Only standard TAP_SCHEMA tables (schemas, tables, columns, keys and key_columns) can be created here. + * If the given table is not part of the schema TAP_SCHEMA (comparison done on the ADQL name case-sensitively) + * and is not a standard TAP_SCHEMA table (comparison done on the ADQL name case-sensitively), + * this function will do nothing and will throw an exception. + *

    + * + *

    Note: + * An extra column is added in TAP_SCHEMA.schemas, TAP_SCHEMA.tables and TAP_SCHEMA.columns: {@value #DB_NAME_COLUMN}. + * This column is particularly used when getting the TAP metadata from the database to alias some schema, table and/or column names in ADQL. + *

    + * + * @param table Table to create. + * @param stmt Statement to use in order to interact with the database. + * + * @throws DBException If the given table is not a standard TAP_SCHEMA table. + * @throws SQLException If any error occurs while querying or updating the database. + */ + protected void createTAPSchemaTable(final TAPTable table, final Statement stmt) throws DBException, SQLException{ + // 1. ENSURE THE GIVEN TABLE IS REALLY A TAP_SCHEMA TABLE (according to the ADQL names): + if (!table.getADQLSchemaName().equalsIgnoreCase(STDSchema.TAPSCHEMA.label) || TAPMetadata.resolveStdTable(table.getADQLName()) == null) + throw new DBException("Forbidden table creation: " + table + " is not a standard table of TAP_SCHEMA!"); + + // 2. BUILD THE SQL QUERY TO CREATE THE TABLE: + StringBuffer sql = new StringBuffer("CREATE TABLE "); + + // a. Write the fully qualified table name: + sql.append(translator.getTableName(table, supportsSchema)); + + // b. List all the columns: + sql.append('('); + Iterator it = table.getColumns(); + while(it.hasNext()){ + TAPColumn col = it.next(); + + // column name: + sql.append(translator.getColumnName(col)); + + // column type: + sql.append(' ').append(convertTypeToDB(col.getDatatype())); + + // last column ? + if (it.hasNext()) + sql.append(','); + } + + // b bis. Add the extra dbName column (giving the database name of a schema, table or column): + if ((supportsSchema && table.getADQLName().equalsIgnoreCase(STDTable.SCHEMAS.label)) || table.getADQLName().equalsIgnoreCase(STDTable.TABLES.label) || table.getADQLName().equalsIgnoreCase(STDTable.COLUMNS.label)) + sql.append(',').append(DB_NAME_COLUMN).append(" VARCHAR"); + + // c. Append the primary key definition, if needed: + String primaryKey = getPrimaryKeyDef(table.getADQLName()); + if (primaryKey != null) + sql.append(',').append(primaryKey); + + // d. End the query: + sql.append(')').append(';'); + + // 3. FINALLY CREATE THE TABLE: + stmt.executeUpdate(sql.toString()); + } + + /** + *

    Get the primary key corresponding to the specified table.

    + * + *

    If the specified table is not a standard TAP_SCHEMA table, NULL will be returned.

    + * + * @param tableName ADQL table name. + * + * @return The primary key definition (prefixed by a space) corresponding to the specified table (ex: " PRIMARY KEY(schema_name)"), + * or NULL if the specified table is not a standard TAP_SCHEMA table. + */ + private String getPrimaryKeyDef(final String tableName){ + STDTable stdTable = TAPMetadata.resolveStdTable(tableName); + if (stdTable == null) + return null; + + boolean caseSensitive = translator.isCaseSensitive(IdentifierField.COLUMN); + switch(stdTable){ + case SCHEMAS: + return " PRIMARY KEY(" + (caseSensitive ? "\"schema_name\"" : "schema_name") + ")"; + case TABLES: + return " PRIMARY KEY(" + (caseSensitive ? "\"table_name\"" : "table_name") + ")"; + case COLUMNS: + return " PRIMARY KEY(" + (caseSensitive ? "\"table_name\"" : "table_name") + ", " + (caseSensitive ? "\"column_name\"" : "column_name") + ")"; + case KEYS: + case KEY_COLUMNS: + return " PRIMARY KEY(" + (caseSensitive ? "\"key_id\"" : "key_id") + ")"; + default: + return null; + } + } + + /** + *

    Create the DB indexes corresponding to the given TAP_SCHEMA table.

    + * + *

    Important note: + * Only standard TAP_SCHEMA tables (schemas, tables, columns, keys and key_columns) can be created here. + * If the given table is not part of the schema TAP_SCHEMA (comparison done on the ADQL name case-sensitively) + * and is not a standard TAP_SCHEMA table (comparison done on the ADQL name case-sensitively), + * this function will do nothing and will throw an exception. + *

    + * + * @param table Table whose indexes must be created here. + * @param stmt Statement to use in order to interact with the database. + * + * @throws DBException If the given table is not a standard TAP_SCHEMA table. + * @throws SQLException If any error occurs while querying or updating the database. + */ + protected void createTAPTableIndexes(final TAPTable table, final Statement stmt) throws DBException, SQLException{ + // 1. Ensure the given table is really a TAP_SCHEMA table (according to the ADQL names): + if (!table.getADQLSchemaName().equalsIgnoreCase(STDSchema.TAPSCHEMA.label) || TAPMetadata.resolveStdTable(table.getADQLName()) == null) + throw new DBException("Forbidden index creation: " + table + " is not a standard table of TAP_SCHEMA!"); + + // Build the fully qualified DB name of the table: + final String dbTableName = translator.getTableName(table, supportsSchema); + + // Build the name prefix of all the indexes to create: + final String indexNamePrefix = "INDEX_" + ((table.getADQLSchemaName() != null) ? (table.getADQLSchemaName() + "_") : "") + table.getADQLName() + "_"; + + Iterator it = table.getColumns(); + while(it.hasNext()){ + TAPColumn col = it.next(); + // Create an index only for columns that have the 'indexed' flag: + if (col.isIndexed() && !isPartOfPrimaryKey(col.getADQLName())) + stmt.executeUpdate("CREATE INDEX " + indexNamePrefix + col.getADQLName() + " ON " + dbTableName + "(" + translator.getColumnName(col) + ");"); + } + } + + /** + * Tell whether the specified column is part of the primary key of its table. + * + * @param adqlName ADQL name of a column. + * + * @return true if the specified column is part of the primary key, + * false otherwise. + */ + private boolean isPartOfPrimaryKey(final String adqlName){ + if (adqlName == null) + return false; + else + return (adqlName.equalsIgnoreCase("schema_name") || adqlName.equalsIgnoreCase("table_name") || adqlName.equalsIgnoreCase("column_name") || adqlName.equalsIgnoreCase("key_id")); + } + + /** + *

    Fill all the standard tables of TAP_SCHEMA (schemas, tables, columns, keys and key_columns).

    + * + *

    This function just call the following functions:

    + *
      + *
    1. {@link #fillSchemas(TAPTable, Iterator)}
    2. + *
    3. {@link #fillTables(TAPTable, Iterator)}
    4. + *
    5. {@link #fillColumns(TAPTable, Iterator)}
    6. + *
    7. {@link #fillKeys(TAPTable, TAPTable, Iterator)}
    8. + *
    + * + * @param meta All schemas and tables to list inside the TAP_SCHEMA tables. + * + * @throws DBException If rows can not be inserted because the SQL update query has failed. + * @throws SQLException If any other SQL exception occurs. + */ + protected void fillTAPSchema(final TAPMetadata meta) throws SQLException, DBException{ + TAPTable metaTable; + + // 1. Fill SCHEMAS: + metaTable = meta.getTable(STDSchema.TAPSCHEMA.label, STDTable.SCHEMAS.label); + Iterator allTables = fillSchemas(metaTable, meta.iterator()); + + // 2. Fill TABLES: + metaTable = meta.getTable(STDSchema.TAPSCHEMA.label, STDTable.TABLES.label); + Iterator allColumns = fillTables(metaTable, allTables); + allTables = null; + + // Fill COLUMNS: + metaTable = meta.getTable(STDSchema.TAPSCHEMA.label, STDTable.COLUMNS.label); + Iterator allKeys = fillColumns(metaTable, allColumns); + allColumns = null; + + // Fill KEYS and KEY_COLUMNS: + metaTable = meta.getTable(STDSchema.TAPSCHEMA.label, STDTable.KEYS.label); + TAPTable metaTable2 = meta.getTable(STDSchema.TAPSCHEMA.label, STDTable.KEY_COLUMNS.label); + fillKeys(metaTable, metaTable2, allKeys); + } + + /** + *

    Fill the standard table TAP_SCHEMA.schemas with the list of all published schemas.

    + * + *

    Note: + * Batch updates may be done here if its supported by the DBMS connection. + * In case of any failure while using this feature, it will be flagged as unsupported and one-by-one updates will be processed. + *

    + * + * @param metaTable Description of TAP_SCHEMA.schemas. + * @param itSchemas Iterator over the list of schemas. + * + * @return Iterator over the full list of all tables (whatever is their schema). + * + * @throws DBException If rows can not be inserted because the SQL update query has failed. + * @throws SQLException If any other SQL exception occurs. + */ + private Iterator fillSchemas(final TAPTable metaTable, final Iterator itSchemas) throws SQLException, DBException{ + List allTables = new ArrayList(); + + // Build the SQL update query: + StringBuffer sql = new StringBuffer("INSERT INTO "); + sql.append(translator.getTableName(metaTable, supportsSchema)).append(" ("); + sql.append(translator.getColumnName(metaTable.getColumn("schema_name"))); + sql.append(", ").append(translator.getColumnName(metaTable.getColumn("description"))); + sql.append(", ").append(translator.getColumnName(metaTable.getColumn("utype"))); + if (supportsSchema){ + sql.append(", ").append(DB_NAME_COLUMN); + sql.append(") VALUES (?, ?, ?, ?);"); + }else + sql.append(") VALUES (?, ?, ?);"); + + // Prepare the statement: + PreparedStatement stmt = null; + try{ + stmt = connection.prepareStatement(sql.toString()); + + // Execute the query for each schema: + int nbRows = 0; + while(itSchemas.hasNext()){ + TAPSchema schema = itSchemas.next(); + nbRows++; + + // list all tables of this schema: + appendAllInto(allTables, schema.iterator()); + + // add the schema entry into the DB: + stmt.setString(1, schema.getADQLName()); + stmt.setString(2, schema.getDescription()); + stmt.setString(3, schema.getUtype()); + if (supportsSchema) + stmt.setString(4, (schema.getDBName() == null || schema.getDBName().equals(schema.getADQLName())) ? null : schema.getDBName()); + executeUpdate(stmt, nbRows); + } + executeBatchUpdates(stmt, nbRows); + }finally{ + close(stmt); + } + + return allTables.iterator(); + } + + /** + *

    Fill the standard table TAP_SCHEMA.tables with the list of all published tables.

    + * + *

    Note: + * Batch updates may be done here if its supported by the DBMS connection. + * In case of any failure while using this feature, it will be flagged as unsupported and one-by-one updates will be processed. + *

    + * + * @param metaTable Description of TAP_SCHEMA.tables. + * @param itTables Iterator over the list of tables. + * + * @return Iterator over the full list of all columns (whatever is their table). + * + * @throws DBException If rows can not be inserted because the SQL update query has failed. + * @throws SQLException If any other SQL exception occurs. + */ + private Iterator fillTables(final TAPTable metaTable, final Iterator itTables) throws SQLException, DBException{ + List allColumns = new ArrayList(); + + // Build the SQL update query: + StringBuffer sql = new StringBuffer("INSERT INTO "); + sql.append(translator.getTableName(metaTable, supportsSchema)).append(" ("); + sql.append(translator.getColumnName(metaTable.getColumn("schema_name"))); + sql.append(", ").append(translator.getColumnName(metaTable.getColumn("table_name"))); + sql.append(", ").append(translator.getColumnName(metaTable.getColumn("table_type"))); + sql.append(", ").append(translator.getColumnName(metaTable.getColumn("description"))); + sql.append(", ").append(translator.getColumnName(metaTable.getColumn("utype"))); + sql.append(", ").append(DB_NAME_COLUMN); + sql.append(") VALUES (?, ?, ?, ?, ?, ?);"); + + // Prepare the statement: + PreparedStatement stmt = null; + try{ + stmt = connection.prepareStatement(sql.toString()); + + // Execute the query for each table: + int nbRows = 0; + while(itTables.hasNext()){ + TAPTable table = itTables.next(); + nbRows++; + + // list all columns of this table: + appendAllInto(allColumns, table.getColumns()); + + // add the table entry into the DB: + stmt.setString(1, table.getADQLSchemaName()); + if (table.isInitiallyQualified()) + stmt.setString(2, table.getADQLSchemaName() + "." + table.getADQLName()); + else + stmt.setString(2, table.getADQLName()); + stmt.setString(3, table.getType().toString()); + stmt.setString(4, table.getDescription()); + stmt.setString(5, table.getUtype()); + stmt.setString(6, (table.getDBName() == null || table.getDBName().equals(table.getADQLName())) ? null : table.getDBName()); + executeUpdate(stmt, nbRows); + } + executeBatchUpdates(stmt, nbRows); + }finally{ + close(stmt); + } + + return allColumns.iterator(); + } + + /** + *

    Fill the standard table TAP_SCHEMA.columns with the list of all published columns.

    + * + *

    Note: + * Batch updates may be done here if its supported by the DBMS connection. + * In case of any failure while using this feature, it will be flagged as unsupported and one-by-one updates will be processed. + *

    + * + * @param metaTable Description of TAP_SCHEMA.columns. + * @param itColumns Iterator over the list of columns. + * + * @return Iterator over the full list of all foreign keys. + * + * @throws DBException If rows can not be inserted because the SQL update query has failed. + * @throws SQLException If any other SQL exception occurs. + */ + private Iterator fillColumns(final TAPTable metaTable, final Iterator itColumns) throws SQLException, DBException{ + List allKeys = new ArrayList(); + + // Build the SQL update query: + StringBuffer sql = new StringBuffer("INSERT INTO "); + sql.append(translator.getTableName(metaTable, supportsSchema)).append(" ("); + sql.append(translator.getColumnName(metaTable.getColumn("table_name"))); + sql.append(", ").append(translator.getColumnName(metaTable.getColumn("column_name"))); + sql.append(", ").append(translator.getColumnName(metaTable.getColumn("description"))); + sql.append(", ").append(translator.getColumnName(metaTable.getColumn("unit"))); + sql.append(", ").append(translator.getColumnName(metaTable.getColumn("ucd"))); + sql.append(", ").append(translator.getColumnName(metaTable.getColumn("utype"))); + sql.append(", ").append(translator.getColumnName(metaTable.getColumn("datatype"))); + sql.append(", ").append(translator.getColumnName(metaTable.getColumn("size"))); + sql.append(", ").append(translator.getColumnName(metaTable.getColumn("principal"))); + sql.append(", ").append(translator.getColumnName(metaTable.getColumn("indexed"))); + sql.append(", ").append(translator.getColumnName(metaTable.getColumn("std"))); + sql.append(", ").append(DB_NAME_COLUMN); + sql.append(") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"); + + // Prepare the statement: + PreparedStatement stmt = null; + try{ + stmt = connection.prepareStatement(sql.toString()); + + // Execute the query for each column: + int nbRows = 0; + while(itColumns.hasNext()){ + TAPColumn col = itColumns.next(); + nbRows++; + + // list all foreign keys of this column: + appendAllInto(allKeys, col.getTargets()); + + // add the column entry into the DB: + if (!(col.getTable() instanceof TAPTable) || ((TAPTable)col.getTable()).isInitiallyQualified()) + stmt.setString(1, col.getTable().getADQLSchemaName() + "." + col.getTable().getADQLName()); + else + stmt.setString(1, col.getTable().getADQLName()); + stmt.setString(2, col.getADQLName()); + stmt.setString(3, col.getDescription()); + stmt.setString(4, col.getUnit()); + stmt.setString(5, col.getUcd()); + stmt.setString(6, col.getUtype()); + stmt.setString(7, col.getDatatype().type.toString()); + stmt.setInt(8, col.getDatatype().length); + stmt.setInt(9, col.isPrincipal() ? 1 : 0); + stmt.setInt(10, col.isIndexed() ? 1 : 0); + stmt.setInt(11, col.isStd() ? 1 : 0); + stmt.setString(12, (col.getDBName() == null || col.getDBName().equals(col.getADQLName())) ? null : col.getDBName()); + executeUpdate(stmt, nbRows); + } + executeBatchUpdates(stmt, nbRows); + }finally{ + close(stmt); + } + + return allKeys.iterator(); + } + + /** + *

    Fill the standard tables TAP_SCHEMA.keys and TAP_SCHEMA.key_columns with the list of all published foreign keys.

    + * + *

    Note: + * Batch updates may be done here if its supported by the DBMS connection. + * In case of any failure while using this feature, it will be flagged as unsupported and one-by-one updates will be processed. + *

    + * + * @param metaKeys Description of TAP_SCHEMA.keys. + * @param metaKeyColumns Description of TAP_SCHEMA.key_columns. + * @param itKeys Iterator over the list of foreign keys. + * + * @throws DBException If rows can not be inserted because the SQL update query has failed. + * @throws SQLException If any other SQL exception occurs. + */ + private void fillKeys(final TAPTable metaKeys, final TAPTable metaKeyColumns, final Iterator itKeys) throws SQLException, DBException{ + // Build the SQL update query for KEYS: + StringBuffer sqlKeys = new StringBuffer("INSERT INTO "); + sqlKeys.append(translator.getTableName(metaKeys, supportsSchema)).append(" ("); + sqlKeys.append(translator.getColumnName(metaKeys.getColumn("key_id"))); + sqlKeys.append(", ").append(translator.getColumnName(metaKeys.getColumn("from_table"))); + sqlKeys.append(", ").append(translator.getColumnName(metaKeys.getColumn("target_table"))); + sqlKeys.append(", ").append(translator.getColumnName(metaKeys.getColumn("description"))); + sqlKeys.append(", ").append(translator.getColumnName(metaKeys.getColumn("utype"))); + sqlKeys.append(") VALUES (?, ?, ?, ?, ?);"); + + PreparedStatement stmtKeys = null, stmtKeyCols = null; + try{ + // Prepare the statement for KEYS: + stmtKeys = connection.prepareStatement(sqlKeys.toString()); + + // Build the SQL update query for KEY_COLUMNS: + StringBuffer sqlKeyCols = new StringBuffer("INSERT INTO "); + sqlKeyCols.append(translator.getTableName(metaKeyColumns, supportsSchema)).append(" ("); + sqlKeyCols.append(translator.getColumnName(metaKeyColumns.getColumn("key_id"))); + sqlKeyCols.append(", ").append(translator.getColumnName(metaKeyColumns.getColumn("from_column"))); + sqlKeyCols.append(", ").append(translator.getColumnName(metaKeyColumns.getColumn("target_column"))); + sqlKeyCols.append(") VALUES (?, ?, ?);"); + + // Prepare the statement for KEY_COLUMNS: + stmtKeyCols = connection.prepareStatement(sqlKeyCols.toString()); + + // Execute the query for each column: + int nbKeys = 0, nbKeyColumns = 0; + while(itKeys.hasNext()){ + TAPForeignKey key = itKeys.next(); + nbKeys++; + + // add the key entry into KEYS: + stmtKeys.setString(1, key.getKeyId()); + if (key.getFromTable().isInitiallyQualified()) + stmtKeys.setString(2, key.getFromTable().getADQLSchemaName() + "." + key.getFromTable().getADQLName()); + else + stmtKeys.setString(2, key.getFromTable().getADQLName()); + if (key.getTargetTable().isInitiallyQualified()) + stmtKeys.setString(3, key.getTargetTable().getADQLSchemaName() + "." + key.getTargetTable().getADQLName()); + else + stmtKeys.setString(3, key.getTargetTable().getADQLName()); + stmtKeys.setString(4, key.getDescription()); + stmtKeys.setString(5, key.getUtype()); + executeUpdate(stmtKeys, nbKeys); + + // add the key columns into KEY_COLUMNS: + Iterator> itAssoc = key.iterator(); + while(itAssoc.hasNext()){ + nbKeyColumns++; + Map.Entry assoc = itAssoc.next(); + stmtKeyCols.setString(1, key.getKeyId()); + stmtKeyCols.setString(2, assoc.getKey()); + stmtKeyCols.setString(3, assoc.getValue()); + executeUpdate(stmtKeyCols, nbKeyColumns); + } + } + + executeBatchUpdates(stmtKeys, nbKeys); + executeBatchUpdates(stmtKeyCols, nbKeyColumns); + }finally{ + close(stmtKeys); + close(stmtKeyCols); + } + } + + /* ***************** */ + /* UPLOAD MANAGEMENT */ + /* ***************** */ + + /** + *

    Important note: + * Only tables uploaded by users can be created in the database. To ensure that, the schema name of this table MUST be {@link STDSchema#UPLOADSCHEMA} ("TAP_UPLOAD") in ADQL. + * If it has another ADQL name, an exception will be thrown. Of course, the DB name of this schema MAY be different. + *

    + * + *

    Important note: + * This function may modify the given {@link TAPTable} object if schemas are not supported by this connection. + * In this case, this function will prefix the table's DB name by the schema's DB name directly inside the given + * {@link TAPTable} object. Then the DB name of the schema will be set to NULL. + *

    + * + *

    Note: + * If the upload schema does not already exist in the database, it will be created. + *

    + * + * @see tap.db.DBConnection#addUploadedTable(tap.metadata.TAPTable, tap.data.TableIterator) + * @see #checkUploadedTableDef(TAPTable) + */ + @Override + public boolean addUploadedTable(TAPTable tableDef, TableIterator data) throws DBException, DataReadException{ + // If no table to upload, consider it has been dropped and return TRUE: + if (tableDef == null) + return true; + + // Check the table is well defined (and particularly the schema is well set with an ADQL name = TAP_UPLOAD): + checkUploadedTableDef(tableDef); + + Statement stmt = null; + try{ + + // Start a transaction: + startTransaction(); + // ...create a statement: + stmt = connection.createStatement(); + + DatabaseMetaData dbMeta = connection.getMetaData(); + + // 1. Create the upload schema, if it does not already exist: + if (!isSchemaExisting(tableDef.getDBSchemaName(), dbMeta)){ + stmt.executeUpdate("CREATE SCHEMA " + translator.getQualifiedSchemaName(tableDef) + ";"); + if (logger != null) + logger.logDB(LogLevel.INFO, this, "SCHEMA_CREATED", "Schema \"" + tableDef.getADQLSchemaName() + "\" (in DB: " + translator.getQualifiedSchemaName(tableDef) + ") created.", null); + } + // 1bis. Ensure the table does not already exist and if it is the case, throw an understandable exception: + else if (isTableExisting(tableDef.getDBSchemaName(), tableDef.getDBName(), dbMeta)){ + DBException de = new DBException("Impossible to create the user uploaded table in the database: " + translator.getTableName(tableDef, supportsSchema) + "! This table already exists."); + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "ADD_UPLOAD_TABLE", de.getMessage(), de); + throw de; + } + + // 2. Create the table: + // ...build the SQL query: + StringBuffer sqlBuf = new StringBuffer("CREATE TABLE "); + sqlBuf.append(translator.getTableName(tableDef, supportsSchema)).append(" ("); + Iterator it = tableDef.getColumns(); + while(it.hasNext()){ + TAPColumn col = it.next(); + // column name: + sqlBuf.append(translator.getColumnName(col)); + // column type: + sqlBuf.append(' ').append(convertTypeToDB(col.getDatatype())); + // last column ? + if (it.hasNext()) + sqlBuf.append(','); + } + sqlBuf.append(");"); + // ...execute the update query: + stmt.executeUpdate(sqlBuf.toString()); + + // 3. Fill the table: + fillUploadedTable(tableDef, data); + + // Commit the transaction: + commit(); + + // Log the end: + if (logger != null) + logger.logDB(LogLevel.INFO, this, "TABLE_CREATED", "Table \"" + tableDef.getADQLName() + "\" (in DB: " + translator.getTableName(tableDef, supportsSchema) + ") created.", null); + + return true; + + }catch(SQLException se){ + rollback(); + if (logger != null) + logger.logDB(LogLevel.WARNING, this, "ADD_UPLOAD_TABLE", "Impossible to create the uploaded table: " + translator.getTableName(tableDef, supportsSchema) + "!", se); + throw new DBException("Impossible to create the uploaded table: " + translator.getTableName(tableDef, supportsSchema) + "!", se); + }catch(DBException de){ + rollback(); + throw de; + }catch(DataReadException dre){ + rollback(); + throw dre; + }finally{ + close(stmt); + endTransaction(); + } + } + + /** + *

    Fill the table uploaded by the user with the given data.

    + * + *

    Note: + * Batch updates may be done here if its supported by the DBMS connection. + * In case of any failure while using this feature, it will be flagged as unsupported and one-by-one updates will be processed. + *

    + * + *

    Note: + * This function proceeds to a formatting of TIMESTAMP and GEOMETRY (point, circle, box, polygon) values. + *

    + * + * @param metaTable Description of the updated table. + * @param data Iterator over the rows to insert. + * + * @return Number of inserted rows. + * + * @throws DBException If rows can not be inserted because the SQL update query has failed. + * @throws SQLException If any other SQL exception occurs. + * @throws DataReadException If there is any error while reading the data from the given {@link TableIterator} (and particularly if a limit - in byte or row - has been reached). + */ + protected int fillUploadedTable(final TAPTable metaTable, final TableIterator data) throws SQLException, DBException, DataReadException{ + // 1. Build the SQL update query: + StringBuffer sql = new StringBuffer("INSERT INTO "); + StringBuffer varParam = new StringBuffer(); + // ...table name: + sql.append(translator.getTableName(metaTable, supportsSchema)).append(" ("); + // ...list of columns: + TAPColumn[] cols = data.getMetadata(); + for(int c = 0; c < cols.length; c++){ + if (c > 0){ + sql.append(", "); + varParam.append(", "); + } + sql.append(translator.getColumnName(cols[c])); + varParam.append('?'); + } + // ...values pattern: + sql.append(") VALUES (").append(varParam).append(");"); + + // 2. Prepare the statement: + PreparedStatement stmt = null; + int nbRows = 0; + try{ + stmt = connection.prepareStatement(sql.toString()); + + // 3. Execute the query for each given row: + while(data.nextRow()){ + nbRows++; + int c = 1; + while(data.hasNextCol()){ + Object val = data.nextCol(); + if (val != null && cols[c - 1] != null){ + /* TIMESTAMP FORMATTING */ + if (cols[c - 1].getDatatype().type == DBDatatype.TIMESTAMP){ + try{ + val = new Timestamp(ISO8601Format.parse(val.toString())); + }catch(ParseException pe){ + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "UPLOAD", "[l. " + nbRows + ", c. " + c + "] Unexpected date format for the value: \"" + val + "\"! A date formatted in ISO8601 was expected.", pe); + throw new DBException("[l. " + nbRows + ", c. " + c + "] Unexpected date format for the value: \"" + val + "\"! A date formatted in ISO8601 was expected.", pe); + } + } + /* GEOMETRY FORMATTING */ + else if (cols[c - 1].getDatatype().type == DBDatatype.POINT || cols[c - 1].getDatatype().type == DBDatatype.REGION){ + Region region; + // parse the region as an STC-S expression: + try{ + region = STCS.parseRegion(val.toString()); + }catch(adql.parser.ParseException e){ + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "UPLOAD", "[l. " + nbRows + ", c. " + c + "] Incorrect STC-S syntax for the geometrical value \"" + val + "\"! " + e.getMessage(), e); + throw new DataReadException("[l. " + nbRows + ", c. " + c + "] Incorrect STC-S syntax for the geometrical value \"" + val + "\"! " + e.getMessage(), e); + } + // translate this STC region into the corresponding column value: + try{ + val = translator.translateGeometryToDB(region); + }catch(adql.parser.ParseException e){ + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "UPLOAD", "[l. " + nbRows + ", c. " + c + "] Impossible to import the ADQL geometry \"" + val + "\" into the database! " + e.getMessage(), e); + throw new DataReadException("[l. " + nbRows + ", c. " + c + "] Impossible to import the ADQL geometry \"" + val + "\" into the database! " + e.getMessage(), e); + } + } + /* BOOLEAN CASE (more generally, type incompatibility) */ + else if (val != null && cols[c - 1].getDatatype().type == DBDatatype.SMALLINT && val instanceof Boolean) + val = ((Boolean)val) ? (short)1 : (short)0; + } + stmt.setObject(c++, val); + } + executeUpdate(stmt, nbRows); + } + executeBatchUpdates(stmt, nbRows); + + return nbRows; + + }finally{ + close(stmt); + } + } + + /** + *

    Important note: + * Only tables uploaded by users can be dropped from the database. To ensure that, the schema name of this table MUST be {@link STDSchema#UPLOADSCHEMA} ("TAP_UPLOAD") in ADQL. + * If it has another ADQL name, an exception will be thrown. Of course, the DB name of this schema MAY be different. + *

    + * + *

    Important note: + * This function may modify the given {@link TAPTable} object if schemas are not supported by this connection. + * In this case, this function will prefix the table's DB name by the schema's DB name directly inside the given + * {@link TAPTable} object. Then the DB name of the schema will be set to NULL. + *

    + * + *

    Note: + * This implementation is able to drop only one uploaded table. So if this function finds more than one table matching to the given one, + * an exception will be thrown and no table will be dropped. + *

    + * + * @see tap.db.DBConnection#dropUploadedTable(tap.metadata.TAPTable) + * @see #checkUploadedTableDef(TAPTable) + */ + @Override + public boolean dropUploadedTable(final TAPTable tableDef) throws DBException{ + // If no table to upload, consider it has been dropped and return TRUE: + if (tableDef == null) + return true; + + // Check the table is well defined (and particularly the schema is well set with an ADQL name = TAP_UPLOAD): + checkUploadedTableDef(tableDef); + + Statement stmt = null; + try{ + + // Check the existence of the table to drop: + if (!isTableExisting(tableDef.getDBSchemaName(), tableDef.getDBName(), connection.getMetaData())) + return true; + + // Execute the update: + stmt = connection.createStatement(); + int cnt = stmt.executeUpdate("DROP TABLE " + translator.getTableName(tableDef, supportsSchema) + ";"); + + // Log the end: + if (logger != null){ + if (cnt >= 0) + logger.logDB(LogLevel.INFO, this, "TABLE_DROPPED", "Table \"" + tableDef.getADQLName() + "\" (in DB: " + translator.getTableName(tableDef, supportsSchema) + ") dropped.", null); + else + logger.logDB(LogLevel.ERROR, this, "TABLE_DROPPED", "Table \"" + tableDef.getADQLName() + "\" (in DB: " + translator.getTableName(tableDef, supportsSchema) + ") NOT dropped.", null); + } + + // Ensure the update is successful: + return (cnt >= 0); + + }catch(SQLException se){ + if (logger != null) + logger.logDB(LogLevel.WARNING, this, "DROP_UPLOAD_TABLE", "Impossible to drop the uploaded table: " + translator.getTableName(tableDef, supportsSchema) + "!", se); + throw new DBException("Impossible to drop the uploaded table: " + translator.getTableName(tableDef, supportsSchema) + "!", se); + }finally{ + close(stmt); + } + } + + /** + *

    Ensures that the given table MUST be inside the upload schema in ADQL.

    + * + *

    Thus, the following cases are taken into account:

    + *
      + *
    • + * The schema name of the given table MUST be {@link STDSchema#UPLOADSCHEMA} ("TAP_UPLOAD") in ADQL. + * If it has another ADQL name, an exception will be thrown. Of course, the DB name of this schema MAY be different. + *
    • + *
    • + * If schemas are not supported by this connection, this function will prefix the table DB name by the schema DB name directly + * inside the given {@link TAPTable} object. Then the DB name of the schema will be set to NULL. + *
    • + *
    + * + * @param tableDef Definition of the table to create/drop. + * + * @throws DBException If the given table is not in a schema + * or if the ADQL name of this schema is not {@link STDSchema#UPLOADSCHEMA} ("TAP_UPLOAD"). + */ + protected void checkUploadedTableDef(final TAPTable tableDef) throws DBException{ + // If the table has no defined schema or if the ADQL name of the schema is not TAP_UPLOAD, throw an exception: + if (tableDef.getSchema() == null || !tableDef.getSchema().getADQLName().equals(STDSchema.UPLOADSCHEMA.label)) + throw new DBException("Missing upload schema! An uploaded table must be inside a schema whose the ADQL name is strictly equals to \"" + STDSchema.UPLOADSCHEMA.label + "\" (but the DB name may be different)."); + + if (!supportsSchema){ + if (tableDef.getADQLSchemaName() != null && tableDef.getADQLSchemaName().trim().length() > 0 && !tableDef.getDBName().startsWith(tableDef.getADQLSchemaName() + "_")) + tableDef.setDBName(tableDef.getADQLSchemaName() + "_" + tableDef.getDBName()); + if (tableDef.getSchema() != null) + tableDef.getSchema().setDBName(null); + } + } + + /* ************** */ + /* TOOL FUNCTIONS */ + /* ************** */ + + /** + *

    Convert the given TAP type into the corresponding DBMS column type.

    + * + *

    + * This function tries first the type conversion using the translator ({@link JDBCTranslator#convertTypeToDB(DBType)}). + * If it fails, a default conversion is done considering all the known types of the following DBMS: + * PostgreSQL, SQLite, MySQL, Oracle and JavaDB/Derby. + *

    + * + * @param type TAP type to convert. + * + * @return The corresponding DBMS type. + * + * @see JDBCTranslator#convertTypeToDB(DBType) + * @see #defaultTypeConversion(DBType) + */ + protected String convertTypeToDB(final DBType type){ + String dbmsType = translator.convertTypeToDB(type); + return (dbmsType == null) ? defaultTypeConversion(type) : dbmsType; + } + + /** + *

    Get the DBMS compatible datatype corresponding to the given column {@link DBType}.

    + * + *

    Note 1: + * This function is able to generate a DB datatype compatible with the currently used DBMS. + * In this current implementation, only Postgresql, Oracle, SQLite, MySQL and Java DB/Derby have been considered. + * Most of the TAP types have been tested only with Postgresql and SQLite without any problem. + * If the DBMS you are using has not been considered, note that this function will return the TAP type expression by default. + *

    + * + *

    Note 2: + * In case the given datatype is NULL or not managed here, the DBMS type corresponding to "VARCHAR" will be returned. + *

    + * + *

    Note 3: + * The special TAP types POINT and REGION are converted into the DBMS type corresponding to "VARCHAR". + *

    + * + * @param datatype Column TAP type. + * + * @return The corresponding DB type, or NULL if the given type is not managed or is NULL. + */ + protected String defaultTypeConversion(DBType datatype){ + if (datatype == null) + datatype = new DBType(DBDatatype.VARCHAR); + + switch(datatype.type){ + + case SMALLINT: + return dbms.equals("sqlite") ? "INTEGER" : "SMALLINT"; + + case INTEGER: + case REAL: + return datatype.type.toString(); + + case BIGINT: + if (dbms.equals("oracle")) + return "NUMBER(19,0)"; + else if (dbms.equals("sqlite")) + return "INTEGER"; + else + return "BIGINT"; + + case DOUBLE: + if (dbms.equals("postgresql") || dbms.equals("oracle")) + return "DOUBLE PRECISION"; + else if (dbms.equals("sqlite")) + return "REAL"; + else + return "DOUBLE"; + + case BINARY: + if (dbms.equals("postgresql")) + return "bytea"; + else if (dbms.equals("sqlite")) + return "BLOB"; + else if (dbms.equals("oracle")) + return "RAW" + (datatype.length > 0 ? "(" + datatype.length + ")" : ""); + else if (dbms.equals("derby")) + return "CHAR" + (datatype.length > 0 ? "(" + datatype.length + ")" : "") + " FOR BIT DATA"; + else + return datatype.type.toString(); + + case VARBINARY: + if (dbms.equals("postgresql")) + return "bytea"; + else if (dbms.equals("sqlite")) + return "BLOB"; + else if (dbms.equals("oracle")) + return "LONG RAW" + (datatype.length > 0 ? "(" + datatype.length + ")" : ""); + else if (dbms.equals("derby")) + return "VARCHAR" + (datatype.length > 0 ? "(" + datatype.length + ")" : "") + " FOR BIT DATA"; + else + return datatype.type.toString(); + + case CHAR: + if (dbms.equals("sqlite")) + return "TEXT"; + else + return "CHAR"; + + case BLOB: + if (dbms.equals("postgresql")) + return "bytea"; + else + return "BLOB"; + + case CLOB: + if (dbms.equals("postgresql") || dbms.equals("mysql") || dbms.equals("sqlite")) + return "TEXT"; + else + return "CLOB"; + + case TIMESTAMP: + if (dbms.equals("sqlite")) + return "TEXT"; + else + return "TIMESTAMP"; + + case POINT: + case REGION: + case VARCHAR: + default: + if (dbms.equals("sqlite")) + return "TEXT"; + else + return "VARCHAR"; + } + } + + /** + *

    Start a transaction.

    + * + *

    + * Basically, if transactions are supported by this connection, the flag AutoCommit is just turned off. + * It will be turned on again when {@link #endTransaction()} is called. + *

    + * + *

    If transactions are not supported by this connection, nothing is done.

    + * + *

    Important note: + * If any error interrupts the START TRANSACTION operation, transactions will be afterwards considered as not supported by this connection. + * So, subsequent call to this function (and any other transaction related function) will never do anything. + *

    + * + * @throws DBException If it is impossible to start a transaction though transactions are supported by this connection. + * If these are not supported, this error can never be thrown. + */ + protected void startTransaction() throws DBException{ + try{ + if (supportsTransaction){ + connection.setAutoCommit(false); + if (logger != null) + logger.logDB(LogLevel.INFO, this, "START_TRANSACTION", "Transaction STARTED.", null); + } + }catch(SQLException se){ + supportsTransaction = false; + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "START_TRANSACTION", "Transaction STARTing impossible!", se); + throw new DBException("Transaction STARTing impossible!", se); + } + } + + /** + *

    Commit the current transaction.

    + * + *

    + * {@link #startTransaction()} must have been called before. If it's not the case the connection + * may throw a {@link SQLException} which will be transformed into a {@link DBException} here. + *

    + * + *

    If transactions are not supported by this connection, nothing is done.

    + * + *

    Important note: + * If any error interrupts the COMMIT operation, transactions will be afterwards considered as not supported by this connection. + * So, subsequent call to this function (and any other transaction related function) will never do anything. + *

    + * + * @throws DBException If it is impossible to commit a transaction though transactions are supported by this connection.. + * If these are not supported, this error can never be thrown. + */ + protected void commit() throws DBException{ + try{ + if (supportsTransaction){ + connection.commit(); + if (logger != null) + logger.logDB(LogLevel.INFO, this, "COMMIT", "Transaction COMMITED.", null); + } + }catch(SQLException se){ + supportsTransaction = false; + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "COMMIT", "Transaction COMMIT impossible!", se); + throw new DBException("Transaction COMMIT impossible!", se); + } + } + + /** + *

    Rollback the current transaction.

    + * + *

    + * {@link #startTransaction()} must have been called before. If it's not the case the connection + * may throw a {@link SQLException} which will be transformed into a {@link DBException} here. + *

    + * + *

    If transactions are not supported by this connection, nothing is done.

    + * + *

    Important note: + * If any error interrupts the ROLLBACK operation, transactions will considered afterwards as not supported by this connection. + * So, subsequent call to this function (and any other transaction related function) will never do anything. + *

    + * + * @throws DBException If it is impossible to rollback a transaction though transactions are supported by this connection.. + * If these are not supported, this error can never be thrown. + */ + protected void rollback(){ + try{ + if (supportsTransaction){ + connection.rollback(); + if (logger != null) + logger.logDB(LogLevel.INFO, this, "ROLLBACK", "Transaction ROLLBACKED.", null); + } + }catch(SQLException se){ + supportsTransaction = false; + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "ROLLBACK", "Transaction ROLLBACK impossible!", se); + } + } + + /** + *

    End the current transaction.

    + * + *

    + * Basically, if transactions are supported by this connection, the flag AutoCommit is just turned on. + *

    + * + *

    If transactions are not supported by this connection, nothing is done.

    + * + *

    Important note: + * If any error interrupts the END TRANSACTION operation, transactions will be afterwards considered as not supported by this connection. + * So, subsequent call to this function (and any other transaction related function) will never do anything. + *

    + * + * @throws DBException If it is impossible to end a transaction though transactions are supported by this connection. + * If these are not supported, this error can never be thrown. + */ + protected void endTransaction(){ + try{ + if (supportsTransaction){ + connection.setAutoCommit(true); + if (logger != null) + logger.logDB(LogLevel.INFO, this, "END_TRANSACTION", "Transaction ENDED.", null); + } + }catch(SQLException se){ + supportsTransaction = false; + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "END_TRANSACTION", "Transaction ENDing impossible!", se); + } + } + + /** + *

    Close silently the given {@link ResultSet}.

    + * + *

    If the given {@link ResultSet} is NULL, nothing (even exception/error) happens.

    + * + *

    + * If any {@link SQLException} occurs during this operation, it is caught and just logged + * (see {@link TAPLog#logDB(uws.service.log.UWSLog.LogLevel, DBConnection, String, String, Throwable)}). + * No error is thrown and nothing else is done. + *

    + * + * @param rs {@link ResultSet} to close. + */ + protected final void close(final ResultSet rs){ + try{ + if (rs != null) + rs.close(); + }catch(SQLException se){ + if (logger != null) + logger.logDB(LogLevel.WARNING, this, "CLOSE", "Can not close a ResultSet!", null); + } + } + + /** + *

    Close silently the given {@link Statement}.

    + * + *

    If the given {@link Statement} is NULL, nothing (even exception/error) happens.

    + * + *

    + * If any {@link SQLException} occurs during this operation, it is caught and just logged + * (see {@link TAPLog#logDB(uws.service.log.UWSLog.LogLevel, DBConnection, String, String, Throwable)}). + * No error is thrown and nothing else is done. + *

    + * + * @param stmt {@link Statement} to close. + */ + protected final void close(final Statement stmt){ + try{ + if (stmt != null) + stmt.close(); + }catch(SQLException se){ + if (logger != null) + logger.logDB(LogLevel.WARNING, this, "CLOSE", "Can not close a Statement!", null); + } + } + + /** + *

    Transform the given column value in a boolean value.

    + * + *

    The following cases are taken into account in function of the given value's type:

    + *
      + *
    • NULL: false is always returned.
    • + * + *
    • {@link Boolean}: the boolean value is returned as provided (but casted in boolean).
    • + * + *
    • {@link Integer}: true is returned only if the integer value is strictly greater than 0, otherwise false is returned.
    • + * + *
    • Other: toString().trim() is first called on this object. Then, an integer value is tried to be extracted from it. + * If it succeeds, the previous rule is applied. If it fails, true will be returned only if the string is "t" or "true" (case insensitively).
    • + *
    + * + * @param colValue The column value to transform in boolean. + * + * @return Its corresponding boolean value. + */ + protected final boolean toBoolean(final Object colValue){ + // NULL => false: + if (colValue == null) + return false; + + // Boolean value => cast in boolean and return this value: + else if (colValue instanceof Boolean) + return ((Boolean)colValue).booleanValue(); + + // Integer value => cast in integer and return true only if the value is positive and not null: + else if (colValue instanceof Integer){ + int intFlag = ((Integer)colValue).intValue(); + return (intFlag > 0); + } + // Otherwise => get the string representation and: + // 1/ try to cast it into an integer and apply the same test as before + // 2/ if the cast fails, return true only if the value is "t" or "true" (case insensitively): + else{ + String strFlag = colValue.toString().trim(); + try{ + int intFlag = Integer.parseInt(strFlag); + return (intFlag > 0); + }catch(NumberFormatException nfe){ + return strFlag.equalsIgnoreCase("t") || strFlag.equalsIgnoreCase("true"); + } + } + } + + /** + * Return NULL if the given column value is an empty string (or it just contains space characters) or NULL. + * Otherwise the given string is returned as provided. + * + * @param dbValue Value to nullify if needed. + * + * @return NULL if the given string is NULL or empty, otherwise the given value. + */ + protected final String nullifyIfNeeded(final String dbValue){ + return (dbValue != null && dbValue.trim().length() <= 0) ? null : dbValue; + } + + /** + * Search a {@link TAPTable} instance whose the ADQL name matches (case sensitively) to the given one. + * + * @param tableName ADQL name of the table to search. + * @param itTables Iterator over the set of tables in which the research must be done. + * + * @return The found table, or NULL if not found. + */ + private TAPTable searchTable(String tableName, final Iterator itTables){ + // Get the schema name, if any prefix the given table name: + String schemaName = null; + int indSep = tableName.indexOf('.'); + if (indSep > 0){ + schemaName = tableName.substring(0, indSep); + tableName = tableName.substring(indSep + 1); + } + + // Search by schema name (if any) and then by table name: + while(itTables.hasNext()){ + // get the table: + TAPTable table = itTables.next(); + // test the schema name (if one was prefixing the table name) (case sensitively): + if (schemaName != null){ + if (table.getADQLSchemaName() == null || !schemaName.equals(table.getADQLSchemaName())) + continue; + } + // test the table name (case sensitively): + if (tableName.equals(table.getADQLName())) + return table; + } + + // NULL if no table matches: + return null; + } + + /** + *

    Tell whether the specified schema exists in the database. + * To do so, it is using the given {@link DatabaseMetaData} object to query the database and list all existing schemas.

    + * + *

    Note: + * This function is completely useless if the connection is not supporting schemas. + *

    + * + *

    Note: + * Test on the schema name is done considering the case sensitivity indicated by the translator + * (see {@link JDBCTranslator#isCaseSensitive(IdentifierField)}). + *

    + * + *

    Note: + * This functions is used by {@link #addUploadedTable(TAPTable, TableIterator)} and {@link #resetTAPSchema(Statement, TAPTable[])}. + *

    + * + * @param schemaName DB name of the schema whose the existence must be checked. + * @param dbMeta Metadata about the database, and mainly the list of all existing schemas. + * + * @return true if the specified schema exists, false otherwise. + * + * @throws SQLException If any error occurs while interrogating the database about existing schema. + */ + protected boolean isSchemaExisting(String schemaName, final DatabaseMetaData dbMeta) throws SQLException{ + if (!supportsSchema || schemaName == null || schemaName.length() == 0) + return true; + + // Determine the case sensitivity to use for the equality test: + boolean caseSensitive = translator.isCaseSensitive(IdentifierField.SCHEMA); + + ResultSet rs = null; + try{ + // List all schemas available and stop when a schema name matches ignoring the case: + rs = dbMeta.getSchemas(); + boolean hasSchema = false; + while(!hasSchema && rs.next()) + hasSchema = equals(rs.getString(1), schemaName, caseSensitive); + return hasSchema; + }finally{ + close(rs); + } + } + + /** + *

    Tell whether the specified table exists in the database. + * To do so, it is using the given {@link DatabaseMetaData} object to query the database and list all existing tables.

    + * + *

    Important note: + * If schemas are not supported by this connection but a schema name is even though provided in parameter, + * the table name will be prefixed by the schema name. + * The research will then be done with NULL as schema name and this prefixed table name. + *

    + * + *

    Note: + * Test on the schema name is done considering the case sensitivity indicated by the translator + * (see {@link JDBCTranslator#isCaseSensitive(IdentifierField)}). + *

    + * + *

    Note: + * This function is used by {@link #addUploadedTable(TAPTable, TableIterator)} and {@link #dropUploadedTable(TAPTable)}. + *

    + * + * @param schemaName DB name of the schema in which the table to search is. If NULL, the table is expected in any schema but ONLY one MUST exist. + * @param tableName DB name of the table to search. + * @param dbMeta Metadata about the database, and mainly the list of all existing tables. + * + * @return true if the specified table exists, false otherwise. + * + * @throws SQLException If any error occurs while interrogating the database about existing tables. + */ + protected boolean isTableExisting(String schemaName, String tableName, final DatabaseMetaData dbMeta) throws DBException, SQLException{ + if (tableName == null || tableName.length() == 0) + return true; + + // Determine the case sensitivity to use for the equality test: + boolean schemaCaseSensitive = translator.isCaseSensitive(IdentifierField.SCHEMA); + boolean tableCaseSensitive = translator.isCaseSensitive(IdentifierField.TABLE); + + ResultSet rs = null; + try{ + + // List all matching tables: + if (supportsSchema){ + String schemaPattern = schemaCaseSensitive ? schemaName : null; + String tablePattern = tableCaseSensitive ? tableName : null; + rs = dbMeta.getTables(null, schemaPattern, tablePattern, null); + }else{ + String tablePattern = tableCaseSensitive ? tableName : null; + rs = dbMeta.getTables(null, null, tablePattern, null); + } + + // Stop on the first table which match completely (schema name + table name in function of their respective case sensitivity): + int cnt = 0; + while(rs.next()){ + String rsSchema = nullifyIfNeeded(rs.getString(2)); + String rsTable = rs.getString(3); + if (!supportsSchema || schemaName == null || equals(rsSchema, schemaName, schemaCaseSensitive)){ + if (equals(rsTable, tableName, tableCaseSensitive)) + cnt++; + } + } + + if (cnt > 1){ + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "TABLE_EXIST", "More than one table match to these criteria (schema=" + schemaName + " (case sensitive?" + schemaCaseSensitive + ") && table=" + tableName + " (case sensitive?" + tableCaseSensitive + "))!", null); + throw new DBException("More than one table match to these criteria (schema=" + schemaName + " (case sensitive?" + schemaCaseSensitive + ") && table=" + tableName + " (case sensitive?" + tableCaseSensitive + "))!"); + } + + return cnt == 1; + + }finally{ + close(rs); + } + } + + /** + *

    Tell whether the specified column exists in the specified table of the database. + * To do so, it is using the given {@link DatabaseMetaData} object to query the database and list all existing columns.

    + * + *

    Important note: + * If schemas are not supported by this connection but a schema name is even though provided in parameter, + * the table name will be prefixed by the schema name. + * The research will then be done with NULL as schema name and this prefixed table name. + *

    + * + *

    Note: + * Test on the schema name is done considering the case sensitivity indicated by the translator + * (see {@link JDBCTranslator#isCaseSensitive(IdentifierField)}). + *

    + * + *

    Note: + * This function is used by {@link #loadSchemas(TAPTable, TAPMetadata, Statement)}, {@link #loadTables(TAPTable, TAPMetadata, Statement)} + * and {@link #loadColumns(TAPTable, List, Statement)}. + *

    + * + * @param schemaName DB name of the table schema. MAY BE NULL + * @param tableName DB name of the table containing the column to search. MAY BE NULL + * @param columnName DB name of the column to search. + * @param dbMeta Metadata about the database, and mainly the list of all existing tables. + * + * @return true if the specified column exists, false otherwise. + * + * @throws SQLException If any error occurs while interrogating the database about existing columns. + */ + protected boolean isColumnExisting(String schemaName, String tableName, String columnName, final DatabaseMetaData dbMeta) throws DBException, SQLException{ + if (columnName == null || columnName.length() == 0) + return true; + + // Determine the case sensitivity to use for the equality test: + boolean schemaCaseSensitive = translator.isCaseSensitive(IdentifierField.SCHEMA); + boolean tableCaseSensitive = translator.isCaseSensitive(IdentifierField.TABLE); + boolean columnCaseSensitive = translator.isCaseSensitive(IdentifierField.COLUMN); + + ResultSet rsT = null, rsC = null; + try{ + /* Note: + * + * The DatabaseMetaData.getColumns(....) function does not work properly + * with the SQLite driver: when all parameters are set to null, meaning all columns of the database + * must be returned, absolutely no rows are selected. + * + * The solution proposed here, is to first search all (matching) tables, and then for each table get + * all its columns and find the matching one(s). + */ + + // List all matching tables: + if (supportsSchema){ + String schemaPattern = schemaCaseSensitive ? schemaName : null; + String tablePattern = tableCaseSensitive ? tableName : null; + rsT = dbMeta.getTables(null, schemaPattern, tablePattern, null); + }else{ + String tablePattern = tableCaseSensitive ? tableName : null; + rsT = dbMeta.getTables(null, null, tablePattern, null); + } + + // For each matching table: + int cnt = 0; + String columnPattern = columnCaseSensitive ? columnName : null; + while(rsT.next()){ + String rsSchema = nullifyIfNeeded(rsT.getString(2)); + String rsTable = rsT.getString(3); + // test the schema name: + if (!supportsSchema || schemaName == null || equals(rsSchema, schemaName, schemaCaseSensitive)){ + // test the table name: + if ((tableName == null || equals(rsTable, tableName, tableCaseSensitive))){ + // list its columns: + rsC = dbMeta.getColumns(null, rsSchema, rsTable, columnPattern); + // count all matching columns: + while(rsC.next()){ + String rsColumn = rsC.getString(4); + if (equals(rsColumn, columnName, columnCaseSensitive)) + cnt++; + } + close(rsC); + } + } + } + + if (cnt > 1){ + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "COLUMN_EXIST", "More than one column match to these criteria (schema=" + schemaName + " (case sensitive?" + schemaCaseSensitive + ") && table=" + tableName + " (case sensitive?" + tableCaseSensitive + ") && column=" + columnName + " (case sensitive?" + columnCaseSensitive + "))!", null); + throw new DBException("More than one column match to these criteria (schema=" + schemaName + " (case sensitive?" + schemaCaseSensitive + ") && table=" + tableName + " (case sensitive?" + tableCaseSensitive + ") && column=" + columnName + " (case sensitive?" + columnCaseSensitive + "))!"); + } + + return cnt == 1; + + }finally{ + close(rsT); + close(rsC); + } + } + + /* + *

    Build a table prefix with the given schema name.

    + * + *

    By default, this function returns: schemaName + "_".

    + * + *

    CAUTION: + * This function is used only when schemas are not supported by the DBMS connection. + * It aims to propose an alternative of the schema notion by prefixing the table name by the schema name. + *

    + * + *

    Note: + * If the given schema is NULL or is an empty string, an empty string will be returned. + * Thus, no prefix will be set....which is very useful when the table name has already been prefixed + * (in such case, the DB name of its schema has theoretically set to NULL). + *

    + * + * @param schemaName (DB) Schema name. + * + * @return The corresponding table prefix, or "" if the given schema name is an empty string or NULL. + * + protected String getTablePrefix(final String schemaName){ + if (schemaName != null && schemaName.trim().length() > 0) + return schemaName + "_"; + else + return ""; + }*/ + + /** + * Tell whether the specified table (using its DB name only) is a standard one or not. + * + * @param dbTableName DB (unqualified) table name. + * @param stdTables List of all tables to consider as the standard ones. + * @param caseSensitive Indicate whether the equality test must be done case sensitively or not. + * + * @return The corresponding {@link STDTable} if the specified table is a standard one, + * NULL otherwise. + * + * @see TAPMetadata#resolveStdTable(String) + */ + protected final STDTable isStdTable(final String dbTableName, final TAPTable[] stdTables, final boolean caseSensitive){ + if (dbTableName != null){ + for(TAPTable t : stdTables){ + if (equals(dbTableName, t.getDBName(), caseSensitive)) + return TAPMetadata.resolveStdTable(t.getADQLName()); + } + } + return null; + } + + /** + *

    "Execute" the query update. This update must concern ONLY ONE ROW.

    + * + *

    + * Note that the "execute" action will be different in function of whether batch update queries are supported or not by this connection: + *

    + *
      + *
    • + * If batch update queries are supported, just {@link PreparedStatement#addBatch()} will be called. + * It means, the query will be appended in a list and will be executed only if + * {@link #executeBatchUpdates(PreparedStatement, int)} is then called. + *
    • + *
    • + * If they are NOT supported, {@link PreparedStatement#executeUpdate()} will merely be called. + *
    • + *
    + * + *

    + * Before returning, and only if batch update queries are not supported, this function is ensuring that exactly one row has been updated. + * If it is not the case, a {@link DBException} is thrown. + *

    + * + *

    Important note: + * If the function {@link PreparedStatement#addBatch()} fails by throwing an {@link SQLException}, batch updates + * will be afterwards considered as not supported by this connection. Besides, if this row is the first one in a batch update (parameter indRow=1), + * then, the error will just be logged and an {@link PreparedStatement#executeUpdate()} will be tried. However, if the row is not the first one, + * the error will be logged but also thrown as a {@link DBException}. In both cases, a subsequent call to + * {@link #executeBatchUpdates(PreparedStatement, int)} will have obviously no effect. + *

    + * + * @param stmt {@link PreparedStatement} in which the update query has been prepared. + * @param indRow Index of the row in the whole update process. It is used only for error management purpose. + * + * @throws SQLException If {@link PreparedStatement#executeUpdate()} fails.
    + * @throws DBException If {@link PreparedStatement#addBatch()} fails and this update does not concern the first row, or if the number of updated rows is different from 1. + */ + protected final void executeUpdate(final PreparedStatement stmt, int indRow) throws SQLException, DBException{ + // BATCH INSERTION: (the query is queued and will be executed later) + if (supportsBatchUpdates){ + // Add the prepared query in the batch queue of the statement: + try{ + stmt.addBatch(); + }catch(SQLException se){ + supportsBatchUpdates = false; + /* + * If the error happens for the first row, it is still possible to insert all rows + * with the non-batch function - executeUpdate(). + * + * Otherwise, it is impossible to insert the previous batched rows ; an exception must be thrown + * and must stop the whole TAP_SCHEMA initialization. + */ + if (indRow == 1){ + if (logger != null) + logger.logDB(LogLevel.WARNING, this, "EXEC_UPDATE", "BATCH query impossible => TRYING AGAIN IN A NORMAL EXECUTION (executeUpdate())!", se); + }else{ + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "EXEC_UPDATE", "BATCH query impossible!", se); + throw new DBException("BATCH query impossible!", se); + } + } + } + + // NORMAL INSERTION: (immediate insertion) + if (!supportsBatchUpdates){ + + // Insert the row prepared in the given statement: + int nbRowsWritten = stmt.executeUpdate(); + + // Check the row has been inserted with success: + if (nbRowsWritten != 1){ + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "EXEC_UPDATE", "ROW " + indRow + " not inserted!", null); + throw new DBException("ROW " + indRow + " not inserted!"); + } + } + } + + /** + *

    Execute all batched queries.

    + * + *

    To do so, {@link PreparedStatement#executeBatch()} and then, if the first was successful, {@link PreparedStatement#clearBatch()} is called.

    + * + *

    + * Before returning, this function is ensuring that exactly the given number of rows has been updated. + * If it is not the case, a {@link DBException} is thrown. + *

    + * + *

    Note: + * This function has no effect if batch queries are not supported. + *

    + * + *

    Important note: + * In case {@link PreparedStatement#executeBatch()} fails by throwing an {@link SQLException}, + * batch update queries will be afterwards considered as not supported by this connection. + *

    + * + * @param stmt {@link PreparedStatement} in which the update query has been prepared. + * @param nbRows Number of rows that should be updated. + * + * @throws DBException If {@link PreparedStatement#executeBatch()} fails, or if the number of updated rows is different from the given one. + */ + protected final void executeBatchUpdates(final PreparedStatement stmt, int nbRows) throws DBException{ + if (supportsBatchUpdates){ + // Execute all the batch queries: + int[] rows; + try{ + rows = stmt.executeBatch(); + }catch(SQLException se){ + supportsBatchUpdates = false; + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "EXEC_UPDATE", "BATCH execution impossible!", se); + throw new DBException("BATCH execution impossible!", se); + } + + // Remove executed queries from the statement: + try{ + stmt.clearBatch(); + }catch(SQLException se){ + if (logger != null) + logger.logDB(LogLevel.WARNING, this, "EXEC_UPDATE", "CLEAR BATCH impossible!", se); + } + + // Count the updated rows: + int nbRowsUpdated = 0; + for(int i = 0; i < rows.length; i++) + nbRowsUpdated += rows[i]; + + // Check all given rows have been inserted with success: + if (nbRowsUpdated != nbRows){ + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "EXEC_UPDATE", "ROWS not all update (" + nbRows + " to update ; " + nbRowsUpdated + " updated)!", null); + throw new DBException("ROWS not all updated (" + nbRows + " to update ; " + nbRowsUpdated + " updated)!"); + } + } + } + + /** + * Append all items of the iterator inside the given list. + * + * @param lst List to update. + * @param it All items to append inside the list. + */ + private < T > void appendAllInto(final List lst, final Iterator it){ + while(it.hasNext()) + lst.add(it.next()); + } + + /** + *

    Tell whether the given DB name is equals (case sensitively or not, in function of the given parameter) + * to the given name coming from a {@link TAPMetadata} object.

    + * + *

    If at least one of the given name is NULL, false is returned.

    + * + *

    Note: + * The comparison will be done in function of the specified case sensitivity BUT ALSO of the case supported and stored by the DBMS. + * For instance, if it has been specified a case insensitivity and that mixed case is not supported by unquoted identifier, + * the comparison must be done, surprisingly, by considering the case if unquoted identifiers are stored in lower or upper case. + * Thus, this special way to evaluate equality should be as closed as possible to the identifier storage and research policies of the used DBMS. + *

    + * + * @param dbName Name provided by the database. + * @param metaName Name provided by a {@link TAPMetadata} object. + * @param caseSensitive true if the equality test must be done case sensitively, false otherwise. + * + * @return true if both names are equal, false otherwise. + */ + protected final boolean equals(final String dbName, final String metaName, final boolean caseSensitive){ + if (dbName == null || metaName == null) + return false; + + if (caseSensitive){ + if (supportsMixedCaseQuotedIdentifier || mixedCaseQuoted) + return dbName.equals(metaName); + else if (lowerCaseQuoted) + return dbName.equals(metaName.toLowerCase()); + else if (upperCaseQuoted) + return dbName.equals(metaName.toUpperCase()); + else + return dbName.equalsIgnoreCase(metaName); + }else{ + if (supportsMixedCaseUnquotedIdentifier) + return dbName.equalsIgnoreCase(metaName); + else if (lowerCaseUnquoted) + return dbName.equals(metaName.toLowerCase()); + else if (upperCaseUnquoted) + return dbName.equals(metaName.toUpperCase()); + else + return dbName.equalsIgnoreCase(metaName); + } + } + + @Override + public void setFetchSize(final int size){ + supportsFetchSize = true; + fetchSize = (size > 0) ? size : IGNORE_FETCH_SIZE; } } diff --git a/src/tap/error/DefaultTAPErrorWriter.java b/src/tap/error/DefaultTAPErrorWriter.java index 40534f0..6b390cb 100644 --- a/src/tap/error/DefaultTAPErrorWriter.java +++ b/src/tap/error/DefaultTAPErrorWriter.java @@ -16,64 +16,308 @@ package tap.error; * You should have received a copy of the GNU Lesser General Public License * along with TAPLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ +import java.io.BufferedWriter; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.sql.SQLException; +import java.util.LinkedHashMap; +import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import tap.ServiceConnection; +import tap.TAPException; +import tap.formatter.OutputFormat; +import tap.formatter.VOTableFormat; +import tap.log.DefaultTAPLog; +import tap.log.TAPLog; import uws.UWSException; +import uws.UWSToolBox; +import uws.job.ErrorSummary; +import uws.job.ErrorType; +import uws.job.UWSJob; import uws.job.user.JobOwner; -import uws.service.error.AbstractServiceErrorWriter; import uws.service.error.ServiceErrorWriter; -import uws.service.log.UWSLog; /** *

    Default implementation of {@link ServiceErrorWriter} for a TAP service.

    * - *

    All errors are written using the function {@link #formatError(Throwable, boolean, uws.job.ErrorType, int, String, JobOwner, HttpServletResponse)} - * of the abstract implementation of the error writer: {@link AbstractServiceErrorWriter}.

    + *

    + * On the contrary to the UWS standard, all errors must be formatted in VOTable. + * So, all errors given to this {@link ServiceErrorWriter} are formatted in VOTable using the structure defined by the IVOA. + * To do that, this class will use the function {@link VOTableFormat#writeError(String, java.util.Map, java.io.PrintWriter)}. + *

    * - *

    A {@link UWSException} may precise the HTTP error code to apply. That's why, {@link #writeError(Throwable, HttpServletResponse, HttpServletRequest, JobOwner, String)} - * has been overridden: to get this error code and submit it to the {@link #formatError(Throwable, boolean, uws.job.ErrorType, int, String, JobOwner, HttpServletResponse)} - * function. Besides, the stack trace of {@link UWSException}s is not printed (except if the message is NULL or empty). - * And this error will be logged only if its error code is {@link UWSException#INTERNAL_SERVER_ERROR}.

    + *

    + * The {@link VOTableFormat} will be got from the {@link ServiceConnection} using {@link ServiceConnection#getOutputFormat(String)} + * with "votable" as parameter. If the returned formatter is not a direct instance or an extension of {@link VOTableFormat}, + * a default instance of this class will be always used. + *

    * - *

    2 formats are managed by this implementation: HTML (default) and JSON. That means the writer will format and - * write a given error in the best appropriate format. This format is chosen thanks to the "Accept" header of the HTTP request. - * If no request is provided or if there is no known format, the HTML format is chosen by default.

    + *

    + * {@link UWSException}s and {@link TAPException}s may precise the HTTP error code to apply, + * which will be used to set the HTTP status of the response. If it is a different kind of exception, + * the HTTP status 500 (INTERNAL SERVER ERROR) will be used. + *

    * - * @author Grégory Mantelet (CDS) - * @version 06/2012 + *

    + * Besides, all exceptions except {@link UWSException} and {@link TAPException} will be logged as FATAL in the TAP context + * (with no event and no object). Thus the full stack trace is available to the administrator so that the error can + * be understood as easily and quickly as possible. + *

    * - * @see AbstractServiceErrorWriter + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (04/2015) */ -public class DefaultTAPErrorWriter extends AbstractServiceErrorWriter { +public class DefaultTAPErrorWriter implements ServiceErrorWriter { - protected final ServiceConnection service; + /** Description of the TAP service using this {@link ServiceErrorWriter}. */ + protected final ServiceConnection service; - public DefaultTAPErrorWriter(final ServiceConnection service){ + /** Logger to use to report any unexpected error. + * This attribute MUST NEVER be used directly, but only with its getter {@link #getLogger()}. */ + protected TAPLog logger = null; + + /** Object to use to format an error message into VOTable. + * This attribute MUST NEVER be used directly, but only with its getter {@link #getFormatter()}. */ + protected VOTableFormat formatter = null; + + /** + *

    Build an error writer for TAP.

    + * + *

    + * On the contrary to the UWS standard, TAP standard defines a format for error reporting. + * Errors should be reported as VOTable document with a defined structure. This one is well + * managed by {@link VOTableFormat} which is actually called by this class when an error must + * be written. + *

    + * + * @param service Description of the TAP service. + * + * @throws NullPointerException If no service description is provided. + */ + public DefaultTAPErrorWriter(final ServiceConnection service) throws NullPointerException{ + if (service == null) + throw new NullPointerException("Missing description of this TAP service! Can not build a ServiceErrorWriter."); this.service = service; } + /** + *

    Get the {@link VOTableFormat} to use in order to format errors.

    + * + *

    Note: + * If not yet set, the formatter of this {@link ServiceErrorWriter} is set to the formatter of VOTable results returned by the {@link ServiceConnection}. + * However this formatter should be a {@link VOTableFormat} instance or an extension (because the function {@link VOTableFormat#writeError(String, java.util.Map, PrintWriter)} is needed). + * Otherwise a default {@link VOTableFormat} instance will be created and always used by this {@link ServiceErrorWriter}. + *

    + * + * @return A VOTable formatter. + * + * @since 2.0 + */ + protected VOTableFormat getFormatter(){ + if (formatter == null){ + OutputFormat fmt = service.getOutputFormat("votable"); + if (fmt == null || !(fmt instanceof VOTableFormat)) + formatter = new VOTableFormat(service); + else + formatter = (VOTableFormat)fmt; + } + return formatter; + } + + /** + *

    Get the logger to use inside this {@link ServiceErrorWriter}.

    + * + *

    Note: + * If not yet set, the logger of this {@link ServiceErrorWriter} is set to the logger used by the {@link ServiceConnection}. + * If none is returned by the {@link ServiceConnection}, a default {@link TAPLog} instance writing logs in System.err + * will be created and always used by this {@link ServiceErrorWriter}. + *

    + * + * @return A logger. + * + * @since 2.0 + */ + protected TAPLog getLogger(){ + if (logger == null){ + logger = service.getLogger(); + if (logger == null) + logger = new DefaultTAPLog(System.err); + } + return logger; + } + + @Override + public boolean writeError(final Throwable t, final HttpServletResponse response, final HttpServletRequest request, final String reqID, final JobOwner user, final String action){ + if (t == null || response == null) + return true; + + boolean written = false; + // If expected error, just write it in VOTable: + if (t instanceof UWSException || t instanceof TAPException){ + // get the error type: + ErrorType type = (t instanceof UWSException) ? ((UWSException)t).getUWSErrorType() : ErrorType.FATAL; + // get the HTTP error code: + int httpErrorCode = (t instanceof UWSException) ? ((UWSException)t).getHttpErrorCode() : ((TAPException)t).getHttpErrorCode(); + // write the VOTable error: + written = writeError(t.getMessage(), type, httpErrorCode, response, request, reqID, user, action); + } + // Otherwise, log it and write a message to the user: + else + // write a message to the user: + written = writeError("INTERNAL SERVER ERROR! Sorry, this error is grave and unexpected. No explanation can be provided for the moment. Details about this error have been reported in the service log files ; you should try again your request later or notify the administrator(s) by yourself (with the following REQ_ID).", ErrorType.FATAL, UWSException.INTERNAL_SERVER_ERROR, response, request, reqID, user, action); + return written; + } + @Override - protected final UWSLog getLogger(){ - return service.getLogger(); + public boolean writeError(final String message, final ErrorType type, final int httpErrorCode, final HttpServletResponse response, final HttpServletRequest request, final String reqID, final JobOwner user, final String action){ + if (message == null || response == null) + return true; + + try{ + // Erase anything written previously in the HTTP response: + response.reset(); + + // Set the HTTP status: + response.setStatus((httpErrorCode <= 0) ? 500 : httpErrorCode); + + // Set the MIME type of the answer (XML for a VOTable document): + response.setContentType("application/xml"); + + // Set the character encoding: + response.setCharacterEncoding(UWSToolBox.DEFAULT_CHAR_ENCODING); + + }catch(IllegalStateException ise){ + /* If it is not possible any more to reset the response header and body, + * the error is anyway written in order to corrupt the HTTP response. + * Thus, it will be obvious that an error occurred and the result is + * incomplete and/or wrong.*/ + } + + try{ + // List any additional information useful to report to the user: + Map addInfos = new LinkedHashMap(); + if (reqID != null) + addInfos.put("REQ_ID", reqID); + if (type != null) + addInfos.put("ERROR_TYPE", type.toString()); + if (user != null) + addInfos.put("USER", user.getID() + ((user.getPseudo() == null) ? "" : " (" + user.getPseudo() + ")")); + if (action != null) + addInfos.put("ACTION", action); + + // Format the error in VOTable and write the document in the given HTTP response: + PrintWriter writer; + try{ + writer = response.getWriter(); + }catch(IllegalStateException ise){ + /* This exception may occur just because either the writer or + * the output-stream can be used (because already got before). + * So, we just have to get the output-stream if getting the writer + * throws an error.*/ + writer = new PrintWriter(new BufferedWriter(new OutputStreamWriter(response.getOutputStream()))); + } + getFormatter().writeError(message, addInfos, writer); + + return true; + }catch(IllegalStateException ise){ + return false; + }catch(IOException ioe){ + return false; + } + } + + @Override + public void writeError(Throwable t, ErrorSummary error, UWSJob job, OutputStream output) throws IOException{ + // Get the error message: + String message; + if (error != null && error.getMessage() != null) + message = error.getMessage(); + else if (t != null) + message = (t.getMessage() == null) ? t.getClass().getName() : t.getMessage(); + else + message = "{NO MESSAGE}"; + + // List any additional information useful to report to the user: + Map addInfos = new LinkedHashMap(); + // error type: + if (error != null && error.getType() != null) + addInfos.put("ERROR_TYPE", error.getType().toString()); + // infos about the exception: + putExceptionInfos(t, addInfos); + // job ID: + if (job != null){ + addInfos.put("JOB_ID", job.getJobId()); + if (job.getOwner() != null) + addInfos.put("USER", job.getOwner().getID() + ((job.getOwner().getPseudo() == null) ? "" : " (" + job.getOwner().getPseudo() + ")")); + } + // action running while the error occurred (only one is possible here: EXECUTING an ADQL query): + addInfos.put("ACTION", "EXECUTING"); + + // Format the error in VOTable and write the document in the given HTTP response: + getFormatter().writeError(message, addInfos, new PrintWriter(output)); + } + + /** + * Add all interesting additional information about the given exception inside the given map. + * + * @param t Exception whose some details must be added inside the given map. + * @param addInfos Map of all additional information. + * + * @since 2.0 + */ + protected void putExceptionInfos(final Throwable t, final Map addInfos){ + if (t != null){ + // Browse the exception stack in order to list all exceptions' messages and to get the last cause of this error: + StringBuffer causes = new StringBuffer(); + Throwable cause = t.getCause(), lastCause = t; + int nbCauses = 0, nbStackTraces = 1; + while(cause != null){ + // new line: + causes.append('\n'); + // append the message: + causes.append("\t- ").append(cause.getMessage()); + // SQLException case: + if (cause instanceof SQLException){ + SQLException se = (SQLException)cause; + while(se.getNextException() != null){ + se = se.getNextException(); + causes.append("\n\t\t- ").append(se.getMessage()); + } + } + // go to the next message: + lastCause = cause; + cause = cause.getCause(); + nbCauses++; + nbStackTraces++; + } + + // Add the list of all causes' message: + if (causes.length() > 0) + addInfos.put("CAUSES", "\n" + nbCauses + causes.toString()); + + // Add the stack trace of the original exception ONLY IF NOT A TAP NOR A UWS EXCEPTION (only unexpected error should be detailed to the users): + if (!(lastCause instanceof TAPException && lastCause instanceof UWSException)){ + ByteArrayOutputStream stackTrace = new ByteArrayOutputStream(); + lastCause.printStackTrace(new PrintStream(stackTrace)); + addInfos.put("ORIGIN_STACK_TRACE", "\n" + nbStackTraces + "\n" + stackTrace.toString()); + } + } } @Override - public void writeError(Throwable t, HttpServletResponse response, HttpServletRequest request, JobOwner user, String action) throws IOException{ - if (t instanceof UWSException){ - UWSException ue = (UWSException)t; - formatError(ue, (ue.getMessage() == null || ue.getMessage().trim().isEmpty()), ue.getUWSErrorType(), ue.getHttpErrorCode(), action, user, response, (request != null) ? request.getHeader("Accept") : null); - if (ue.getHttpErrorCode() == UWSException.INTERNAL_SERVER_ERROR) - getLogger().error(ue); - getLogger().httpRequest(request, user, action, ue.getHttpErrorCode(), ue.getMessage(), ue); - }else - super.writeError(t, response, request, user, action); + public String getErrorDetailsMIMEType(){ + return "application/xml"; } } diff --git a/src/tap/file/LocalTAPFileManager.java b/src/tap/file/LocalTAPFileManager.java deleted file mode 100644 index 2e9beec..0000000 --- a/src/tap/file/LocalTAPFileManager.java +++ /dev/null @@ -1,148 +0,0 @@ -package tap.file; - -/* - * This file is part of TAPLibrary. - * - * TAPLibrary is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * TAPLibrary is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with TAPLibrary. If not, see . - * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) - */ - -import java.io.File; - -import tap.db.DBConnection; -import uws.UWSException; - -import uws.service.file.DefaultOwnerGroupIdentifier; -import uws.service.file.LocalUWSFileManager; -import uws.service.file.OwnerGroupIdentifier; - -/** - *

    - * Lets creating and managing all files needed in a TAP service. - * These files are: UWS job results and errors, log files, backup files and the upload directory. - *

    - *

    - * All files are written in the local machine, into the given directory. - *

    - * - * @author Grégory Mantelet (CDS) - * @version 06/2012 - * - * @see LocalUWSFileManager - */ -public class LocalTAPFileManager extends LocalUWSFileManager implements TAPFileManager { - - /** Default name of the upload directory. */ - public final static String DEFAULT_UPLOAD_DIRECTORY_NAME = "Upload"; - - /** Default name of the DB activity log file. */ - public final static String DEFAULT_DB_ACTIVITY_LOG_FILE_NAME = "service_db_activity.log"; - - /** Local directory in which all uploaded files will be kept until they are read or ignored (in this case, they will be deleted). */ - private final File uploadDirectory; - - /** - *

    Builds a {@link TAPFileManager} which manages all UWS files in the given directory.

    - *

    - * There will be one directory for each owner ID and owner directories will be grouped - * thanks to {@link DefaultOwnerGroupIdentifier}. - *

    - * - * @param root TAP root directory. - * - * @throws UWSException If the given root directory is null, is not a directory or has not the READ and WRITE permissions. - * - * @see LocalUWSFileManager#LocalUWSFileManager(File) - * @see #getUploadDirectoryName() - */ - public LocalTAPFileManager(File root) throws UWSException{ - super(root); - uploadDirectory = new File(rootDirectory, getUploadDirectoryName()); - } - - /** - *

    Builds a {@link TAPFileManager} which manages all UWS files in the given directory.

    - *

    - * If, according to the third parameter, the owner directories must be grouped, - * the {@link DefaultOwnerGroupIdentifier} will be used. - *

    - * - * @param root TAP root directory. - * @param oneDirectoryForEachUser true to create one directory for each owner ID, false otherwise. - * @param groupUserDirectories true to group user directories, false otherwise. - * note: this value is ignored if the previous parameter is false. - * - * @throws UWSException If the given root directory is null, is not a directory or has not the READ and WRITE permissions. - * - * @see LocalUWSFileManager#LocalUWSFileManager(File, boolean, boolean) - * @see #getUploadDirectoryName() - */ - public LocalTAPFileManager(File root, boolean oneDirectoryForEachUser, boolean groupUserDirectories) throws UWSException{ - super(root, oneDirectoryForEachUser, groupUserDirectories); - uploadDirectory = new File(rootDirectory, getUploadDirectoryName()); - } - - /** - * Builds a {@link TAPFileManager} which manages all UWS files in the given directory. - * - * @param root TAP root directory. - * @param oneDirectoryForEachUser true to create one directory for each owner ID, false otherwise. - * @param groupUserDirectories true to group user directories, false otherwise. - * note: this value is ignored if the previous parameter is false. - * @param ownerGroupIdentifier The "function" to use to identify the group of a job owner. - *
      - *
    • note 1: this value is ignored if one of the two previous parameters is false.
    • - *
    • note 2: if this value is null but the previous parameters are true, - * {@link DefaultOwnerGroupIdentifier} will be chosen as default group identifier.
    • - *
    - * - * @throws UWSException If the given root directory is null, is not a directory or has not the READ and WRITE permissions. - * - * @see LocalUWSFileManager#LocalUWSFileManager(File, boolean, boolean, OwnerGroupIdentifier) - * @see #getUploadDirectoryName() - */ - public LocalTAPFileManager(File root, boolean oneDirectoryForEachUser, boolean groupUserDirectories, OwnerGroupIdentifier ownerGroupIdentifier) throws UWSException{ - super(root, oneDirectoryForEachUser, groupUserDirectories, ownerGroupIdentifier); - uploadDirectory = new File(rootDirectory, getUploadDirectoryName()); - } - - @Override - protected String getLogFileName(final String logTypeGroup){ - if (logTypeGroup != null && logTypeGroup.equals(DBConnection.LOG_TYPE_DB_ACTIVITY.getCustomType())) - return DEFAULT_DB_ACTIVITY_LOG_FILE_NAME; - else - return super.getLogFileName(logTypeGroup); - } - - /** - *

    Gets the name of the directory in which all uploaded files will be saved.

    - * - *

    note 1: this function is called ONLY one time: at the creation.

    - *

    note 2: by default, this function returns: {@link #DEFAULT_UPLOAD_DIRECTORY_NAME}.

    - * - * @return The name of the upload directory. - */ - protected String getUploadDirectoryName(){ - return DEFAULT_UPLOAD_DIRECTORY_NAME; - } - - @Override - public final File getUploadDirectory(){ - if (uploadDirectory != null && !uploadDirectory.exists()) - uploadDirectory.mkdirs(); - return uploadDirectory; - } - -} diff --git a/src/tap/file/TAPFileManager.java b/src/tap/file/TAPFileManager.java deleted file mode 100644 index f15a024..0000000 --- a/src/tap/file/TAPFileManager.java +++ /dev/null @@ -1,44 +0,0 @@ -package tap.file; - -/* - * This file is part of TAPLibrary. - * - * TAPLibrary is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * TAPLibrary is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with TAPLibrary. If not, see . - * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) - */ - -import java.io.File; - -import uws.service.file.UWSFileManager; - -/** - * Minimal API of the object which will be used by the TAP service (but more particularly by its UWS resource) - * to create, delete, write and read files needed to the service (i.e. results, errors, logs, backups, upload files). - * - * @author Grégory Mantelet (CDS) - * @version 06/2012 - * - * @see UWSFileManager - */ -public interface TAPFileManager extends UWSFileManager { - - /** - * Local directory in which all uploaded files will be kept until they are read or ignored (in this case, they will be deleted). - * - * @return Path of the directory in which uploaded files must be written. - */ - public File getUploadDirectory(); - -} diff --git a/src/tap/formatter/FITSFormat.java b/src/tap/formatter/FITSFormat.java new file mode 100644 index 0000000..20c7d92 --- /dev/null +++ b/src/tap/formatter/FITSFormat.java @@ -0,0 +1,101 @@ +package tap.formatter; + +/* + * This file is part of TAPLibrary. + * + * TAPLibrary is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * TAPLibrary is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with TAPLibrary. If not, see . + * + * Copyright 2014-2015 - Astronomisches Rechen Institut (ARI) + */ + +import java.io.IOException; +import java.io.OutputStream; + +import tap.ServiceConnection; +import tap.TAPException; +import tap.TAPExecutionReport; +import tap.data.TableIterator; +import tap.formatter.VOTableFormat.LimitedStarTable; +import uk.ac.starlink.fits.FitsTableWriter; +import uk.ac.starlink.table.ColumnInfo; +import uk.ac.starlink.table.StarTable; +import uk.ac.starlink.table.StoragePolicy; + +/** + * Format any given query (table) result into FITS. + * + * @author Grégory Mantelet (ARI) + * @version 2.0 (04/2015) + * @since 2.0 + */ +public class FITSFormat implements OutputFormat { + + /** The {@link ServiceConnection} to use (for the log and to have some information about the service (particularly: name, description). */ + protected final ServiceConnection service; + + /** + * Creates a FITS formatter. + * + * @param service The service to use (for the log and to have some information about the service (particularly: name, description). + * + * @throws NullPointerException If the given service connection is null. + */ + public FITSFormat(final ServiceConnection service) throws NullPointerException{ + if (service == null) + throw new NullPointerException("The given service connection is NULL !"); + + this.service = service; + } + + @Override + public String getMimeType(){ + return "application/fits"; + } + + @Override + public String getShortMimeType(){ + return "fits"; + } + + @Override + public String getDescription(){ + return null; + } + + @Override + public String getFileExtension(){ + return "fits"; + } + + @Override + public void writeResult(TableIterator result, OutputStream output, TAPExecutionReport execReport, Thread thread) throws TAPException, IOException, InterruptedException{ + // Extract the columns' metadata: + ColumnInfo[] colInfos = VOTableFormat.toColumnInfos(result, execReport, thread); + + // Turns the result set into a table: + LimitedStarTable table = new LimitedStarTable(result, colInfos, execReport.parameters.getMaxRec()); + + // Copy the table on disk (or in memory if the table is short): + StarTable copyTable = StoragePolicy.PREFER_DISK.copyTable(table); + + /* Format the table in FITS (2 passes are needed for that, hence the copy on disk), + * and write it in the given output stream: */ + new FitsTableWriter().writeStarTable(copyTable, output); + + execReport.nbRows = table.getNbReadRows(); + + output.flush(); + } + +} diff --git a/src/tap/formatter/HTMLFormat.java b/src/tap/formatter/HTMLFormat.java new file mode 100644 index 0000000..df179fa --- /dev/null +++ b/src/tap/formatter/HTMLFormat.java @@ -0,0 +1,215 @@ +package tap.formatter; + +/* + * This file is part of TAPLibrary. + * + * TAPLibrary is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * TAPLibrary is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with TAPLibrary. If not, see . + * + * Copyright 2014 - Astronomisches Rechen Institut (ARI) + */ + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; + +import tap.ServiceConnection; +import tap.TAPException; +import tap.TAPExecutionReport; +import tap.data.TableIterator; +import uk.ac.starlink.votable.VOSerializer; +import uws.ISO8601Format; +import adql.db.DBColumn; + +/** + * Format any given query (table) result into HTML. + * + * @author Grégory Mantelet (ARI) + * @version 2.0 (10/2014) + * @since 2.0 + */ +public class HTMLFormat implements OutputFormat { + + /** The {@link ServiceConnection} to use (for the log and to have some information about the service (particularly: name, description). */ + protected final ServiceConnection service; + + /** + * Creates an HTML formatter. + * + * @param service Description of the TAP service. + * + * @throws NullPointerException If the given service connection is null. + */ + public HTMLFormat(final ServiceConnection service) throws NullPointerException{ + if (service == null) + throw new NullPointerException("The given service connection is NULL!"); + + this.service = service; + } + + @Override + public String getMimeType(){ + return "text/html"; + } + + @Override + public String getShortMimeType(){ + return "html"; + } + + @Override + public String getDescription(){ + return null; + } + + @Override + public String getFileExtension(){ + return ".html"; + } + + @Override + public void writeResult(TableIterator result, OutputStream output, TAPExecutionReport execReport, Thread thread) throws TAPException, IOException, InterruptedException{ + // Prepare the output stream: + final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(output)); + writer.write(""); + writer.newLine(); + + // Write header: + DBColumn[] columns = writeHeader(result, writer, execReport, thread); + + if (thread.isInterrupted()) + throw new InterruptedException(); + + // Write data: + writeData(result, columns, writer, execReport, thread); + + writer.write("
    "); + writer.newLine(); + writer.flush(); + } + + /** + * Write the whole header (one row whose columns are just the columns' name). + * + * @param result Result to write later (but it contains also metadata that was extracted from the result itself). + * @param writer Output in which the metadata must be written. + * @param execReport Execution report (which contains the metadata extracted/guessed from the ADQL query). + * @param thread Thread which has asked for this formatting (it must be used in order to test the {@link Thread#isInterrupted()} flag and so interrupt everything if need). + * + * @return All the written metadata. + * + * @throws IOException If there is an error while writing something in the output. + * @throws InterruptedException If the thread has been interrupted. + * @throws TAPException If any other error occurs. + */ + protected DBColumn[] writeHeader(TableIterator result, BufferedWriter writer, TAPExecutionReport execReport, Thread thread) throws IOException, TAPException, InterruptedException{ + // Prepend a description of this result: + writer.write("TAP result"); + if (service.getProviderName() != null) + writer.write(" from " + VOSerializer.formatText(service.getProviderName())); + writer.write(" on " + ISO8601Format.format(System.currentTimeMillis())); + writer.write("
    " + VOSerializer.formatText(execReport.parameters.getQuery()) + ""); + writer.write(""); + writer.newLine(); + + // Get the columns meta: + DBColumn[] selectedColumns = execReport.resultingColumns; + + // If meta are not known, no header will be written: + int nbColumns = (selectedColumns == null) ? -1 : selectedColumns.length; + if (nbColumns > 0){ + writer.write(""); + writer.newLine(); + writer.write(""); + + // Write all columns' name: + for(int i = 0; i < nbColumns; i++){ + writer.write(""); + writer.write(VOSerializer.formatText(selectedColumns[i].getADQLName())); + writer.write(""); + } + + // Go to a new line (in order to prepare the data writing): + writer.write(""); + writer.newLine(); + writer.write(""); + writer.newLine(); + writer.flush(); + } + + // Returns the written columns: + return selectedColumns; + } + + /** + * Write all the data rows. + * + * @param result Result to write. + * @param selectedColumns All columns' metadata. + * @param writer Print writer in which the data must be written. + * @param execReport Execution report (which contains the maximum allowed number of records to output). + * @param thread Thread which has asked for this formatting (it must be used in order to test the {@link Thread#isInterrupted()} flag and so interrupt everything if need). + * + * @throws IOException If there is an error while writing something in the output stream. + * @throws InterruptedException If the thread has been interrupted. + * @throws TAPException If any other error occurs. + */ + protected void writeData(TableIterator result, DBColumn[] selectedColumns, BufferedWriter writer, TAPExecutionReport execReport, Thread thread) throws IOException, TAPException, InterruptedException{ + execReport.nbRows = 0; + + writer.write(""); + writer.newLine(); + + while(result.nextRow()){ + // Stop right now the formatting if the job has been aborted/canceled/interrupted: + if (thread.isInterrupted()) + throw new InterruptedException(); + + // Deal with OVERFLOW, if needed: + if (execReport.parameters.getMaxRec() > 0 && execReport.nbRows >= execReport.parameters.getMaxRec()){ // that's to say: OVERFLOW ! + writer.write("OVERFLOW (more rows were available but have been truncated by the TAP service)"); + writer.newLine(); + break; + } + + writer.write(""); + + while(result.hasNextCol()){ + writer.write(""); + + // Write the column value: + Object colVal = result.nextCol(); + if (colVal != null) + writer.write(VOSerializer.formatText(colVal.toString())); + + writer.write(""); + + if (thread.isInterrupted()) + throw new InterruptedException(); + } + writer.write(""); + writer.newLine(); + execReport.nbRows++; + + // flush the writer every 30 lines: + if (execReport.nbRows % 30 == 0) + writer.flush(); + } + + writer.write(""); + writer.newLine(); + writer.flush(); + } + +} diff --git a/src/tap/formatter/JSONFormat.java b/src/tap/formatter/JSONFormat.java index c15625f..4e80889 100644 --- a/src/tap/formatter/JSONFormat.java +++ b/src/tap/formatter/JSONFormat.java @@ -16,142 +16,275 @@ package tap.formatter; * You should have received a copy of the GNU Lesser General Public License * along with TAPLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ +import java.io.BufferedWriter; import java.io.IOException; import java.io.OutputStream; -import java.io.PrintWriter; +import java.io.OutputStreamWriter; import org.json.JSONException; import org.json.JSONWriter; -import cds.savot.writer.SavotWriter; - -import adql.db.DBColumn; - import tap.ServiceConnection; import tap.TAPException; import tap.TAPExecutionReport; +import tap.data.TableIterator; import tap.metadata.TAPColumn; -import tap.metadata.TAPTypes; - -public abstract class JSONFormat< R > implements OutputFormat { +import tap.metadata.VotType; +import adql.db.DBColumn; +import adql.db.DBType; +import adql.db.DBType.DBDatatype; - /** Indicates whether a format report (start and end date/time) must be printed in the log output. */ - private boolean logFormatReport; +/** + * Format any given query (table) result into JSON. + * + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (04/2015) + */ +public class JSONFormat implements OutputFormat { /** The {@link ServiceConnection} to use (for the log and to have some information about the service (particularly: name, description). */ - protected final ServiceConnection service; + protected final ServiceConnection service; - public JSONFormat(final ServiceConnection service){ - this(service, false); - } + /** + * Build a JSON formatter. + * + * @param service Description of the TAP service. + * + * @throws NullPointerException If the given service connection is null. + */ + public JSONFormat(final ServiceConnection service) throws NullPointerException{ + if (service == null) + throw new NullPointerException("The given service connection is NULL!"); - public JSONFormat(final ServiceConnection service, final boolean logFormatReport){ this.service = service; - this.logFormatReport = logFormatReport; } + @Override public String getMimeType(){ return "application/json"; } + @Override public String getShortMimeType(){ return "json"; } + @Override public String getDescription(){ return null; } + @Override public String getFileExtension(){ return "json"; } @Override - public void writeResult(R queryResult, OutputStream output, TAPExecutionReport execReport, Thread thread) throws TAPException, InterruptedException{ + public void writeResult(TableIterator result, OutputStream output, TAPExecutionReport execReport, Thread thread) throws TAPException, IOException, InterruptedException{ try{ - long start = System.currentTimeMillis(); - PrintWriter writer = new PrintWriter(output); + // Prepare the output stream for JSON: + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(output)); JSONWriter out = new JSONWriter(writer); + // { out.object(); + // "metadata": [...] out.key("metadata"); - DBColumn[] columns = writeMetadata(queryResult, out, execReport, thread); + + // Write metadata part: + DBColumn[] columns = writeMetadata(result, out, execReport, thread); writer.flush(); + if (thread.isInterrupted()) + throw new InterruptedException(); + + // "data": [...] out.key("data"); - int nbRows = writeData(queryResult, columns, out, execReport, thread); + // Write the data part: + writeData(result, columns, out, execReport, thread); + + // } out.endObject(); writer.flush(); - if (logFormatReport) - service.getLogger().info("JOB " + execReport.jobID + " WRITTEN\tResult formatted (in JSON ; " + nbRows + " rows ; " + columns.length + " columns) in " + (System.currentTimeMillis() - start) + " ms !"); }catch(JSONException je){ - throw new TAPException("Error while writing a query result in JSON !", je); - }catch(IOException ioe){ - throw new TAPException("Error while writing a query result in JSON !", ioe); + throw new TAPException(je.getMessage(), je); } } - protected abstract DBColumn[] writeMetadata(R queryResult, JSONWriter out, TAPExecutionReport execReport, Thread thread) throws IOException, TAPException, InterruptedException, JSONException; + /** + * Write the whole metadata part of the JSON file. + * + * @param result Result to write later (but it contains also metadata that was extracted from the result itself). + * @param out Output stream in which the metadata must be written. + * @param execReport Execution report (which contains the metadata extracted/guessed from the ADQL query). + * @param thread Thread which has asked for this formatting (it must be used in order to test the {@link Thread#isInterrupted()} flag and so interrupt everything if need). + * + * @return All the written metadata. + * + * @throws IOException If there is an error while writing something in the output stream. + * @throws InterruptedException If the thread has been interrupted. + * @throws JSONException If there is an error while formatting something in JSON. + * @throws TAPException If any other error occurs. + * + * @see #getValidColMeta(DBColumn, TAPColumn) + */ + protected DBColumn[] writeMetadata(TableIterator result, JSONWriter out, TAPExecutionReport execReport, Thread thread) throws IOException, TAPException, InterruptedException, JSONException{ + out.array(); + + // Get the metadata extracted/guesses from the ADQL query: + DBColumn[] columnsFromQuery = execReport.resultingColumns; + + // Get the metadata extracted from the result: + TAPColumn[] columnsFromResult = result.getMetadata(); + + int indField = 0; + if (columnsFromQuery != null){ + + // For each column: + for(DBColumn field : columnsFromQuery){ + + // Try to build/get appropriate metadata for this field/column: + TAPColumn colFromResult = (columnsFromResult != null && indField < columnsFromResult.length) ? columnsFromResult[indField] : null; + TAPColumn tapCol = getValidColMeta(field, colFromResult); + + // Ensure these metadata are well returned at the end of this function: + columnsFromQuery[indField] = tapCol; + + // Write the field/column metadata in the JSON output: + writeFieldMeta(tapCol, out); + indField++; + } + } + + out.endArray(); + return columnsFromQuery; + } /** - *

    Formats in a VOTable field and writes the given {@link TAPColumn} in the given Writer.

    + * Try to get or otherwise to build appropriate metadata using those extracted from the ADQL query and those extracted from the result. * - *

    Note: If the VOTable datatype is int, short or long a NULL values is set by adding a node VALUES: <VALUES null="..." />

    + * @param typeFromQuery Metadata extracted/guessed from the ADQL query. + * @param typeFromResult Metadata extracted/guessed from the result. * - * @param col The column metadata to format into a VOTable field. + * @return The most appropriate metadata. + */ + protected TAPColumn getValidColMeta(final DBColumn typeFromQuery, final TAPColumn typeFromResult){ + if (typeFromQuery != null && typeFromQuery instanceof TAPColumn) + return (TAPColumn)typeFromQuery; + else if (typeFromResult != null){ + if (typeFromQuery != null) + return (TAPColumn)typeFromResult.copy(typeFromQuery.getDBName(), typeFromQuery.getADQLName(), null); + else + return (TAPColumn)typeFromResult.copy(); + }else + return new TAPColumn((typeFromQuery != null) ? typeFromQuery.getADQLName() : "?", new DBType(DBDatatype.VARCHAR), "?"); + } + + /** + * Formats in JSON and writes the given {@link TAPColumn} in the given output. + * + * @param tapCol The column metadata to format/write in JSON. * @param out The stream in which the formatted column metadata must be written. * * @throws IOException If there is an error while writing the field metadata. + * @throws JSONException If there is an error while formatting something in JSON format. * @throws TAPException If there is any other error (by default: never happen). */ protected void writeFieldMeta(TAPColumn tapCol, JSONWriter out) throws IOException, TAPException, JSONException{ + // { out.object(); - out.key("name").value(tapCol.getName()); + // "name": "..." + out.key("name").value(tapCol.getADQLName()); + // "description": "..." (if any) if (tapCol.getDescription() != null && tapCol.getDescription().trim().length() > 0) out.key("description").value(tapCol.getDescription()); - out.key("datatype").value(tapCol.getVotType().datatype); + // "datatype": "..." + VotType votType = new VotType(tapCol.getDatatype()); + out.key("datatype").value(votType.datatype); - int arraysize = tapCol.getVotType().arraysize; - if (arraysize == TAPTypes.STAR_SIZE) - out.key("arraysize").value("*"); - else if (arraysize > 0) - out.key("arraysize").value(arraysize); + // "arraysize": "..." (if any) + if (votType.arraysize != null) + out.key("arraysize").value(votType.arraysize); - if (tapCol.getVotType().xtype != null) - out.key("xtype").value(tapCol.getVotType().xtype); + // "xtype": "..." (if any) + if (votType.xtype != null) + out.key("xtype").value(votType.xtype); + // "unit": "..." (if any) if (tapCol.getUnit() != null && tapCol.getUnit().length() > 0) out.key("unit").value(tapCol.getUnit()); + // "ucd": "..." (if any) if (tapCol.getUcd() != null && tapCol.getUcd().length() > 0) out.key("ucd").value(tapCol.getUcd()); + // "utype": "..." (if any) if (tapCol.getUtype() != null && tapCol.getUtype().length() > 0) out.key("utype").value(tapCol.getUtype()); + // } out.endObject(); } - protected abstract int writeData(R queryResult, DBColumn[] selectedColumns, JSONWriter out, TAPExecutionReport execReport, Thread thread) throws IOException, TAPException, InterruptedException, JSONException; + /** + * Write the whole data part of the JSON file. + * + * @param result Result to write. + * @param selectedColumns All columns' metadata. + * @param out Output stream in which the data must be written. + * @param execReport Execution report (which contains the maximum allowed number of records to output). + * @param thread Thread which has asked for this formatting (it must be used in order to test the {@link Thread#isInterrupted()} flag and so interrupt everything if need). + * + * @throws IOException If there is an error while writing something in the output stream. + * @throws InterruptedException If the thread has been interrupted. + * @throws JSONException If there is an error while formatting something in JSON. + * @throws TAPException If any other error occurs. + */ + protected void writeData(TableIterator result, DBColumn[] selectedColumns, JSONWriter out, TAPExecutionReport execReport, Thread thread) throws IOException, TAPException, InterruptedException, JSONException{ + // [ + out.array(); + + execReport.nbRows = 0; + while(result.nextRow()){ + // Stop right now the formatting if the job has been aborted/canceled/interrupted: + if (thread.isInterrupted()) + throw new InterruptedException(); + + // Deal with OVERFLOW, if needed: + if (execReport.parameters.getMaxRec() > 0 && execReport.nbRows >= execReport.parameters.getMaxRec()) + break; + + // [ + out.array(); + int indCol = 0; + while(result.hasNextCol()) + // ... + writeFieldValue(result.nextCol(), selectedColumns[indCol++], out); + // ] + out.endArray(); + execReport.nbRows++; + } + + // ] + out.endArray(); + } /** - *

    Writes the given field value in the given OutputStream.

    + *

    Writes the given field value in JSON and into the given output.

    * - *

    - * The given value will be encoded as an XML element (see {@link SavotWriter#encodeElement(String)}. - * Besides, if the given value is null and if the column datatype is int, - * short or long, the NULL values declared in the field metadata will be written.

    + *

    note: special numeric values NaN and Inf (double or float) will be written as NULL values.

    * * @param value The value to write. * @param column The corresponding column metadata. diff --git a/src/tap/formatter/OutputFormat.java b/src/tap/formatter/OutputFormat.java index 13fa7c2..f450c4b 100644 --- a/src/tap/formatter/OutputFormat.java +++ b/src/tap/formatter/OutputFormat.java @@ -16,26 +16,25 @@ package tap.formatter; * You should have received a copy of the GNU Lesser General Public License * along with TAPLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Astronomisches Rechen Institut (ARI) */ +import java.io.IOException; import java.io.OutputStream; import tap.TAPException; import tap.TAPExecutionReport; +import tap.data.TableIterator; /** * Describes an output format and formats a given query result into this format. * - * @author Grégory Mantelet (CDS) - * - * @param The type of raw query result (i.e. {@link java.sql.ResultSet}). + * @author Grégory Mantelet (CDS;ARI) * - * @version 06/2012 - * - * @see VOTableFormat + * @version 2.0 (03/2015) */ -public interface OutputFormat< R > { +public interface OutputFormat { /** * Gets the MIME type corresponding to this format. @@ -66,27 +65,19 @@ public interface OutputFormat< R > { public String getFileExtension(); /** - * Formats the given query result and writes it in the given output stream. - * - * @param queryResult The raw result to format (i.e. a {@link java.sql.ResultSet}). - * @param output The output stream (a ServletOutputStream or a stream on a file) in which the formatted result must be written. - * @param execReport The report of the execution of the TAP query whose the result must be now written. - * @param thread The thread which has asked the result writting. - * - * @throws TAPException If there is an error while formatting/writing the query result. - */ - public void writeResult(final R queryResult, final OutputStream output, final TAPExecutionReport execReport, final Thread thread) throws TAPException, InterruptedException; - - /* - * Formats the given query result and writes it in some way accessible through the returned {@link Result}. + *

    Formats the given query result and writes it in the given output stream.

    * - * @param queryResult The raw result to format (i.e. a {@link java.sql.ResultSet}). - * @param job The job which processed the query. + *

    Note: the given output stream should not be closed at the end of this function. It is up to the called to do it.

    * - * @return The {@link Result} which provides an access to the formatted query result. + * @param result The raw (table) result to format. + * @param output The output stream (a ServletOutputStream or a stream on a file) in which the formatted result must be written. + * @param execReport The report of the execution of the TAP query whose the result must be now written. + * @param thread The thread which has asked the result writing. * - * @throws TAPException If there is an error while formatting/writing the query result. - * - public Result writeResult(final R queryResult, final TAPJob job) throws TAPException;*/ + * @throws TAPException If there is an error while formatting the query result. + * @throws IOException If any error occurs while writing into the given stream. + * @throws InterruptedException If the query has been interrupted/aborted. + */ + public void writeResult(final TableIterator result, final OutputStream output, final TAPExecutionReport execReport, final Thread thread) throws TAPException, IOException, InterruptedException; } diff --git a/src/tap/formatter/ResultSet2JsonFormatter.java b/src/tap/formatter/ResultSet2JsonFormatter.java deleted file mode 100644 index d7e47f8..0000000 --- a/src/tap/formatter/ResultSet2JsonFormatter.java +++ /dev/null @@ -1,121 +0,0 @@ -package tap.formatter; - -/* - * This file is part of TAPLibrary. - * - * TAPLibrary is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * TAPLibrary is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with TAPLibrary. If not, see . - * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) - */ - -import java.io.IOException; - -import java.sql.ResultSet; -import java.sql.ResultSetMetaData; -import java.sql.SQLException; - -import org.json.JSONException; -import org.json.JSONWriter; - -import tap.ServiceConnection; -import tap.TAPException; -import tap.TAPExecutionReport; - -import tap.metadata.TAPColumn; -import tap.metadata.TAPTypes; - -import adql.db.DBColumn; - -public class ResultSet2JsonFormatter extends JSONFormat implements ResultSetFormatter { - - public ResultSet2JsonFormatter(ServiceConnection service, boolean logFormatReport){ - super(service, logFormatReport); - } - - public ResultSet2JsonFormatter(ServiceConnection service){ - super(service); - } - - @Override - protected DBColumn[] writeMetadata(ResultSet queryResult, JSONWriter out, TAPExecutionReport execReport, Thread thread) throws IOException, TAPException, InterruptedException, JSONException{ - out.array(); - DBColumn[] selectedColumns = execReport.resultingColumns; - - try{ - ResultSetMetaData meta = queryResult.getMetaData(); - int indField = 1; - if (selectedColumns != null){ - for(DBColumn field : selectedColumns){ - TAPColumn tapCol = null; - try{ - tapCol = (TAPColumn)field; - }catch(ClassCastException ex){ - tapCol = new TAPColumn(field.getADQLName()); - tapCol.setDatatype(meta.getColumnTypeName(indField), TAPTypes.NO_SIZE); - service.getLogger().warning("Unknown DB datatype for the field \"" + tapCol.getName() + "\" ! It is supposed to be \"" + tapCol.getDatatype() + "\" (original value: \"" + meta.getColumnTypeName(indField) + "\")."); - selectedColumns[indField - 1] = tapCol; - } - writeFieldMeta(tapCol, out); - indField++; - - if (thread.isInterrupted()) - throw new InterruptedException(); - } - } - }catch(SQLException e){ - service.getLogger().error("Job N°" + execReport.jobID + " - Impossible to get the metadata of the given ResultSet !", e); - } - - out.endArray(); - return selectedColumns; - } - - @Override - protected int writeData(ResultSet queryResult, DBColumn[] selectedColumns, JSONWriter out, TAPExecutionReport execReport, Thread thread) throws IOException, TAPException, InterruptedException, JSONException{ - out.array(); - int nbRows = 0; - try{ - int nbColumns = queryResult.getMetaData().getColumnCount(); - while(queryResult.next()){ - if (execReport.parameters.getMaxRec() > 0 && nbRows >= execReport.parameters.getMaxRec()) // that's to say: OVERFLOW ! - break; - - out.array(); - Object value; - for(int i = 1; i <= nbColumns; i++){ - value = formatValue(queryResult.getObject(i), selectedColumns[i - 1]); - writeFieldValue(value, selectedColumns[i - 1], out); - if (thread.isInterrupted()) - throw new InterruptedException(); - } - out.endArray(); - nbRows++; - - if (thread.isInterrupted()) - throw new InterruptedException(); - } - }catch(SQLException se){ - throw new TAPException("Job N°" + execReport.jobID + " - Impossible to get the " + (nbRows + 1) + "-th rows from the given ResultSet !", se); - } - - out.endArray(); - return nbRows; - } - - @Override - public Object formatValue(Object value, DBColumn colMeta){ - return value; - } - -} diff --git a/src/tap/formatter/ResultSet2SVFormatter.java b/src/tap/formatter/ResultSet2SVFormatter.java deleted file mode 100644 index 6289570..0000000 --- a/src/tap/formatter/ResultSet2SVFormatter.java +++ /dev/null @@ -1,105 +0,0 @@ -package tap.formatter; - -/* - * This file is part of TAPLibrary. - * - * TAPLibrary is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * TAPLibrary is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with TAPLibrary. If not, see . - * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) - */ - -import java.io.IOException; -import java.io.PrintWriter; - -import java.sql.ResultSet; -import java.sql.SQLException; - -import adql.db.DBColumn; - -import tap.ServiceConnection; -import tap.TAPException; -import tap.TAPExecutionReport; - -public class ResultSet2SVFormatter extends SVFormat implements ResultSetFormatter { - - public ResultSet2SVFormatter(final ServiceConnection service, char colSeparator, boolean delimitStrings){ - super(service, colSeparator, delimitStrings); - } - - public ResultSet2SVFormatter(final ServiceConnection service, char colSeparator){ - super(service, colSeparator); - } - - public ResultSet2SVFormatter(final ServiceConnection service, String colSeparator, boolean delimitStrings){ - super(service, colSeparator, delimitStrings); - } - - public ResultSet2SVFormatter(final ServiceConnection service, String colSeparator){ - super(service, colSeparator); - } - - @Override - protected DBColumn[] writeMetadata(ResultSet queryResult, PrintWriter writer, TAPExecutionReport execReport, Thread thread) throws IOException, TAPException, InterruptedException{ - DBColumn[] selectedColumns = execReport.resultingColumns; - int nbColumns = (selectedColumns == null) ? -1 : selectedColumns.length; - if (nbColumns > 0){ - for(int i = 0; i < nbColumns - 1; i++){ - writer.print(selectedColumns[i].getADQLName()); - writer.print(separator); - } - writer.print(selectedColumns[nbColumns - 1].getADQLName()); - writer.println(); - writer.flush(); - } - return selectedColumns; - } - - @Override - protected int writeData(ResultSet queryResult, DBColumn[] selectedColumns, PrintWriter writer, TAPExecutionReport execReport, Thread thread) throws IOException, TAPException, InterruptedException{ - int nbRows = 0; - try{ - int nbColumns = queryResult.getMetaData().getColumnCount(); - while(queryResult.next()){ - if (execReport.parameters.getMaxRec() > 0 && nbRows >= execReport.parameters.getMaxRec()) // that's to say: OVERFLOW ! - break; - - Object value; - for(int i = 1; i <= nbColumns; i++){ - value = formatValue(queryResult.getObject(i), selectedColumns[i - 1]); - writeFieldValue(value, selectedColumns[i - 1], writer); - if (i != nbColumns) - writer.print(separator); - if (thread.isInterrupted()) - throw new InterruptedException(); - } - writer.println(); - nbRows++; - - if (thread.isInterrupted()) - throw new InterruptedException(); - } - writer.flush(); - }catch(SQLException se){ - throw new TAPException("Job N°" + execReport.jobID + " - Impossible to get the " + (nbRows + 1) + "-th rows from the given ResultSet !", se); - } - - return nbRows; - } - - @Override - public Object formatValue(Object value, DBColumn colMeta){ - return value; - } - -} diff --git a/src/tap/formatter/ResultSet2TextFormatter.java b/src/tap/formatter/ResultSet2TextFormatter.java deleted file mode 100644 index 045bf80..0000000 --- a/src/tap/formatter/ResultSet2TextFormatter.java +++ /dev/null @@ -1,85 +0,0 @@ -package tap.formatter; - -/* - * This file is part of TAPLibrary. - * - * TAPLibrary is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * TAPLibrary is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with TAPLibrary. If not, see . - * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) - */ - -import java.sql.ResultSet; -import java.sql.SQLException; - -import adql.db.DBColumn; - -import cds.util.AsciiTable; - -import tap.ServiceConnection; -import tap.TAPException; -import tap.TAPExecutionReport; - -public class ResultSet2TextFormatter extends TextFormat implements ResultSetFormatter { - - public ResultSet2TextFormatter(ServiceConnection service){ - super(service); - } - - @Override - protected String getHeader(ResultSet queryResult, TAPExecutionReport execReport, Thread thread) throws TAPException{ - DBColumn[] selectedColumns = execReport.resultingColumns; - StringBuffer line = new StringBuffer(); - int nbColumns = (selectedColumns == null) ? -1 : selectedColumns.length; - if (nbColumns > 0){ - for(int i = 0; i < nbColumns - 1; i++) - line.append(selectedColumns[i].getADQLName()).append('|'); - line.append(selectedColumns[nbColumns - 1].getADQLName()); - } - return line.toString(); - } - - @Override - protected int writeData(ResultSet queryResult, AsciiTable asciiTable, TAPExecutionReport execReport, Thread thread) throws TAPException{ - int nbRows = 0; - try{ - DBColumn[] selectedColumns = execReport.resultingColumns; - int nbColumns = selectedColumns.length; - StringBuffer line = new StringBuffer(); - while(queryResult.next()){ - if (execReport.parameters.getMaxRec() > 0 && nbRows >= execReport.parameters.getMaxRec()) // that's to say: OVERFLOW ! - break; - - line.delete(0, line.length()); - Object value; - for(int i = 1; i <= nbColumns; i++){ - value = formatValue(queryResult.getObject(i), selectedColumns[i - 1]); - writeFieldValue(value, selectedColumns[i - 1], line); - if (i != nbColumns) - line.append('|'); - } - asciiTable.addLine(line.toString()); - nbRows++; - } - }catch(SQLException se){ - throw new TAPException("Job N°" + execReport.jobID + " - Impossible to get the " + (nbRows + 1) + "-th rows from the given ResultSet !", se); - } - return nbRows; - } - - @Override - public Object formatValue(Object value, DBColumn colMeta){ - return value; - } - -} diff --git a/src/tap/formatter/ResultSet2VotableFormatter.java b/src/tap/formatter/ResultSet2VotableFormatter.java deleted file mode 100644 index cc841bd..0000000 --- a/src/tap/formatter/ResultSet2VotableFormatter.java +++ /dev/null @@ -1,127 +0,0 @@ -package tap.formatter; - -/* - * This file is part of TAPLibrary. - * - * TAPLibrary is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * TAPLibrary is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with TAPLibrary. If not, see . - * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) - */ - -import java.io.IOException; - -import tap.TAPExecutionReport; -import tap.TAPException; - -import java.io.OutputStream; -import java.io.PrintWriter; - -import java.sql.ResultSet; -import java.sql.ResultSetMetaData; -import java.sql.SQLException; - -import tap.ServiceConnection; -import tap.metadata.TAPColumn; -import tap.metadata.TAPTypes; - -import adql.db.DBColumn; - -/** - * Formats a {@link ResultSet} into a VOTable. - * - * @author Grégory Mantelet (CDS) - * @version 11/2011 - */ -public class ResultSet2VotableFormatter extends VOTableFormat implements ResultSetFormatter { - - public ResultSet2VotableFormatter(final ServiceConnection service) throws NullPointerException{ - super(service); - } - - public ResultSet2VotableFormatter(final ServiceConnection service, final boolean logFormatReport) throws NullPointerException{ - super(service, logFormatReport); - } - - @Override - protected DBColumn[] writeMetadata(final ResultSet queryResult, final PrintWriter output, final TAPExecutionReport execReport, final Thread thread) throws IOException, TAPException, InterruptedException{ - DBColumn[] selectedColumns = execReport.resultingColumns; - try{ - ResultSetMetaData meta = queryResult.getMetaData(); - int indField = 1; - if (selectedColumns != null){ - for(DBColumn field : selectedColumns){ - TAPColumn tapCol = null; - try{ - tapCol = (TAPColumn)field; - }catch(ClassCastException ex){ - tapCol = new TAPColumn(field.getADQLName()); - tapCol.setDatatype(meta.getColumnTypeName(indField), TAPTypes.NO_SIZE); - service.getLogger().warning("Unknown DB datatype for the field \"" + tapCol.getName() + "\" ! It is supposed to be \"" + tapCol.getDatatype() + "\" (original value: \"" + meta.getColumnTypeName(indField) + "\")."); - selectedColumns[indField - 1] = tapCol; - } - writeFieldMeta(tapCol, output); - indField++; - - if (thread.isInterrupted()) - throw new InterruptedException(); - } - } - }catch(SQLException e){ - service.getLogger().error("Job N°" + execReport.jobID + " - Impossible to get the metadata of the given ResultSet !", e); - output.println("Error while getting field(s) metadata"); - } - return selectedColumns; - } - - @Override - protected int writeData(final ResultSet queryResult, final DBColumn[] selectedColumns, final OutputStream output, final TAPExecutionReport execReport, final Thread thread) throws IOException, TAPException, InterruptedException{ - int nbRows = 0; - try{ - output.write("\t\t\t\t\n".getBytes()); - int nbColumns = queryResult.getMetaData().getColumnCount(); - while(queryResult.next()){ - if (execReport.parameters.getMaxRec() > 0 && nbRows >= execReport.parameters.getMaxRec()) - break; - - output.write("\t\t\t\t\t\n".getBytes()); - Object value; - for(int i = 1; i <= nbColumns; i++){ - output.write("\t\t\t\t\t\t".getBytes()); - value = formatValue(queryResult.getObject(i), selectedColumns[i - 1]); - writeFieldValue(value, selectedColumns[i - 1], output); - output.write("\n".getBytes()); - - if (thread.isInterrupted()) - throw new InterruptedException(); - } - - output.write("\t\t\t\t\t\n".getBytes()); - nbRows++; - - if (thread.isInterrupted()) - throw new InterruptedException(); - } - output.write("\t\t\t\t\n".getBytes()); - return nbRows; - }catch(SQLException e){ - throw new TAPException("Job N°" + execReport.jobID + " - Impossible to get the " + (nbRows + 1) + "-th rows from the given ResultSet !", e); - } - } - - @Override - public Object formatValue(Object value, DBColumn colMeta){ - return value; - } - -} diff --git a/src/tap/formatter/SVFormat.java b/src/tap/formatter/SVFormat.java index 259fff2..5c0c19d 100644 --- a/src/tap/formatter/SVFormat.java +++ b/src/tap/formatter/SVFormat.java @@ -16,71 +16,189 @@ package tap.formatter; * You should have received a copy of the GNU Lesser General Public License * along with TAPLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ +import java.io.BufferedWriter; import java.io.IOException; import java.io.OutputStream; -import java.io.PrintWriter; +import java.io.OutputStreamWriter; -import cds.savot.writer.SavotWriter; -import adql.db.DBColumn; import tap.ServiceConnection; import tap.TAPException; import tap.TAPExecutionReport; +import tap.data.TableIterator; +import adql.db.DBColumn; -public abstract class SVFormat< R > implements OutputFormat { - - /** Indicates whether a format report (start and end date/time) must be printed in the log output. */ - private boolean logFormatReport; +/** + * Format any given query (table) result into CSV or TSV (or with custom separator). + * + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (04/2015) + */ +public class SVFormat implements OutputFormat { + /** Column separator for CSV format. */ public static final char COMMA_SEPARATOR = ','; + /** Column separator for sCSV format. */ public static final char SEMI_COLON_SEPARATOR = ';'; + /** Column separator for TSV format. */ public static final char TAB_SEPARATOR = '\t'; - protected final ServiceConnection service; + /** The {@link ServiceConnection} to use (for the log and to have some information about the service (particularly: name, description). */ + protected final ServiceConnection service; + /** Column separator to use. */ protected final String separator; + + /** Indicate whether String values must be delimited by double quotes (default) or not. */ protected final boolean delimitStr; - public SVFormat(final ServiceConnection service, char colSeparator){ + /** MIME type associated with this format. + * @since 1.1 */ + protected final String mimeType; + + /** Alias of the MIME type associated with this format. + * @since 1.1 */ + protected final String shortMimeType; + + /** + * Build a SVFormat (in which String values are delimited by double quotes). + * + * @param service Description of the TAP service. + * @param colSeparator Column separator to use. + * + * @throws NullPointerException If the given service connection is null. + */ + public SVFormat(final ServiceConnection service, char colSeparator) throws NullPointerException{ this(service, colSeparator, true); } - public SVFormat(final ServiceConnection service, char colSeparator, boolean delimitStrings){ - this(service, colSeparator, delimitStrings, false); + /** + * Build a SVFormat. + * + * @param service Description of the TAP service. + * @param colSeparator Column separator to use. + * @param delimitStrings true if String values must be delimited by double quotes, false otherwise. + * + * @throws NullPointerException If the given service connection is null. + */ + public SVFormat(final ServiceConnection service, char colSeparator, boolean delimitStrings) throws NullPointerException{ + this(service, colSeparator, delimitStrings, null, null); } - public SVFormat(final ServiceConnection service, char colSeparator, boolean delimitStrings, final boolean logFormatReport){ - separator = "" + colSeparator; - delimitStr = delimitStrings; - this.service = service; - this.logFormatReport = logFormatReport; + /** + * Build a SVFormat. + * + * @param service Description of the TAP service. + * @param colSeparator Column separator to use. + * @param delimitStrings true if String values must be delimited by double quotes, false otherwise. + * @param mime The MIME type to associate with this format. note: this MIME type is then used by a user to specify the result format he wants. + * @param shortMime The alias of the MIME type to associate with this format. note: this short MIME type is then used by a user to specify the result format he wants. + * + * @throws NullPointerException If the given service connection is null. + * + * @since 2.0 + */ + public SVFormat(final ServiceConnection service, char colSeparator, boolean delimitStrings, final String mime, final String shortMime) throws NullPointerException{ + this(service, "" + colSeparator, delimitStrings, mime, shortMime); } - public SVFormat(final ServiceConnection service, String colSeparator){ + /** + * Build a SVFormat (in which String values are delimited by double quotes). + * + * @param service Description of the TAP service. + * @param colSeparator Column separator to use. + * + * @throws NullPointerException If the given service connection is null. + */ + public SVFormat(final ServiceConnection service, String colSeparator) throws NullPointerException{ this(service, colSeparator, true); } - public SVFormat(final ServiceConnection service, String colSeparator, boolean delimitStrings){ - separator = (colSeparator == null) ? ("" + COMMA_SEPARATOR) : colSeparator; + /** + * Build a SVFormat. + * + * @param service Description of the TAP service. + * @param colSeparator Column separator to use. + * @param delimitStrings true if String values must be delimited by double quotes, false otherwise. + * + * @throws NullPointerException If the given service connection is null. + */ + public SVFormat(final ServiceConnection service, String colSeparator, boolean delimitStrings) throws NullPointerException{ + this(service, colSeparator, delimitStrings, null, null); + } + + /** + * Build a SVFormat. + * + * @param service Description of the TAP service. + * @param colSeparator Column separator to use. + * @param delimitStrings true if String values must be delimited by double quotes, false otherwise. + * @param mime The MIME type to associate with this format. note: this MIME type is then used by a user to specify the result format he wants. + * @param shortMime The alias of the MIME type to associate with this format. note: this short MIME type is then used by a user to specify the result format he wants. + * + * @throws NullPointerException If the given service connection is null. + * + * @since 2.0 + */ + public SVFormat(final ServiceConnection service, String colSeparator, boolean delimitStrings, final String mime, final String shortMime) throws NullPointerException{ + if (service == null) + throw new NullPointerException("The given service connection is NULL!"); + + separator = (colSeparator == null || colSeparator.length() <= 0) ? ("" + COMMA_SEPARATOR) : colSeparator; delimitStr = delimitStrings; + mimeType = (mime == null || mime.trim().length() <= 0) ? guessMimeType(separator) : mime; + shortMimeType = (shortMime == null || shortMime.trim().length() <= 0) ? guessShortMimeType(separator) : shortMime; this.service = service; } - public String getMimeType(){ + /** + *

    Try to guess the MIME type to associate with this SV format, in function of the column separator.

    + * + *

    + * By default, only "," or ";" (text/csv) and [TAB] (text/tab-separated-values) are supported. + * If the separator is unknown, "text/plain" will be returned. + *

    + * + *

    Note: In order to automatically guess more MIME types, you should overwrite this function.

    + * + * @param separator Column separator of this SV format. + * + * @return The guessed MIME type. + * + * @since 2.0 + */ + protected String guessMimeType(final String separator){ switch(separator.charAt(0)){ case COMMA_SEPARATOR: case SEMI_COLON_SEPARATOR: return "text/csv"; case TAB_SEPARATOR: - return "text/tsv"; + return "text/tab-separated-values"; default: return "text/plain"; } } - public String getShortMimeType(){ + /** + *

    Try to guess the short MIME type to associate with this SV format, in function of the column separator.

    + * + *

    + * By default, only "," or ";" (csv) and [TAB] (tsv) are supported. + * If the separator is unknown, "text" will be returned. + *

    + * + *

    Note: In order to automatically guess more short MIME types, you should overwrite this function.

    + * + * @param separator Column separator of this SV format. + * + * @return The guessed short MIME type. + * + * @since 2.0 + */ + protected String guessShortMimeType(final String separator){ switch(separator.charAt(0)){ case COMMA_SEPARATOR: case SEMI_COLON_SEPARATOR: @@ -92,10 +210,22 @@ public abstract class SVFormat< R > implements OutputFormat { } } + @Override + public final String getMimeType(){ + return mimeType; + } + + @Override + public final String getShortMimeType(){ + return shortMimeType; + } + + @Override public String getDescription(){ return null; } + @Override public String getFileExtension(){ switch(separator.charAt(0)){ case COMMA_SEPARATOR: @@ -109,55 +239,127 @@ public abstract class SVFormat< R > implements OutputFormat { } @Override - public void writeResult(R queryResult, OutputStream output, TAPExecutionReport execReport, Thread thread) throws TAPException, InterruptedException{ - try{ - final long startTime = System.currentTimeMillis(); + public void writeResult(TableIterator result, OutputStream output, TAPExecutionReport execReport, Thread thread) throws TAPException, IOException, InterruptedException{ + // Prepare the output stream: + final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(output)); - final PrintWriter writer = new PrintWriter(output); + // Write header: + DBColumn[] columns = writeHeader(result, writer, execReport, thread); - // Write header: - DBColumn[] columns = writeMetadata(queryResult, writer, execReport, thread); + if (thread.isInterrupted()) + throw new InterruptedException(); - // Write data: - int nbRows = writeData(queryResult, columns, writer, execReport, thread); + // Write data: + writeData(result, columns, writer, execReport, thread); - writer.flush(); + writer.flush(); + } - if (logFormatReport) - service.getLogger().info("JOB " + execReport.jobID + " WRITTEN\tResult formatted (in SV[" + delimitStr + "] ; " + nbRows + " rows ; " + columns.length + " columns) in " + (System.currentTimeMillis() - startTime) + " ms !"); + /** + * Write the whole header (one row whose columns are just the columns' name). + * + * @param result Result to write later (but it contains also metadata that was extracted from the result itself). + * @param writer Output in which the metadata must be written. + * @param execReport Execution report (which contains the metadata extracted/guessed from the ADQL query). + * @param thread Thread which has asked for this formatting (it must be used in order to test the {@link Thread#isInterrupted()} flag and so interrupt everything if need). + * + * @return All the written metadata. + * + * @throws IOException If there is an error while writing something in the output. + * @throws InterruptedException If the thread has been interrupted. + * @throws TAPException If any other error occurs. + */ + protected DBColumn[] writeHeader(TableIterator result, BufferedWriter writer, TAPExecutionReport execReport, Thread thread) throws IOException, TAPException, InterruptedException{ + // Get the columns meta: + DBColumn[] selectedColumns = execReport.resultingColumns; + + // If meta are not known, no header will be written: + int nbColumns = (selectedColumns == null) ? -1 : selectedColumns.length; + if (nbColumns > 0){ + // Write all columns' name: + for(int i = 0; i < nbColumns - 1; i++){ + writer.write(selectedColumns[i].getADQLName()); + writer.write(separator); + } + writer.write(selectedColumns[nbColumns - 1].getADQLName()); - }catch(Exception ex){ - service.getLogger().error("While formatting in (T/C)SV !", ex); + // Go to a new line (in order to prepare the data writing): + writer.newLine(); + writer.flush(); } + + // Returns the written columns: + return selectedColumns; } - protected abstract DBColumn[] writeMetadata(R queryResult, PrintWriter writer, TAPExecutionReport execReport, Thread thread) throws IOException, TAPException, InterruptedException; + /** + * Write all the data rows. + * + * @param result Result to write. + * @param selectedColumns All columns' metadata. + * @param writer Writer in which the data must be written. + * @param execReport Execution report (which contains the maximum allowed number of records to output). + * @param thread Thread which has asked for this formatting (it must be used in order to test the {@link Thread#isInterrupted()} flag and so interrupt everything if need). + * + * @throws IOException If there is an error while writing something in the given writer. + * @throws InterruptedException If the thread has been interrupted. + * @throws TAPException If any other error occurs. + */ + protected void writeData(TableIterator result, DBColumn[] selectedColumns, BufferedWriter writer, TAPExecutionReport execReport, Thread thread) throws IOException, TAPException, InterruptedException{ + execReport.nbRows = 0; + + while(result.nextRow()){ + // Stop right now the formatting if the job has been aborted/canceled/interrupted: + if (thread.isInterrupted()) + throw new InterruptedException(); + + // Deal with OVERFLOW, if needed: + if (execReport.parameters.getMaxRec() > 0 && execReport.nbRows >= execReport.parameters.getMaxRec()) // that's to say: OVERFLOW ! + break; - protected abstract int writeData(R queryResult, DBColumn[] selectedColumns, PrintWriter writer, TAPExecutionReport execReport, Thread thread) throws IOException, TAPException, InterruptedException; + int indCol = 0; + while(result.hasNextCol()){ + // Write the column value: + writeFieldValue(result.nextCol(), selectedColumns[indCol++], writer); + + // Append the column separator: + if (result.hasNextCol()) + writer.write(separator); + } + writer.newLine(); + + execReport.nbRows++; + + // flush the writer every 30 lines: + if (execReport.nbRows % 30 == 0) + writer.flush(); + } + writer.flush(); + } /** - *

    Writes the given field value in the given OutputStream.

    + *

    Writes the given field value in the given Writer.

    * *

    - * The given value will be encoded as an XML element (see {@link SavotWriter#encodeElement(String)}. - * Besides, if the given value is null and if the column datatype is int, - * short or long, the NULL values declared in the field metadata will be written.

    + * A String value will be delimited if {@link #delimitStr} is true, otherwise this type of value will + * be processed like the other type of values: no delimiter and just transformed into a string. + *

    * * @param value The value to write. * @param column The corresponding column metadata. - * @param out The stream in which the field value must be written. + * @param writer The stream in which the field value must be written. * * @throws IOException If there is an error while writing the given field value in the given stream. * @throws TAPException If there is any other error (by default: never happen). */ - protected void writeFieldValue(final Object value, final DBColumn column, final PrintWriter writer) throws IOException, TAPException{ + protected void writeFieldValue(final Object value, final DBColumn column, final BufferedWriter writer) throws IOException, TAPException{ if (value != null){ if ((delimitStr && value instanceof String) || value.toString().contains(separator)){ - writer.print('"'); - writer.print(value.toString().replaceAll("\"", "'")); - writer.print('"'); + writer.write('"'); + writer.write(value.toString().replaceAll("\"", "'")); + writer.write('"'); }else - writer.print(value.toString()); + writer.write(value.toString()); } } } diff --git a/src/tap/formatter/TextFormat.java b/src/tap/formatter/TextFormat.java index 9c5c734..7b6f513 100644 --- a/src/tap/formatter/TextFormat.java +++ b/src/tap/formatter/TextFormat.java @@ -16,90 +16,214 @@ package tap.formatter; * You should have received a copy of the GNU Lesser General Public License * along with TAPLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ +import java.io.BufferedWriter; +import java.io.IOException; import java.io.OutputStream; - -import adql.db.DBColumn; - -import cds.util.AsciiTable; +import java.io.OutputStreamWriter; import tap.ServiceConnection; - import tap.TAPException; import tap.TAPExecutionReport; +import tap.data.TableIterator; +import adql.db.DBColumn; +import cds.util.AsciiTable; -public abstract class TextFormat< R > implements OutputFormat { - - /** Indicates whether a format report (start and end date/time) must be printed in the log output. */ - private boolean logFormatReport; - - protected final ServiceConnection service; - - public TextFormat(final ServiceConnection service){ - this(service, false); - } +/** + * Format any given query (table) result into a simple table ASCII representation + * (columns' width are adjusted so that all columns are well aligned and of the same width). + * + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (04/2015) + */ +public class TextFormat implements OutputFormat { + + /** Internal column separator. + * Note: the output separator is however always a |. + * @since 2.0 */ + protected static final char COL_SEP = '\u25c6'; + + /** The {@link ServiceConnection} to use (for the log and to have some information about the service (particularly: name, description). */ + protected final ServiceConnection service; + + /** + * Build a {@link TextFormat}. + * + * @param service Description of the TAP service. + * + * @throws NullPointerException If the given service connection is null. + */ + public TextFormat(final ServiceConnection service) throws NullPointerException{ + if (service == null) + throw new NullPointerException("The given service connection is NULL!"); - public TextFormat(final ServiceConnection service, final boolean logFormatReport){ this.service = service; - this.logFormatReport = logFormatReport; } + @Override public String getMimeType(){ return "text/plain"; } + @Override public String getShortMimeType(){ return "text"; } + @Override public String getDescription(){ return null; } + @Override public String getFileExtension(){ return "txt"; } @Override - public void writeResult(R queryResult, OutputStream output, TAPExecutionReport execReport, Thread thread) throws TAPException, InterruptedException{ - try{ - AsciiTable asciiTable = new AsciiTable('|'); + public void writeResult(TableIterator result, OutputStream output, TAPExecutionReport execReport, Thread thread) throws TAPException, IOException, InterruptedException{ + // Prepare the formatting of the whole output: + AsciiTable asciiTable = new AsciiTable(COL_SEP); + + // Write header: + String headerLine = getHeader(result, execReport, thread); + asciiTable.addHeaderLine(headerLine); + asciiTable.endHeaderLine(); + + if (thread.isInterrupted()) + throw new InterruptedException(); + + // Write data into the AsciiTable object: + boolean overflow = writeData(result, asciiTable, execReport, thread); + + // Finally write the formatted ASCII table (header + data) in the output stream: + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(output)); + String[] lines = asciiTable.displayAligned(new int[]{AsciiTable.LEFT}, '|'); + execReport.nbRows = 0; + for(String l : lines){ + // stop right now the formatting if the job has been aborted/canceled/interrupted: + if (thread.isInterrupted()) + throw new InterruptedException(); + // write the line: + writer.write(l); + writer.newLine(); + // update the counter of written lines: + execReport.nbRows++; + // flush the writer every 30 lines: + if (execReport.nbRows % 30 == 0) + writer.flush(); + } - final long startTime = System.currentTimeMillis(); + // Add a line in case of an OVERFLOW: + if (overflow) + writer.write("\nOVERFLOW (more rows were available but have been truncated by the TAP service)"); + + writer.flush(); + } - // Write header: - String headerLine = getHeader(queryResult, execReport, thread); - asciiTable.addHeaderLine(headerLine); - asciiTable.endHeaderLine(); + /** + * Get the whole header (one row whose columns are just the columns' name). + * + * @param result Result to write later (but it contains also metadata that was extracted from the result itself). + * @param execReport Execution report (which contains the metadata extracted/guessed from the ADQL query). + * @param thread Thread which has asked for this formatting (it must be used in order to test the {@link Thread#isInterrupted()} flag and so interrupt everything if need). + * + * @return All the written metadata. + * + * @throws TAPException If any other error occurs. + */ + protected String getHeader(final TableIterator result, final TAPExecutionReport execReport, final Thread thread) throws TAPException{ + // Get the columns meta: + DBColumn[] selectedColumns = execReport.resultingColumns; + + StringBuffer line = new StringBuffer(); + + // If meta are not known, no header will be written: + int nbColumns = (selectedColumns == null) ? -1 : selectedColumns.length; + if (nbColumns > 0){ + + // Write all columns' name: + for(int i = 0; i < nbColumns - 1; i++) + line.append(selectedColumns[i].getADQLName()).append(COL_SEP); + line.append(selectedColumns[nbColumns - 1].getADQLName()); + } - // Write data: - int nbRows = writeData(queryResult, asciiTable, execReport, thread); + // Return the header line: + return line.toString(); + } - // Write all lines in the output stream: - String[] lines = asciiTable.displayAligned(new int[]{AsciiTable.LEFT}); - for(String l : lines){ - output.write(l.getBytes()); - output.write('\n'); + /** + * Write all the data rows into the given {@link AsciiTable} object. + * + * @param queryResult Result to write. + * @param asciiTable Output in which the rows (as string) must be written. + * @param execReport Execution report (which contains the maximum allowed number of records to output). + * @param thread Thread which has asked for this formatting (it must be used in order to test the {@link Thread#isInterrupted()} flag and so interrupt everything if need). + * + * @return true if an overflow (i.e. nbDBRows > MAXREC) is detected, false otherwise. + * + * @throws InterruptedException If the thread has been interrupted. + * @throws TAPException If any other error occurs. + */ + protected boolean writeData(final TableIterator queryResult, final AsciiTable asciiTable, final TAPExecutionReport execReport, final Thread thread) throws TAPException, InterruptedException{ + execReport.nbRows = 0; + boolean overflow = false; + + // Get the list of columns: + DBColumn[] selectedColumns = execReport.resultingColumns; + int nbColumns = selectedColumns.length; + + StringBuffer line = new StringBuffer(); + while(queryResult.nextRow()){ + // Stop right now the formatting if the job has been aborted/canceled/interrupted: + if (thread.isInterrupted()) + throw new InterruptedException(); + + // Deal with OVERFLOW, if needed: + if (execReport.parameters.getMaxRec() > 0 && execReport.nbRows >= execReport.parameters.getMaxRec()){ + overflow = true; + break; } - output.flush(); - if (logFormatReport) - service.getLogger().info("JOB " + execReport.jobID + " WRITTEN\tResult formatted (in text ; " + nbRows + " rows ; " + ((execReport != null && execReport.resultingColumns != null) ? "?" : execReport.resultingColumns.length) + " columns) in " + (System.currentTimeMillis() - startTime) + " ms !"); + // Clear the line buffer: + line.delete(0, line.length()); - }catch(Exception ex){ - service.getLogger().error("While formatting in text/plain !", ex); - } - } + int indCol = 0; + while(queryResult.hasNextCol()){ - protected abstract String getHeader(final R queryResult, final TAPExecutionReport execReport, final Thread thread) throws TAPException; + // Write the column value: + writeFieldValue(queryResult.nextCol(), selectedColumns[indCol++], line); - protected abstract int writeData(final R queryResult, final AsciiTable asciiTable, final TAPExecutionReport execReport, final Thread thread) throws TAPException; + // Write the column separator (if needed): + if (indCol != nbColumns) + line.append(COL_SEP); + } + + // Append the line/row in the ASCII table: + asciiTable.addLine(line.toString()); + + execReport.nbRows++; + } + return overflow; + } + + /** + * Writes the given field value in the given buffer. + * + * @param value The value to write. + * @param tapCol The corresponding column metadata. + * @param line The buffer in which the field value must be written. + */ protected void writeFieldValue(final Object value, final DBColumn tapCol, final StringBuffer line){ - Object obj = value; - if (obj != null) - line.append(obj.toString()); + if (value != null){ + if (value instanceof String) + line.append('"').append(value.toString()).append('"'); + else + line.append(value.toString()); + } } } diff --git a/src/tap/formatter/VOTableFormat.java b/src/tap/formatter/VOTableFormat.java index e5b5625..f38b17e 100644 --- a/src/tap/formatter/VOTableFormat.java +++ b/src/tap/formatter/VOTableFormat.java @@ -16,327 +16,697 @@ package tap.formatter; * You should have received a copy of the GNU Lesser General Public License * along with TAPLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Astronomisches Rechen Institut (ARI) */ +import java.io.BufferedWriter; import java.io.IOException; - -import tap.TAPExecutionReport; -import tap.TAPJob; -import tap.TAPException; - -import uws.job.Result; - import java.io.OutputStream; +import java.io.OutputStreamWriter; import java.io.PrintWriter; - -import cds.savot.writer.SavotWriter; +import java.util.Iterator; +import java.util.Map; import tap.ServiceConnection; +import tap.TAPException; +import tap.TAPExecutionReport; +import tap.data.DataReadException; +import tap.data.TableIterator; +import tap.error.DefaultTAPErrorWriter; import tap.metadata.TAPColumn; import tap.metadata.VotType; +import tap.metadata.VotType.VotDatatype; +import uk.ac.starlink.table.AbstractStarTable; +import uk.ac.starlink.table.ColumnInfo; +import uk.ac.starlink.table.DefaultValueInfo; +import uk.ac.starlink.table.DescribedValue; +import uk.ac.starlink.table.RowSequence; +import uk.ac.starlink.table.StarTable; +import uk.ac.starlink.votable.DataFormat; +import uk.ac.starlink.votable.VOSerializer; +import uk.ac.starlink.votable.VOTableVersion; import adql.db.DBColumn; +import adql.db.DBType; +import adql.db.DBType.DBDatatype; /** - *

    Formats the given type of query result in VOTable.

    - *

    - * This abstract class is only able to format the skeleton of the VOTable. - * However, it also provides useful methods to format field metadata and field value (including NULL values). - *

    + *

    Format any given query (table) result into VOTable.

    + * *

    - * Attributes of the VOTable node are by default set by this class but can be overridden if necessary thanks to the corresponding class attributes: - * {@link #votTableVersion}, {@link #xmlnsXsi}, {@link #xsiNoNamespaceSchemaLocation}, {@link #xsiSchemaLocation} and - * {@link #xmlns}. + * Format and version of the resulting VOTable can be provided in parameters at the construction time. + * This formatter is using STIL. So all formats and versions managed by STIL are also here. + * Basically, you have the following formats: TABLEDATA, BINARY, BINARY2 (only when using VOTable v1.3) and FITS. + * The versions are: 1.0, 1.1, 1.2 and 1.3. *

    + * + *

    Note: The MIME type is automatically set in function of the given VOTable serialization:

    + *
      + *
    • none or unknown: equivalent to BINARY
    • + *
    • BINARY: "application/x-votable+xml" = "votable"
    • + *
    • BINARY2: "application/x-votable+xml;serialization=BINARY2" = "votable/b2"
    • + *
    • TABLEDATA: "application/x-votable+xml;serialization=TABLEDATA" = "votable/td"
    • + *
    • FITS: "application/x-votable+xml;serialization=FITS" = "votable/fits"
    • + *
    + *

    It is however possible to change these default values thanks to {@link #setMimeType(String, String)}.

    + * + *

    In addition of the INFO elements for QUERY_STATUS="OK" and QUERY_STATUS="OVERFLOW", two additional INFO elements are written:

    + *
      + *
    • PROVIDER = {@link ServiceConnection#getProviderName()} and {@link ServiceConnection#getProviderDescription()}
    • + *
    • QUERY = the ADQL query at the origin of this result.
    • + *
    + * *

    - * When overridding this class, you must implement {@link #writeMetadata(Object, PrintWriter, TAPJob)} and - * {@link #writeData(Object, DBColumn[], OutputStream, TAPJob)}. - * Both are called by {@link #writeResult(Object, OutputStream, TAPJob)}. Finally you will also have to implement - * {@link #writeResult(Object, TAPJob)}, which must format the given result into a VOTable saved in some way accessible - * through the returned {@link Result}. + * Furthermore, this formatter provides a function to format an error in VOTable: {@link #writeError(String, Map, PrintWriter)}. + * This is useful for TAP which requires to return in VOTable any error that occurs while any operation. + * See {@link DefaultTAPErrorWriter} for more details. *

    * - * @author Grégory Mantelet (CDS) - * @version 06/2012 - * - * @param Type of the result to format in VOTable (i.e. {@link java.sql.ResultSet}). - * - * @see ResultSet2VotableFormatter + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (04/2015) */ -public abstract class VOTableFormat< R > implements OutputFormat { - - /** Indicates whether a format report (start and end date/time) must be printed in the log output. */ - private boolean logFormatReport; +public class VOTableFormat implements OutputFormat { /** The {@link ServiceConnection} to use (for the log and to have some information about the service (particularly: name, description). */ - protected final ServiceConnection service; + protected final ServiceConnection service; - protected String votTableVersion = "1.2"; - protected String xmlnsXsi = "http://www.w3.org/2001/XMLSchema-instance"; - protected String xsiSchemaLocation = "http://www.ivoa.net/xml/VOTable/v1.2"; - protected String xsiNoNamespaceSchemaLocation = null; - protected String xmlns = "http://www.ivoa.net/xml/VOTable/v1.2"; + /** Format of the VOTable data part in which data must be formatted. Possible values are: TABLEDATA, BINARY, BINARY2 or FITS. By default, it is set to BINARY. */ + protected final DataFormat votFormat; + + /** VOTable version in which table data must be formatted. By default, it is set to v13. */ + protected final VOTableVersion votVersion; + + /** MIME type associated with this format. */ + protected String mimeType; + + /** Short form of the MIME type associated with this format. */ + protected String shortMimeType; /** - * Creates a VOTable formatter without format report. + *

    Creates a VOTable formatter.

    + * + *

    Note: + * The MIME type is automatically set to "application/x-votable+xml" = "votable". + * It is however possible to change this default value thanks to {@link #setMimeType(String, String)}. + *

    * * @param service The service to use (for the log and to have some information about the service (particularly: name, description). * * @throws NullPointerException If the given service connection is null. + */ + public VOTableFormat(final ServiceConnection service) throws NullPointerException{ + this(service, null, null); + } + + /** + *

    Creates a VOTable formatter.

    * - * @see #VOTableFormat(ServiceConnection, boolean) + * Note: The MIME type is automatically set in function of the given VOTable serialization: + *
      + *
    • none or unknown: equivalent to BINARY
    • + *
    • BINARY: "application/x-votable+xml" = "votable"
    • + *
    • BINARY2: "application/x-votable+xml;serialization=BINARY2" = "votable/b2"
    • + *
    • TABLEDATA: "application/x-votable+xml;serialization=TABLEDATA" = "votable/td"
    • + *
    • FITS: "application/x-votable+xml;serialization=FITS" = "votable/fits"
    • + *
    + *

    It is however possible to change these default values thanks to {@link #setMimeType(String, String)}.

    + * + * @param service The service to use (for the log and to have some information about the service (particularly: name, description). + * @param votFormat Serialization of the VOTable data part. (TABLEDATA, BINARY, BINARY2 or FITS). + * + * @throws NullPointerException If the given service connection is null. */ - public VOTableFormat(final ServiceConnection service) throws NullPointerException{ - this(service, false); + public VOTableFormat(final ServiceConnection service, final DataFormat votFormat) throws NullPointerException{ + this(service, votFormat, null); } /** - * Creates a VOTable formatter. + *

    Creates a VOTable formatter.

    + * + * Note: The MIME type is automatically set in function of the given VOTable serialization: + *
      + *
    • none or unknown: equivalent to BINARY
    • + *
    • BINARY: "application/x-votable+xml" = "votable"
    • + *
    • BINARY2: "application/x-votable+xml;serialization=BINARY2" = "votable/b2"
    • + *
    • TABLEDATA: "application/x-votable+xml;serialization=TABLEDATA" = "votable/td"
    • + *
    • FITS: "application/x-votable+xml;serialization=FITS" = "votable/fits"
    • + *
    + *

    It is however possible to change these default values thanks to {@link #setMimeType(String, String)}.

    * * @param service The service to use (for the log and to have some information about the service (particularly: name, description). - * @param logFormatReport true to append a format report (start and end date/time) in the log output, false otherwise. + * @param votFormat Serialization of the VOTable data part. (TABLEDATA, BINARY, BINARY2 or FITS). + * @param votVersion Version of the resulting VOTable. * * @throws NullPointerException If the given service connection is null. */ - public VOTableFormat(final ServiceConnection service, final boolean logFormatReport) throws NullPointerException{ + public VOTableFormat(final ServiceConnection service, final DataFormat votFormat, final VOTableVersion votVersion) throws NullPointerException{ if (service == null) - throw new NullPointerException("The given service connection is NULL !"); + throw new NullPointerException("The given service connection is NULL!"); + this.service = service; - this.logFormatReport = logFormatReport; + + // Set the VOTable serialization and version: + this.votFormat = (votFormat == null) ? DataFormat.BINARY : votFormat; + this.votVersion = (votVersion == null) ? VOTableVersion.V13 : votVersion; + + // Deduce automatically the MIME type and its short expression: + if (this.votFormat.equals(DataFormat.BINARY)){ + this.mimeType = "application/x-votable+xml"; + this.shortMimeType = "votable"; + }else if (this.votFormat.equals(DataFormat.BINARY2)){ + this.mimeType = "application/x-votable+xml;serialization=BINARY2"; + this.shortMimeType = "votable/b2"; + }else if (this.votFormat.equals(DataFormat.TABLEDATA)){ + this.mimeType = "application/x-votable+xml;serialization=TABLEDATA"; + this.shortMimeType = "votable/td"; + }else if (this.votFormat.equals(DataFormat.FITS)){ + this.mimeType = "application/x-votable+xml;serialization=FITS"; + this.shortMimeType = "votable/fits"; + }else{ + this.mimeType = "application/x-votable+xml"; + this.shortMimeType = "votable"; + } } + @Override public final String getMimeType(){ - return "text/xml"; + return mimeType; } + @Override public final String getShortMimeType(){ - return "votable"; + return shortMimeType; + } + + /** + *

    Set the MIME type associated with this format.

    + * + *

    Note: NULL means no modification of the current value:

    + * + * @param mimeType Full MIME type of this VOTable format. note: if NULL, the MIME type is not modified. + * @param shortForm Short form of this MIME type. note: if NULL, the short MIME type is not modified. + */ + public final void setMimeType(final String mimeType, final String shortForm){ + if (mimeType != null) + this.mimeType = mimeType; + if (shortForm != null) + this.shortMimeType = shortForm; + } + + /** + * Get the set VOTable data serialization/format (e.g. BINARY, TABLEDATA). + * + * @return The data format. + */ + public final DataFormat getVotSerialization(){ + return votFormat; } + /** + * Get the set VOTable version. + * + * @return The VOTable version. + */ + public final VOTableVersion getVotVersion(){ + return votVersion; + } + + @Override public String getDescription(){ return null; } + @Override public String getFileExtension(){ return "xml"; } /** - *

    The skeleton of the resulting VOTable is written in this method:

    - *
      - *
    • <?xml version="1.0" encoding="UTF-8">
    • - *
    • {@link #writeHeader(PrintWriter, TAPJob)}
    • - *
    • <TABLE>
    • - *
    • <DATA>
    • - *
    • {@link #writeData(Object, DBColumn[], OutputStream, TAPJob)}
    • - *
    • </DATA>
    • - *
    • if (nbRows >= job.getMaxRec()) <INFO name="QUERY_STATUS" value="OVERFLOW" />
    • - *
    • </RESOURCE>
    • - *
    • </VOTABLE>
    • - *
    + *

    Write the given error message as VOTable document.

    + * + *

    Note: + * In the TAP protocol, all errors must be returned as VOTable. The class {@link DefaultTAPErrorWriter} is in charge of the management + * and reporting of all errors. It is calling this function while the error message to display to the user is ready and + * must be written in the HTTP response. + *

    * - * @see tap.formatter.OutputFormat#writeResult(Object, OutputStream, TAPExecutionReport) + *

    Here is the XML format of this VOTable error:

    + *
    +	 * 	<VOTABLE version="..." xmlns="..." >
    +	 * 		<RESOURCE type="results">
    +	 * 			<INFO name="QUERY_STATUS" value="ERROR>
    +	 * 				...
    +	 * 			</INFO>
    +	 * 			<INFO name="PROVIDER" value="...">...</INFO>
    +	 * 			<!-- other optional INFOs (e.g. request parameters) -->
    +	 * 		</RESOURCE>
    +	 * 	</VOTABLE>
    +	 * 
    + * + * @param message Error message to display to the user. + * @param otherInfo List of other additional information to display. optional + * @param writer Stream in which the VOTable error must be written. + * + * @throws IOException If any error occurs while writing in the given output. + * + * @since 2.0 */ - public final void writeResult(final R queryResult, final OutputStream output, final TAPExecutionReport execReport, final Thread thread) throws TAPException, InterruptedException{ - try{ - long start = System.currentTimeMillis(); - - PrintWriter out = new PrintWriter(output); - out.println(""); - writeHeader(out, execReport); - out.println("\t\t"); - DBColumn[] columns = writeMetadata(queryResult, out, execReport, thread); - out.println("\t\t\t"); - out.flush(); - int nbRows = writeData(queryResult, columns, output, execReport, thread); - output.flush(); - out.println("\t\t\t"); - out.println("\t\t
    "); - // OVERFLOW ? - if (execReport.parameters.getMaxRec() > 0 && nbRows >= execReport.parameters.getMaxRec()) - out.println("\t\t"); - out.println("\t"); - out.println(""); - out.flush(); - - if (logFormatReport) - service.getLogger().info("JOB " + execReport.jobID + " WRITTEN\tResult formatted (in VOTable ; " + nbRows + " rows ; " + columns.length + " columns) in " + (System.currentTimeMillis() - start) + " ms !"); - }catch(IOException ioe){ - throw new TAPException("Error while writing a query result in VOTable !", ioe); + public void writeError(final String message, final Map otherInfo, final PrintWriter writer) throws IOException{ + BufferedWriter out = new BufferedWriter(writer); + + // Set the root VOTABLE node: + out.write(""); + out.newLine(); + out.write(""); + out.newLine(); + + // The RESOURCE note MUST have a type "results": [REQUIRED] + out.write(""); + out.newLine(); + + // Indicate that the query has been successfully processed: [REQUIRED] + out.write("" + (message == null ? "" : VOSerializer.formatText(message)) + ""); + out.newLine(); + + // Append the PROVIDER information (if any): [OPTIONAL] + if (service.getProviderName() != null){ + out.write("" + ((service.getProviderDescription() == null) ? "" : VOSerializer.formatText(service.getProviderDescription())) + ""); + out.newLine(); + } + + // Append the ADQL query at the origin of this result: [OPTIONAL] + if (otherInfo != null){ + Iterator> it = otherInfo.entrySet().iterator(); + while(it.hasNext()){ + Map.Entry entry = it.next(); + if (entry.getValue() != null){ + if (entry.getValue().startsWith("\n")){ + int sep = entry.getValue().substring(1).indexOf('\n'); + if (sep < 0) + sep = 0; + else + sep++; + out.write("\n" + entry.getValue().substring(sep + 1) + "\n"); + }else + out.write(""); + out.newLine(); + } + } } + + out.flush(); + + /* Write footer. */ + out.write(""); + out.newLine(); + out.write(""); + out.newLine(); + + out.flush(); + } + + @Override + public final void writeResult(final TableIterator queryResult, final OutputStream output, final TAPExecutionReport execReport, final Thread thread) throws TAPException, IOException, InterruptedException{ + ColumnInfo[] colInfos = toColumnInfos(queryResult, execReport, thread); + + /* Turns the result set into a table. */ + LimitedStarTable table = new LimitedStarTable(queryResult, colInfos, execReport.parameters.getMaxRec()); + + /* Prepares the object that will do the serialization work. */ + VOSerializer voser = VOSerializer.makeSerializer(votFormat, votVersion, table); + BufferedWriter out = new BufferedWriter(new OutputStreamWriter(output)); + + /* Write header. */ + writeHeader(votVersion, execReport, out); + + if (thread.isInterrupted()) + throw new InterruptedException(); + + /* Write table element. */ + voser.writeInlineTableElement(out); + execReport.nbRows = table.getNbReadRows(); + out.flush(); + + /* Check for overflow and write INFO if required. */ + if (table.lastSequenceOverflowed()){ + out.write(""); + out.newLine(); + } + + /* Write footer. */ + out.write(""); + out.newLine(); + out.write(""); + out.newLine(); + + out.flush(); } /** - *

    Writes the root node of the VOTable: <VOTABLE>.

    - *

    - * Attributes of this node are written thanks to their corresponding attributes in this class: - * {@link #votTableVersion}, {@link #xmlnsXsi}, {@link #xsiNoNamespaceSchemaLocation}, {@link #xsiSchemaLocation} and {@link #xmlns}. - * They are written only if different from null. - *

    + *

    Writes the first VOTable nodes/elements preceding the data: VOTABLE, RESOURCE and 3 INFOS (QUERY_STATUS, PROVIDER, QUERY).

    * - * @param output Writer in which the root node must be written. + * @param votVersion Target VOTable version. * @param execReport The report of the query execution. + * @param out Writer in which the root node must be written. * * @throws IOException If there is an error while writing the root node in the given Writer. * @throws TAPException If there is any other error (by default: never happen). */ - protected void writeHeader(final PrintWriter output, final TAPExecutionReport execReport) throws IOException, TAPException{ - StringBuffer strBuf = new StringBuffer("'); - output.println(strBuf); - - output.println("\t"); - - // INFO items: - output.println("\t\t"); - output.println("\t\t" + ((service.getProviderDescription() == null) ? "" : SavotWriter.encodeElement(service.getProviderDescription())) + ""); - output.println("\t\t"); + protected void writeHeader(final VOTableVersion votVersion, final TAPExecutionReport execReport, final BufferedWriter out) throws IOException, TAPException{ + // Set the root VOTABLE node: + out.write(""); + out.newLine(); + out.write(""); + out.newLine(); + + // The RESOURCE note MUST have a type "results": [REQUIRED] + out.write(""); + out.newLine(); + + // Indicate that the query has been successfully processed: [REQUIRED] + out.write(""); + out.newLine(); + + // Append the PROVIDER information (if any): [OPTIONAL] + if (service.getProviderName() != null){ + out.write("" + ((service.getProviderDescription() == null) ? "" : VOSerializer.formatText(service.getProviderDescription())) + ""); + out.newLine(); + } + + // Append the ADQL query at the origin of this result: [OPTIONAL] + String adqlQuery = execReport.parameters.getQuery(); + if (adqlQuery != null){ + out.write(""); + out.newLine(); + } + + /* TODO Add somewhere in the table header the different Coordinate Systems used in this result! + * 2 ways to do so: + * 1/ COOSYS (deprecated from VOTable 1.2, but soon un-deprecated) + * 2/ a GROUP item with the STC expression of the coordinate system. + */ + + out.flush(); } /** - *

    Writes fields' metadata of the given query result in the given Writer.

    - *

    Important: To write write metadata of a given field you can use {@link #writeFieldMeta(TAPColumn, PrintWriter)}.

    + * Writes fields' metadata of the given query result. * - * @param queryResult The query result from whose fields' metadata must be written. - * @param output Writer in which fields' metadata must be written. + * @param result The query result from whose fields' metadata must be written. * @param execReport The report of the query execution. - * @param thread The thread which asked for the result writting. + * @param thread The thread which asked for the result writing. * - * @return Extracted field's metadata. + * @return Extracted field's metadata, or NULL if no metadata have been found (theoretically, it never happens). * - * @throws IOException If there is an error while writing the metadata in the given Writer. + * @throws IOException If there is an error while writing the metadata. * @throws TAPException If there is any other error. * @throws InterruptedException If the given thread has been interrupted. */ - protected abstract DBColumn[] writeMetadata(final R queryResult, final PrintWriter output, final TAPExecutionReport execReport, final Thread thread) throws IOException, TAPException, InterruptedException; + public static final ColumnInfo[] toColumnInfos(final TableIterator result, final TAPExecutionReport execReport, final Thread thread) throws IOException, TAPException, InterruptedException{ + // Get the metadata extracted/guesses from the ADQL query: + DBColumn[] columnsFromQuery = execReport.resultingColumns; + + // Get the metadata extracted from the result: + TAPColumn[] columnsFromResult = result.getMetadata(); + + int indField = 0; + if (columnsFromQuery != null){ + + // Initialize the resulting array: + ColumnInfo[] colInfos = new ColumnInfo[columnsFromQuery.length]; + + // For each column: + for(DBColumn field : columnsFromQuery){ + + // Try to build/get appropriate metadata for this field/column: + TAPColumn colFromResult = (columnsFromResult != null && indField < columnsFromResult.length) ? columnsFromResult[indField] : null; + TAPColumn tapCol = getValidColMeta(field, colFromResult); + + // Build the corresponding ColumnInfo object: + colInfos[indField] = getColumnInfo(tapCol); + + indField++; + } + + return colInfos; + }else + return null; + } /** - *

    Formats in a VOTable field and writes the given {@link TAPColumn} in the given Writer.

    + * Try to get or otherwise to build appropriate metadata using those extracted from the ADQL query and those extracted from the result. * - *

    Note: If the VOTable datatype is int, short or long a NULL values is set by adding a node VALUES: <VALUES null="..." />

    + * @param typeFromQuery Metadata extracted/guessed from the ADQL query. + * @param typeFromResult Metadata extracted/guessed from the result. * - * @param col The column metadata to format into a VOTable field. - * @param out The stream in which the formatted column metadata must be written. - * - * @throws IOException If there is an error while writing the field metadata. - * @throws TAPException If there is any other error (by default: never happen). + * @return The most appropriate metadata. */ - protected void writeFieldMeta(TAPColumn col, PrintWriter out) throws IOException, TAPException{ - StringBuffer fieldline = new StringBuffer("\t\t\t"); - - fieldline.append(" 0) - fieldline.append(" ucd=").append('"').append(SavotWriter.encodeAttribute(col.getUcd())).append('"'); + // Set the shape (VOTable arraysize): + colInfo.setShape(getShape(votType.arraysize)); - if (col.getUtype() != null && col.getUtype().length() > 0) - fieldline.append(" utype=").append('"').append(SavotWriter.encodeAttribute(col.getUtype())).append('"'); + // Set this value may be NULL (note: it is not really necessary since STIL set this flag to TRUE by default): + colInfo.setNullable(true); - if (col.getUnit() != null && col.getUnit().length() > 0) - fieldline.append(" unit=").append('"').append(SavotWriter.encodeAttribute(col.getUnit())).append('"'); + // Set the XType (if any): + if (votType.xtype != null) + colInfo.setAuxDatum(new DescribedValue(new DefaultValueInfo("xtype", String.class, "VOTable xtype attribute"), votType.xtype)); - if (col.getDescription() != null && !col.getDescription().trim().isEmpty()) - description = col.getDescription().trim(); - else - description = null; + // Set the additional information: unit, UCD and UType: + colInfo.setUnitString(tapCol.getUnit()); + colInfo.setUCD(tapCol.getUcd()); + colInfo.setUtype(tapCol.getUtype()); - if (nullVal != null || description != null){ - fieldline.append(">\n"); - if (nullVal != null) - fieldline.append("\n"); - if (description != null) - fieldline.append("").append(SavotWriter.encodeElement(description)).append("\n"); - fieldline.append(""); - out.println(fieldline); - }else{ - fieldline.append("/>"); - out.println(fieldline); - } + return colInfo; } /** - *

    Writes the data of the given query result in the given OutputStream.

    - *

    Important: To write a field value you can use {@link #writeFieldValue(Object, DBColumn, OutputStream)}.

    - * - * @param queryResult The query result which contains the data to write. - * @param selectedColumns The columns selected by the query. - * @param output The stream in which the data must be written. - * @param execReport The report of the query execution. - * @param thread The thread which asked for the result writting. + * Convert the VOTable datatype string into a corresponding {@link Class} object. * - * @return The number of written rows. (note: if this number is greater than the value of MAXREC: OVERFLOW) + * @param datatype Value of the VOTable attribute "datatype". + * @param arraysize Value of the VOTable attribute "arraysize". * - * @throws IOException If there is an error while writing the data in the given stream. - * @throws TAPException If there is any other error. - * @throws InterruptedException If the given thread has been interrupted. + * @return The corresponding {@link Class} object. */ - protected abstract int writeData(final R queryResult, final DBColumn[] selectedColumns, final OutputStream output, final TAPExecutionReport execReport, final Thread thread) throws IOException, TAPException, InterruptedException; + protected static final Class getDatatypeClass(final VotDatatype datatype, final String arraysize){ + // Determine whether this type is an array or not: + boolean isScalar = arraysize == null || (arraysize.length() == 1 && arraysize.equals("1")); + + // Guess the corresponding Class object (see section "7.1.4 Data Types" of the STIL documentation): + switch(datatype){ + case BIT: + return boolean[].class; + case BOOLEAN: + return isScalar ? Boolean.class : boolean[].class; + case DOUBLE: + return isScalar ? Double.class : double[].class; + case DOUBLECOMPLEX: + return double[].class; + case FLOAT: + return isScalar ? Float.class : float[].class; + case FLOATCOMPLEX: + return float[].class; + case INT: + return isScalar ? Integer.class : int[].class; + case LONG: + return isScalar ? Long.class : long[].class; + case SHORT: + return isScalar ? Short.class : short[].class; + case UNSIGNEDBYTE: + return isScalar ? Short.class : short[].class; + case CHAR: + case UNICODECHAR: + default: /* If the type is not know (theoretically, never happens), return char[*] by default. */ + return isScalar ? Character.class : String.class; + } + } /** - *

    Writes the given field value in the given OutputStream.

    - * - *

    - * The given value will be encoded as an XML element (see {@link SavotWriter#encodeElement(String)}. - * Besides, if the given value is null and if the column datatype is int, - * short or long, the NULL values declared in the field metadata will be written.

    + * Convert the given VOTable arraysize into a {@link ColumnInfo} shape. * - * @param value The value to write. - * @param column The corresponding column metadata. - * @param output The stream in which the field value must be written. + * @param arraysize Value of the VOTable attribute "arraysize". * - * @throws IOException If there is an error while writing the given field value in the given stream. - * @throws TAPException If there is any other error (by default: never happen). + * @return The corresponding {@link ColumnInfo} shape. */ - protected void writeFieldValue(final Object value, final DBColumn column, final OutputStream output) throws IOException, TAPException{ - String fieldValue = (value == null) ? null : value.toString(); - if (fieldValue == null && column instanceof TAPColumn) - fieldValue = getNullValue(((TAPColumn)column).getVotType().datatype); - if (fieldValue != null) - output.write(SavotWriter.encodeElement(fieldValue).getBytes()); + protected static final int[] getShape(final String arraysize){ + /* + * Note: multi-dimensional arrays are forbidden in the TAP library, + * so no 'nxm...' is possible. + */ + + // No arraysize => empty array: + if (arraysize == null) + return new int[0]; + + // '*' or 'n*' => {-1}: + else if (arraysize.charAt(arraysize.length() - 1) == '*') + return new int[]{-1}; + + // 'n' => {n}: + else{ + try{ + return new int[]{Integer.parseInt(arraysize)}; + }catch(NumberFormatException nfe){ + // if the given arraysize is incorrect (theoretically, never happens), it is like no arraysize has been provided: + return new int[0]; + } + } } /** - *

    Gets the NULL value corresponding to the given datatype:

    - *
      - *
    • for int: {@link Integer#MIN_VALUE}
    • - *
    • for short: {@link Short#MIN_VALUE}
    • - *
    • for long: {@link Long#MIN_VALUE}
    • - *
    • for anything else, null will be returned.
    • - *
    - * - * @param datatype A VOTable datatype. + *

    + * Special {@link StarTable} able to read a fixed maximum number of rows {@link TableIterator}. + * However, if no limit is provided, all rows are read. + *

    * - * @return The corresponding NULL value, or null if there is none. + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (10/2014) + * @since 2.0 */ - public static final String getNullValue(String datatype){ - if (datatype == null) - return null; + public static class LimitedStarTable extends AbstractStarTable { + + /** Number of columns to read. */ + private final int nbCol; + + /** Information about all columns to read. */ + private final ColumnInfo[] columnInfos; + + /** Iterator over the data to read using this special {@link StarTable} */ + private final TableIterator tableIt; + + /** Limit on the number of rows to read. Over this limit, an "overflow" event occurs and {@link #overflow} is set to TRUE. */ + private final long maxrec; + + /** Indicates whether the maximum allowed number of rows has already been read or not. When true, no more row can be read. */ + private boolean overflow; + + /** Last read row. If NULL, no row has been read or no more row is available. */ + private Object[] row = null; + + /** Number of rows read until now. */ + private int nbRows; + + /** + * Build this special {@link StarTable}. + * + * @param tableIt Data on which to iterate using this special {@link StarTable}. + * @param colInfos Information about all columns. + * @param maxrec Limit on the number of rows to read. (if negative, there will be no limit) + */ + LimitedStarTable(final TableIterator tableIt, final ColumnInfo[] colInfos, final long maxrec){ + this.tableIt = tableIt; + nbCol = colInfos.length; + columnInfos = colInfos; + this.maxrec = maxrec; + overflow = false; + } - datatype = datatype.trim().toLowerCase(); + /** + * Indicates whether the last row sequence dispensed by + * this table's getRowSequence method was truncated at maxrec rows. + * + * @return true if the last row sequence overflowed + */ + public boolean lastSequenceOverflowed(){ + return overflow; + } - if (datatype.equals("short")) - return "" + Short.MIN_VALUE; - else if (datatype.equals("int")) - return "" + Integer.MIN_VALUE; - else if (datatype.equals("long")) - return "" + Long.MIN_VALUE; - else - return null; + /** + * Get the number of rows that have been successfully read until now. + * + * @return Number of all read rows. + */ + public int getNbReadRows(){ + return nbRows; + } + + @Override + public int getColumnCount(){ + return nbCol; + } + + @Override + public ColumnInfo getColumnInfo(final int colInd){ + return columnInfos[colInd]; + } + + @Override + public long getRowCount(){ + return -1; + } + + @Override + public RowSequence getRowSequence() throws IOException{ + overflow = false; + row = new Object[nbCol]; + + return new RowSequence(){ + long irow = -1; + + @Override + public boolean next() throws IOException{ + irow++; + try{ + if (maxrec < 0 || irow < maxrec){ + boolean hasNext = tableIt.nextRow(); + if (hasNext){ + for(int i = 0; i < nbCol && tableIt.hasNextCol(); i++) + row[i] = tableIt.nextCol(); + nbRows++; + }else + row = null; + return hasNext; + }else{ + overflow = tableIt.nextRow(); + row = null; + return false; + } + }catch(DataReadException dre){ + if (dre.getCause() != null && dre.getCause() instanceof IOException) + throw (IOException)(dre.getCause()); + else + throw new IOException(dre); + } + } + + @Override + public Object[] getRow() throws IOException{ + return row; + } + + @Override + public Object getCell(int cellIndex) throws IOException{ + return row[cellIndex]; + } + + @Override + public void close() throws IOException{} + }; + } } } diff --git a/src/tap/log/DefaultTAPLog.java b/src/tap/log/DefaultTAPLog.java index 62b6971..2750fed 100644 --- a/src/tap/log/DefaultTAPLog.java +++ b/src/tap/log/DefaultTAPLog.java @@ -16,151 +16,162 @@ package tap.log; * You should have received a copy of the GNU Lesser General Public License * along with TAPLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.io.OutputStream; import java.io.PrintWriter; +import java.sql.SQLException; +import tap.TAPException; import tap.TAPExecutionReport; +import tap.TAPSyncJob; import tap.db.DBConnection; - -import tap.file.TAPFileManager; - -import tap.metadata.TAPMetadata; -import tap.metadata.TAPTable; - +import tap.parameters.TAPParameters; +import uws.UWSException; +import uws.service.file.UWSFileManager; import uws.service.log.DefaultUWSLog; /** * Default implementation of the {@link TAPLog} interface which lets logging any message about a TAP service. * - * @author Grégory Mantelet (CDS) - * @version 06/2012 + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (04/2015) * * @see DefaultUWSLog */ public class DefaultTAPLog extends DefaultUWSLog implements TAPLog { - public DefaultTAPLog(TAPFileManager fm){ + /** + *

    Builds a {@link TAPLog} which will use the given file + * manager to get the log output (see {@link UWSFileManager#getLogOutput(uws.service.log.UWSLog.LogLevel, String)}).

    + * + *

    note 1: This constructor is particularly useful if the way of managing log output may change in the given file manager. + * Indeed, the output may change in function of the type of message to log ({@link uws.service.log.UWSLog.LogLevel}).

    + * + *

    note 2 If no output can be found in the file manager the standard error output ({@link System#err}) + * will be chosen automatically for all log messages.

    + * + * @param fm A TAP file manager. + * + * @see DefaultUWSLog#DefaultUWSLog(UWSFileManager) + */ + public DefaultTAPLog(final UWSFileManager fm){ super(fm); } - public DefaultTAPLog(OutputStream output){ + /** + *

    Builds a {@link TAPLog} which will print all its + * messages into the given stream.

    + * + *

    note: the given output will be used whatever is the type of message to log ({@link uws.service.log.UWSLog.LogLevel}).

    + * + * @param output An output stream. + * + * @see DefaultUWSLog#DefaultUWSLog(OutputStream) + */ + public DefaultTAPLog(final OutputStream output){ super(output); } - public DefaultTAPLog(PrintWriter writer){ + /** + *

    Builds a {@link TAPLog} which will print all its + * messages into the given stream.

    + * + *

    note: the given output will be used whatever is the type of message to log ({@link uws.service.log.UWSLog.LogLevel}).

    + * + * @param writer A print writer. + * + * @see DefaultUWSLog#DefaultUWSLog(PrintWriter) + */ + public DefaultTAPLog(final PrintWriter writer){ super(writer); } - public void queryFinished(final TAPExecutionReport report){ - StringBuffer buffer = new StringBuffer("QUERY END FOR " + report.jobID + ""); - buffer.append(" - success ? ").append(report.success); - buffer.append(" - synchronous ? ").append(report.synchronous); - buffer.append(" - total duration = ").append(report.getTotalDuration()).append("ms"); - buffer.append(" => upload=").append(report.getUploadDuration()).append("ms"); - buffer.append(", parsing=").append(report.getParsingDuration()).append("ms"); - buffer.append(", translating=").append(report.getTranslationDuration()).append("ms"); - buffer.append(", execution=").append(report.getExecutionDuration()).append("ms"); - buffer.append(", formatting[").append(report.parameters.getFormat()).append("]=").append(report.getFormattingDuration()).append("ms"); - info(buffer.toString()); - } - - public void dbActivity(final String message){ - dbActivity(message, null); - } - - public void dbActivity(final String message, final Throwable t){ - String msgType = (t == null) ? "[INFO] " : "[ERROR] "; - log(DBConnection.LOG_TYPE_DB_ACTIVITY, ((message == null) ? null : (msgType + message)), t); - } - - public void dbInfo(final String message){ - dbActivity(message); - } - - public void dbError(final String message, final Throwable t){ - dbActivity(message, t); - } - - @Override - public void tapMetadataFetched(TAPMetadata metadata){ - dbActivity("TAP metadata fetched from the database !"); - } - - @Override - public void tapMetadataLoaded(TAPMetadata metadata){ - dbActivity("TAP metadata loaded into the database !"); - } - - @Override - public void connectionOpened(DBConnection connection, String dbName){ - //dbActivity("A connection has been opened to the database \""+dbName+"\" !"); - } - - @Override - public void connectionClosed(DBConnection connection){ - //dbActivity("A database connection has been closed !"); - } - - @Override - public void transactionStarted(final DBConnection connection){ - //dbActivity("A transaction has been started !"); - } - - @Override - public void transactionCancelled(final DBConnection connection){ - //dbActivity("A transaction has been cancelled !"); - } - - @Override - public void transactionEnded(final DBConnection connection){ - //dbActivity("A transaction has been ended/commited !"); - } - - @Override - public void schemaCreated(final DBConnection connection, String schema){ - dbActivity("CREATE SCHEMA \"" + schema + "\"\t" + connection.getID()); - } - - @Override - public void schemaDropped(final DBConnection connection, String schema){ - dbActivity("DROP SCHEMA \"" + schema + "\"\t" + connection.getID()); - } - - protected final String getFullDBName(final TAPTable table){ - return (table.getSchema() != null) ? (table.getSchema().getDBName() + ".") : ""; - } - @Override - public void tableCreated(final DBConnection connection, TAPTable table){ - dbActivity("CREATE TABLE \"" + getFullDBName(table) + "\" (ADQL name: \"" + table.getFullName() + "\")\t" + connection.getID()); + protected void printException(Throwable error, final PrintWriter out){ + if (error != null){ + if (error instanceof UWSException || error instanceof TAPException || error.getClass().getPackage().getName().startsWith("adql.")){ + if (error.getCause() != null) + printException(error.getCause(), out); + else{ + out.println("Caused by a " + error.getClass().getName() + " " + getExceptionOrigin(error)); + if (error.getMessage() != null) + out.println("\t" + error.getMessage()); + } + }else if (error instanceof SQLException){ + out.println("Caused by a " + error.getClass().getName() + " " + getExceptionOrigin(error)); + out.print("\t"); + do{ + out.println(error.getMessage()); + error = ((SQLException)error).getNextException(); + if (error != null) + out.print("\t=> "); + }while(error != null); + }else{ + out.print("Caused by a "); + error.printStackTrace(out); + } + } } @Override - public void tableDropped(final DBConnection connection, TAPTable table){ - dbActivity("DROP TABLE \"" + getFullDBName(table) + "\" (ADQL name: \"" + table.getFullName() + "\")\t" + connection.getID()); - } + public void logDB(LogLevel level, final DBConnection connection, final String event, final String message, final Throwable error){ + // If the type is missing: + if (level == null) + level = (error != null) ? LogLevel.ERROR : LogLevel.INFO; - @Override - public void rowsInserted(final DBConnection connection, TAPTable table, int nbInsertedRows){ - dbActivity("INSERT ROWS (" + ((nbInsertedRows > 0) ? nbInsertedRows : "???") + ") into \"" + getFullDBName(table) + "\" (ADQL name: \"" + table.getFullName() + "\")\t" + connection.getID()); - } + // Log or not? + if (!canLog(level)) + return; - @Override - public void sqlQueryExecuting(final DBConnection connection, String sql){ - dbActivity("EXECUTING SQL QUERY \t" + connection.getID() + "\n" + ((sql == null) ? "???" : sql.replaceAll("\n", " ").replaceAll("\t", " ").replaceAll("\r", ""))); - } + // log the main given error: + log(level, "DB", event, (connection != null ? connection.getID() : null), message, null, error); - @Override - public void sqlQueryError(final DBConnection connection, String sql, Throwable t){ - dbActivity("EXECUTION ERROR\t" + connection.getID(), t); + /* Some SQL exceptions (like BatchUpdateException) have a next exception which provides more information. + * Here, the stack trace of the next exception is also logged: + */ + if (error != null && error instanceof SQLException && ((SQLException)error).getNextException() != null){ + PrintWriter out = getOutput(level, "DB"); + out.println("[NEXT EXCEPTION]"); + ((SQLException)error).getNextException().printStackTrace(out); + out.flush(); + } } @Override - public void sqlQueryExecuted(final DBConnection connection, String sql){ - dbActivity("SUCCESSFULL END OF EXECUTION\t" + connection.getID()); + public void logTAP(LogLevel level, final Object obj, final String event, final String message, final Throwable error){ + // If the type is missing: + if (level == null) + level = (error != null) ? LogLevel.ERROR : LogLevel.INFO; + + // Log or not? + if (!canLog(level)) + return; + + // Get more information (when known event and available object): + String jobId = null, msgAppend = null; + try{ + if (event != null && obj != null){ + if (event.equals("SYNC_INIT")) + msgAppend = "QUERY=" + ((TAPParameters)obj).getQuery(); + else if (obj instanceof TAPSyncJob){ + log(level, "JOB", event, ((TAPSyncJob)obj).getID(), message, null, error); + return; + }else if (obj instanceof TAPExecutionReport){ + TAPExecutionReport report = (TAPExecutionReport)obj; + jobId = report.jobID; + msgAppend = (report.synchronous ? "SYNC" : "ASYNC") + ",duration=" + report.getTotalDuration() + "ms (upload=" + report.getUploadDuration() + ",parse=" + report.getParsingDuration() + ",exec=" + report.getExecutionDuration() + ",format[" + report.parameters.getFormat() + "]=" + report.getFormattingDuration() + ")"; + }else if (event.equalsIgnoreCase("WRITING_ERROR")) + jobId = obj.toString(); + } + }catch(Throwable t){ + error("Error while preparing a log message in logTAP(...)! The message will be logger but without additional information such as the job ID.", t); + } + + // Log the message: + log(level, "TAP", event, jobId, message, msgAppend, error); } } diff --git a/src/tap/log/TAPLog.java b/src/tap/log/TAPLog.java index 04439aa..288eac7 100644 --- a/src/tap/log/TAPLog.java +++ b/src/tap/log/TAPLog.java @@ -16,59 +16,93 @@ package tap.log; * You should have received a copy of the GNU Lesser General Public License * along with TAPLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import tap.TAPExecutionReport; +import tap.TAPSyncJob; import tap.db.DBConnection; - -import tap.metadata.TAPMetadata; -import tap.metadata.TAPTable; - +import tap.parameters.TAPParameters; import uws.service.log.UWSLog; /** - * Lets logging any kind of message about a TAP service. + * Let log any kind of message about a TAP service. * - * @author Grégory Mantelet (CDS) - * @version 06/2012 + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (04/2015) */ public interface TAPLog extends UWSLog { - public void queryFinished(final TAPExecutionReport report); - - public void dbInfo(final String message); - - public void dbError(final String message, final Throwable t); - - public void tapMetadataFetched(final TAPMetadata metadata); - - public void tapMetadataLoaded(final TAPMetadata metadata); - - public void connectionOpened(final DBConnection connection, final String dbName); - - public void connectionClosed(final DBConnection connection); - - public void transactionStarted(final DBConnection connection); - - public void transactionCancelled(final DBConnection connection); - - public void transactionEnded(final DBConnection connection); - - public void schemaCreated(final DBConnection connection, final String schema); - - public void schemaDropped(final DBConnection connection, final String schema); - - public void tableCreated(final DBConnection connection, final TAPTable table); - - public void tableDropped(final DBConnection connection, final TAPTable table); - - public void rowsInserted(final DBConnection connection, final TAPTable table, final int nbInsertedRows); - - public void sqlQueryExecuting(final DBConnection connection, final String sql); - - public void sqlQueryError(final DBConnection connection, final String sql, final Throwable t); - - public void sqlQueryExecuted(final DBConnection connection, final String sql); + /** + *

    Log a message and/or an error in the DB (database) context.

    + * + *

    List of all events sent by the library (case sensitive):

    + *
      + *
    • CONNECTION_LACK
    • + *
    • TRANSLATE
    • + *
    • EXECUTE
    • + *
    • RESULT
    • + *
    • LOAD_TAP_SCHEMA
    • + *
    • CLEAN_TAP_SCHEMA
    • + *
    • CREATE_TAP_SCHEMA
    • + *
    • TABLE_EXIST
    • + *
    • COLUMN_EXIST
    • + *
    • EXEC_UPDATE
    • + *
    • ADD_UPLOAD_TABLE
    • + *
    • DROP_UPLOAD_TABLE
    • + *
    • START_TRANSACTION
    • + *
    • COMMIT
    • + *
    • ROLLBACK
    • + *
    • END_TRANSACTION
    • + *
    • CLOSE
    • + *
    + * + * @param level Level of the log (info, warning, error, ...). SHOULD NOT be NULL, but if NULL anyway, the level SHOULD be considered as INFO + * @param connection DB connection from which this log comes. MAY be NULL + * @param event Event at the origin of this log or action executed by the given database connection while this log is sent. MAY be NULL + * @param message Message to log. MAY be NULL + * @param error Error/Exception to log. MAY be NULL + * + * @since 2.0 + */ + public void logDB(final LogLevel level, final DBConnection connection, final String event, final String message, final Throwable error); + + /** + *

    Log a message and/or an error in the general context of TAP.

    + * + *

    + * One of the parameter is of type {@link Object}. This object can be used to provide more information to the log function + * in order to describe as much as possible the state and/or result event. + *

    + * + *

    List of all events sent by the library (case sensitive):

    + *
      + *
    • IDENT_USER (with a NULL "obj")
    • + *
    • SYNC_INIT (with "obj" as an instance of {@link TAPParameters})
    • + *
    • ASYNC_INIT (with a NULL "obj")
    • + *
    • START (with "obj" as an instance of {@link TAPSyncJob})
    • + *
    • UPLOADING (with "obj" as an instance of {@link TAPExecutionReport})
    • + *
    • PARSING (with "obj" as an instance of {@link TAPExecutionReport})
    • + *
    • START_DB_EXECUTION (with "obj" as an instance of {@link TAPExecutionReport})
    • + *
    • WRITING_RESULT (with "obj" as an instance of {@link TAPExecutionReport})
    • + *
    • RESULT_WRITTEN (with "obj" as an instance of {@link TAPExecutionReport})
    • + *
    • START_STEP (with "obj" as an instance of {@link TAPExecutionReport})
    • + *
    • END_EXEC (with "obj" as an instance of {@link TAPExecutionReport})
    • + *
    • END_DB_EXECUTION (with "obj" as an instance of {@link TAPExecutionReport})
    • + *
    • DROP_UPLOAD (with "obj" as an instance of {@link TAPExecutionReport})
    • + *
    • TIME_OUT (with "obj" as an instance of {@link TAPSyncJob})
    • + *
    • END (with "obj" as an instance of {@link TAPSyncJob})
    • + *
    + * + * @param level Level of the log (info, warning, error, ...). SHOULD NOT be NULL, but if NULL anyway, the level SHOULD be considered as INFO + * @param obj Object providing more information about the event/object at the origin of this log. MAY be NULL + * @param event Event at the origin of this log or action currently executed by TAP while this log is sent. MAY be NULL + * @param message Message to log. MAY be NULL + * @param error Error/Exception to log. MAY be NULL + * + * @since 2.0 + */ + public void logTAP(final LogLevel level, final Object obj, final String event, final String message, final Throwable error); } diff --git a/src/tap/metadata/TAPColumn.java b/src/tap/metadata/TAPColumn.java index 94b69da..30d4d1d 100644 --- a/src/tap/metadata/TAPColumn.java +++ b/src/tap/metadata/TAPColumn.java @@ -16,49 +16,142 @@ package tap.metadata; * You should have received a copy of the GNU Lesser General Public License * along with TAPLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ +import java.awt.List; import java.util.ArrayList; import java.util.Iterator; +import java.util.Map; import adql.db.DBColumn; import adql.db.DBTable; +import adql.db.DBType; +import adql.db.DBType.DBDatatype; +/** + *

    Represent a column as described by the IVOA standard in the TAP protocol definition.

    + * + *

    + * This object representation has exactly the same fields as the column of the table TAP_SCHEMA.columns. + * But it also provides a way to add other data. For instance, if information not listed in the standard + * may be stored here, they can be using the function {@link #setOtherData(Object)}. This object can be + * a single value (integer, string, ...), but also a {@link Map}, {@link List}, etc... + *

    + * + *

    Important note: + * A {@link TAPColumn} object MUST always have a DB name. That's why by default, at the creation + * the DB name is the ADQL name. Once created, it is possible to set the DB name with {@link #setDBName(String)}. + * This DB name MUST be UNqualified and without double quotes. If a NULL or empty value is provided, + * nothing is done and the object keeps its former DB name. + *

    + * + *

    Set a table

    + * + *

    + * By default a column is detached (not part of a table). To specify the table in which this column is, + * you must use {@link TAPTable#addColumn(TAPColumn)}. By doing this, the table link inside this column + * will be set automatically and you will be able to get the table with {@link #getTable()}. + *

    + * + *

    Foreign keys

    + * + *

    + * In case this column is linked to one or several of other tables, it will be possible to list all + * foreign keys where the target columns is with {@link #getTargets()}. In the same way, it will be + * possible to list all foreign keys in which this column is a target with {@link #getSources()}. + * However, in order to ensure the consistency between all metadata, these foreign key's links are + * set at the table level by the table itself using {@link #addSource(TAPForeignKey)} and + * {@link #addTarget(TAPForeignKey)}. + *

    + * + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (02/2015) + */ public class TAPColumn implements DBColumn { + /** Name that this column MUST have in ADQL queries. */ private final String adqlName; + /** Name that this column have in the database. + * Note: It CAN NOT be NULL. By default, it is the ADQL name. */ private String dbName = null; + /** Table which owns this column. + * Note: It should be NULL only at the construction or for a quick representation of a column. + * Then, this attribute is automatically set by a {@link TAPTable} when adding this column inside it + * with {@link TAPTable#addColumn(TAPColumn)}. */ private DBTable table = null; + /** Description of this column. + * Note: Standard TAP column field ; MAY be NULL. */ private String description = null; + /** Unit of this column's values. + * Note: Standard TAP column field ; MAY be NULL. */ private String unit = null; + /** UCD describing the scientific content of this column. + * Note: Standard TAP column field ; MAY be NULL. */ private String ucd = null; + /** UType associating this column with a data-model. + * Note: Standard TAP column field ; MAY be NULL. */ private String utype = null; - private String datatype = null; - - private int size = TAPTypes.NO_SIZE; - - private VotType votType = null; + /** Type of this column. + * Note: Standard TAP column field ; CAN'T be NULL. */ + private DBType datatype = new DBType(DBDatatype.VARCHAR); + /** Flag indicating whether this column is one of those that should be returned by default. + * Note: Standard TAP column field ; FALSE by default. */ private boolean principal = false; + /** Flag indicating whether this column is indexed in the database. + * Note: Standard TAP column field ; FALSE by default. */ private boolean indexed = false; + /** Flag indicating whether this column can be set to NULL in the database. + * Note: Standard TAP column field ; FALSE by default. + * @since 2.0 */ + private boolean nullable = false; + + /** Flag indicating whether this column is defined by a standard. + * Note: Standard TAP column field ; FALSE by default. */ private boolean std = false; + /** Let add some information in addition of the ones of the TAP protocol. + * Note: This object can be anything: an {@link Integer}, a {@link String}, a {@link Map}, a {@link List}, ... + * Its content is totally free and never used or checked. */ protected Object otherData = null; + /** List all foreign keys in which this column is a source. + *

    CAUTION: For consistency consideration, this attribute SHOULD never be modified! + * It is set by the constructor and filled ONLY by the table.

    */ protected final ArrayList lstTargets; + /** List all foreign keys in which this column is a target. + *

    CAUTION: For consistency consideration, this attribute SHOULD never be modified! + * It is set by the constructor and filled ONLY by the table.

    */ protected final ArrayList lstSources; + /** + *

    Build a VARCHAR {@link TAPColumn} instance with the given ADQL name.

    + * + *

    Note: + * The DB name is set by default with the ADQL name. To set the DB name, + * you MUST call then {@link #setDBName(String)}. + * The datatype is set by default to VARCHAR. + *

    + * + *

    Note: + * If the given ADQL name is prefixed (= it has some text separated by a '.' before the column name), + * this prefix will be removed. Only the part after the '.' character will be kept. + *

    + * + * @param columnName Name that this column MUST have in ADQL queries. CAN'T be NULL ; this name can never be changed after. + */ public TAPColumn(String columnName){ if (columnName == null || columnName.trim().length() == 0) throw new NullPointerException("Missing column name !"); @@ -67,28 +160,199 @@ public class TAPColumn implements DBColumn { dbName = adqlName; lstTargets = new ArrayList(1); lstSources = new ArrayList(1); - setDefaultType(); } - public TAPColumn(String columnName, String description){ + /** + *

    Build a {@link TAPColumn} instance with the given ADQL name and datatype.

    + * + *

    Note: + * The DB name is set by default with the ADQL name. To set the DB name, + * you MUST call then {@link #setDBName(String)}. + *

    + * + *

    Note: + * If the given ADQL name is prefixed (= it has some text separated by a '.' before the column name), + * this prefix will be removed. Only the part after the '.' character will be kept. + *

    + * + *

    Note: + * The datatype is set by calling the function {@link #setDatatype(DBType)} which does not do + * anything if the given datatype is NULL. + *

    + * + * @param columnName Name that this column MUST have in ADQL queries. CAN'T be NULL ; this name can never be changed after. + * @param type Datatype of this column. If NULL, VARCHAR will be the datatype of this column + * + * @see #setDatatype(DBType) + */ + public TAPColumn(String columnName, DBType type){ this(columnName); + setDatatype(type); + } + + /** + *

    Build a VARCHAR {@link TAPColumn} instance with the given ADQL name and description.

    + * + *

    Note: + * The DB name is set by default with the ADQL name. To set the DB name, + * you MUST call then {@link #setDBName(String)}. + *

    + * + *

    Note: + * If the given ADQL name is prefixed (= it has some text separated by a '.' before the column name), + * this prefix will be removed. Only the part after the '.' character will be kept. + *

    + * + * @param columnName Name that this column MUST have in ADQL queries. CAN'T be NULL ; this name can never be changed after. + * @param description Description of the column's content. May be NULL + */ + public TAPColumn(String columnName, String description){ + this(columnName, (DBType)null, description); + } + + /** + *

    Build a {@link TAPColumn} instance with the given ADQL name, datatype and description.

    + * + *

    Note: + * The DB name is set by default with the ADQL name. To set the DB name, + * you MUST call then {@link #setDBName(String)}. + *

    + * + *

    Note: + * If the given ADQL name is prefixed (= it has some text separated by a '.' before the column name), + * this prefix will be removed. Only the part after the '.' character will be kept. + *

    + * + *

    Note: + * The datatype is set by calling the function {@link #setDatatype(DBType)} which does do + * anything if the given datatype is NULL. + *

    + * + * @param columnName Name that this column MUST have in ADQL queries. CAN'T be NULL ; this name can never be changed after. + * @param type Datatype of this column. If NULL, VARCHAR will be the datatype of this column + * @param description Description of the column's content. May be NULL + */ + public TAPColumn(String columnName, DBType type, String description){ + this(columnName, type); this.description = description; } + /** + *

    Build a VARCHAR {@link TAPColumn} instance with the given ADQL name, description and unit.

    + * + *

    Note: + * The DB name is set by default with the ADQL name. To set the DB name, + * you MUST call then {@link #setDBName(String)}. + *

    + * + *

    Note: + * If the given ADQL name is prefixed (= it has some text separated by a '.' before the column name), + * this prefix will be removed. Only the part after the '.' character will be kept. + *

    + * + * @param columnName Name that this column MUST have in ADQL queries. CAN'T be NULL ; this name can never be changed after. + * @param description Description of the column's content. May be NULL + * @param unit Unit of the column's values. May be NULL + */ public TAPColumn(String columnName, String description, String unit){ - this(columnName, description); + this(columnName, null, description, unit); + } + + /** + *

    Build a {@link TAPColumn} instance with the given ADQL name, type, description and unit.

    + * + *

    Note: + * The DB name is set by default with the ADQL name. To set the DB name, + * you MUST call then {@link #setDBName(String)}. + *

    + * + *

    Note: + * If the given ADQL name is prefixed (= it has some text separated by a '.' before the column name), + * this prefix will be removed. Only the part after the '.' character will be kept. + *

    + * + *

    Note: + * The datatype is set by calling the function {@link #setDatatype(DBType)} which does do + * anything if the given datatype is NULL. + *

    + * + * @param columnName Name that this column MUST have in ADQL queries. CAN'T be NULL ; this name can never be changed after. + * @param type Datatype of this column. If NULL, VARCHAR will be the datatype of this column + * @param description Description of the column's content. May be NULL + * @param unit Unit of the column's values. May be NULL + */ + public TAPColumn(String columnName, DBType type, String description, String unit){ + this(columnName, type, description); this.unit = unit; } + /** + *

    Build a VARCHAR {@link TAPColumn} instance with the given fields.

    + * + *

    Note: + * The DB name is set by default with the ADQL name. To set the DB name, + * you MUST call then {@link #setDBName(String)}. + *

    + * + *

    Note: + * If the given ADQL name is prefixed (= it has some text separated by a '.' before the column name), + * this prefix will be removed. Only the part after the '.' character will be kept. + *

    + * + *

    Note: + * The datatype is set by calling the function {@link #setDatatype(DBType)} which does do + * anything if the given datatype is NULL. + *

    + * + * @param columnName Name that this column MUST have in ADQL queries. CAN'T be NULL ; this name can never be changed after. + * @param description Description of the column's content. May be NULL + * @param unit Unit of the column's values. May be NULL + * @param ucd UCD describing the scientific content of this column. + * @param utype UType associating this column with a data-model. + */ public TAPColumn(String columnName, String description, String unit, String ucd, String utype){ - this(columnName, description, unit); + this(columnName, null, description, unit, ucd, utype); + } + + /** + *

    Build a {@link TAPColumn} instance with the given fields.

    + * + *

    Note: + * The DB name is set by default with the ADQL name. To set the DB name, + * you MUST call then {@link #setDBName(String)}. + *

    + * + *

    Note: + * If the given ADQL name is prefixed (= it has some text separated by a '.' before the column name), + * this prefix will be removed. Only the part after the '.' character will be kept. + *

    + * + *

    Note: + * The datatype is set by calling the function {@link #setDatatype(DBType)} which does do + * anything if the given datatype is NULL. + *

    + * + * @param columnName Name that this column MUST have in ADQL queries. CAN'T be NULL ; this name can never be changed after. + * @param type Datatype of this column. If NULL, VARCHAR will be the datatype of this column + * @param description Description of the column's content. May be NULL + * @param unit Unit of the column's values. May be NULL + * @param ucd UCD describing the scientific content of this column. + * @param utype UType associating this column with a data-model. + */ + public TAPColumn(String columnName, DBType type, String description, String unit, String ucd, String utype){ + this(columnName, type, description, unit); this.ucd = ucd; this.utype = utype; } /** - * @return The name. + * Get the ADQL name (the name this column MUST have in ADQL queries). + * + * @return Its ADQL name. + * @see #getADQLName() + * @deprecated Does not do anything special: just call {@link #getADQLName()}. */ + @Deprecated public final String getName(){ return getADQLName(); } @@ -103,255 +367,397 @@ public class TAPColumn implements DBColumn { return dbName; } + /** + *

    Change the name that this column MUST have in the database (i.e. in SQL queries).

    + * + *

    Note: + * If the given value is NULL or an empty string, nothing is done ; the DB name keeps is former value. + *

    + * + * @param name The new database name of this column. + */ public final void setDBName(String name){ name = (name != null) ? name.trim() : name; - dbName = (name == null || name.length() == 0) ? adqlName : name; + if (name != null && name.length() > 0) + dbName = name; } - /** - * @return The table. - */ + @Override public final DBTable getTable(){ return table; } /** - * @param table The table to set. + *

    Set the table in which this column is.

    + * + *

    Warning: + * For consistency reasons, this function SHOULD be called only by the {@link TAPTable} + * that owns this column. + *

    + * + *

    Important note: + * If this column was already linked with another {@link TAPTable} object, the previous link is removed + * here, but also in the table (by calling {@link TAPTable#removeColumn(String)}). + *

    + * + * @param table The table that owns this column. */ - public final void setTable(DBTable table){ + protected final void setTable(final DBTable table){ + if (this.table != null && this.table instanceof TAPTable && (table == null || !table.equals(this.table))) + ((TAPTable)this.table).removeColumn(adqlName); this.table = table; } /** - * @return The description. + * Get the description of this column. + * + * @return Its description. MAY be NULL */ public final String getDescription(){ return description; } /** - * @param description The description to set. + * Set the description of this column. + * + * @param description Its new description. MAY be NULL */ public final void setDescription(String description){ this.description = description; } /** - * @return The unit. + * Get the unit of the column's values. + * + * @return Its unit. MAY be NULL */ public final String getUnit(){ return unit; } /** - * @param unit The unit to set. + * Set the unit of the column's values. + * + * @param unit Its new unit. MAY be NULL */ public final void setUnit(String unit){ this.unit = unit; } /** - * @return The ucd. + * Get the UCD describing the scientific content of this column. + * + * @return Its UCD. MAY be NULL */ public final String getUcd(){ return ucd; } /** - * @param ucd The ucd to set. + * Set the UCD describing the scientific content of this column. + * + * @param ucd Its new UCD. MAY be NULL */ public final void setUcd(String ucd){ this.ucd = ucd; } /** - * @return The utype. + * Get the UType associating this column with a data-model. + * + * @return Its UType. MAY be NULL */ public final String getUtype(){ return utype; } /** - * @param utype The utype to set. + * Set the UType associating this column with a data-model. + * + * @param utype Its new UType. MAY be NULL */ public final void setUtype(String utype){ this.utype = utype; } /** - * @return The datatype. + * Get the type of the column's values. + * + * @return Its datatype. CAN'T be NULL */ - public final String getDatatype(){ + @Override + public final DBType getDatatype(){ return datatype; } /** - * @return Array size (>0 or 2 special values: {@link TAPTypes#NO_SIZE} and {@link TAPTypes#STAR_SIZE}). - */ - public final int getArraySize(){ - return size; - } - - /** - *

    Sets the DB datatype, the size and uses these information to set the corresponding VOTable type.

    - * Important: - *
      - *
    • If the given datatype is not known according to {@link TAPTypes#getDBType(String)}, the datatype of this column is set to its default value (see {@link #setDefaultType()}),
    • - *
    • The VOTable type is set automatically thanks to {@link TAPTypes#getVotType(String, int)}.
    • - *
    + *

    Set the type of the column's values.

    * - * @param datatype The datatype to set. - * @param size Array size (>0 or 2 special values: {@link TAPTypes#NO_SIZE} and {@link TAPTypes#STAR_SIZE}). + *

    Note: + * The datatype won't be changed, if the given type is NULL. + *

    * - * @see TAPTypes#getDBType(VotType) - * @see TAPTypes#getVotType(String, int) - * @see #setDefaultType() - */ - public final void setDatatype(String datatype, int size){ - this.datatype = TAPTypes.getDBType(datatype); - this.size = (size <= 0 && size != TAPTypes.STAR_SIZE) ? TAPTypes.NO_SIZE : size; - - if (this.datatype == null) - setDefaultType(); - else - this.votType = TAPTypes.getVotType(this.datatype, this.size); - } - - /** - * @return The VOTable type to use. + * @param type Its new datatype. */ - public final VotType getVotType(){ - return votType; + public final void setDatatype(final DBType type){ + if (type != null) + datatype = type; } /** - *

    Sets the VOTable type and uses it to set the DB datatype and its size.

    - * Important: - *
      - *
    • If the given VOTable type is not known according to {@link TAPTypes#getDBType(VotType)}, the DB datatype of this column and its size are set to the default value (see {@link #setDefaultType()}).
    • - *
    - * - * @param type A full VOTable type (that's to say: datatype, arraysize and xtype). + * Tell whether this column is one of those returned by default. * - * @see TAPTypes#getDBType(VotType) - * @see #setDefaultType() - */ - public final void setVotType(final VotType type){ - this.votType = type; - this.datatype = TAPTypes.getDBType(type); - this.size = type.arraysize; - - if (this.datatype == null) - setDefaultType(); - } - - /** - * Sets the default DB datatype (VARCHAR) and its corresponding VOTable type (char , *). - */ - protected final void setDefaultType(){ - datatype = TAPTypes.VARCHAR; - size = TAPTypes.STAR_SIZE; - votType = TAPTypes.getVotType(datatype, size); - } - - /** - * @return The principal. + * @return true if this column should be returned by default, false otherwise. */ public final boolean isPrincipal(){ return principal; } /** - * @param principal The principal to set. + * Set whether this column should be one of those returned by default. + * + * @param principal true if this column should be returned by default, false otherwise. */ public final void setPrincipal(boolean principal){ this.principal = principal; } /** - * @return The indexed. + * Tell whether this column is indexed. + * + * @return true if this column is indexed, false otherwise. */ public final boolean isIndexed(){ return indexed; } /** - * @param indexed The indexed to set. + * Set whether this column is indexed or not. + * + * @param indexed true if this column is indexed, false otherwise. */ public final void setIndexed(boolean indexed){ this.indexed = indexed; } /** - * @return The std. + * Tell whether this column is nullable. + * + * @return true if this column is nullable, false otherwise. + * + * @since 2.0 + */ + public final boolean isNullable(){ + return nullable; + } + + /** + * Set whether this column is nullable or not. + * + * @param nullable true if this column is nullable, false otherwise. + * + * @since 2.0 + */ + public final void setNullable(boolean nullable){ + this.nullable = nullable; + } + + /** + * Tell whether this column is defined by a standard. + * + * @return true if this column is defined by a standard, false otherwise. */ public final boolean isStd(){ return std; } /** - * @param std The std to set. + * Set whether this column is defined by a standard. + * + * @param std true if this column is defined by a standard, false otherwise. */ public final void setStd(boolean std){ this.std = std; } + /** + *

    Get the other (piece of) information associated with this column.

    + * + *

    Note: + * By default, NULL is returned, but it may be any kind of value ({@link Integer}, + * {@link String}, {@link Map}, {@link List}, ...). + *

    + * + * @return The other (piece of) information. MAY be NULL + */ public Object getOtherData(){ return otherData; } + /** + * Set the other (piece of) information associated with this column. + * + * @param data Another information about this column. MAY be NULL + */ public void setOtherData(Object data){ otherData = data; } + /** + *

    Let add a foreign key in which this column is a source (= which is targeting another column).

    + * + *

    Note: + * Nothing is done if the given value is NULL. + *

    + * + *

    Warning: + * For consistency reasons, this function SHOULD be called only by the {@link TAPTable} + * that owns this column or that is part of the foreign key. + *

    + * + * @param key A foreign key. + */ protected void addTarget(TAPForeignKey key){ if (key != null) lstTargets.add(key); } - protected int getNbTargets(){ + /** + * Get the number of times this column is targeting another column. + * + * @return How many this column is source in a foreign key. + */ + public int getNbTargets(){ return lstTargets.size(); } - protected Iterator getTargets(){ + /** + * Get the list of foreign keys in which this column is a source (= is targeting another column). + * + * @return List of foreign keys in which this column is a source. + */ + public Iterator getTargets(){ return lstTargets.iterator(); } + /** + *

    Remove the fact that this column is a source (= is targeting another column) + * in the given foreign key.

    + * + *

    Note: + * Nothing is done if the given value is NULL. + *

    + * + *

    Warning: + * For consistency reasons, this function SHOULD be called only by the {@link TAPTable} + * that owns this column or that is part of the foreign key. + *

    + * + * @param key Foreign key in which this column was targeting another column. + */ protected void removeTarget(TAPForeignKey key){ - lstTargets.remove(key); + if (key != null) + lstTargets.remove(key); } + /** + *

    Remove the fact that this column is a source (= is targeting another column) + * in any foreign key in which it was.

    + * + *

    Warning: + * For consistency reasons, this function SHOULD be called only by the {@link TAPTable} + * that owns this column or that is part of the foreign key. + *

    + */ protected void removeAllTargets(){ lstTargets.clear(); } + /** + *

    Let add a foreign key in which this column is a target (= which is targeted by another column).

    + * + *

    Note: + * Nothing is done if the given value is NULL. + *

    + * + *

    Warning: + * For consistency reasons, this function SHOULD be called only by the {@link TAPTable} + * that owns this column or that is part of the foreign key. + *

    + * + * @param key A foreign key. + */ protected void addSource(TAPForeignKey key){ if (key != null) lstSources.add(key); } - protected int getNbSources(){ + /** + * Get the number of times this column is targeted by another column. + * + * @return How many this column is target in a foreign key. + */ + public int getNbSources(){ return lstSources.size(); } - protected Iterator getSources(){ + /** + * Get the list of foreign keys in which this column is a target (= is targeted another column). + * + * @return List of foreign keys in which this column is a target. + */ + public Iterator getSources(){ return lstSources.iterator(); } + /** + *

    Remove the fact that this column is a target (= is targeted by another column) + * in the given foreign key.

    + * + *

    Note: + * Nothing is done if the given value is NULL. + *

    + * + *

    Warning: + * For consistency reasons, this function SHOULD be called only by the {@link TAPTable} + * that owns this column or that is part of the foreign key. + *

    + * + * @param key Foreign key in which this column was targeted by another column. + */ protected void removeSource(TAPForeignKey key){ lstSources.remove(key); } + /** + *

    Remove the fact that this column is a target (= is targeted by another column) + * in any foreign key in which it was.

    + * + *

    Warning: + * For consistency reasons, this function SHOULD be called only by the {@link TAPTable} + * that owns this column or that is part of the foreign key. + *

    + */ protected void removeAllSources(){ lstSources.clear(); } + /** + *

    Warning: + * Since the type of the other data is not known, the copy of its value + * can not be done properly. So, this column and its copy will share the same other data object. + * If it is also needed to make a deep copy of this other data object, this function MUST be + * overridden. + * + * + * @see adql.db.DBColumn#copy(java.lang.String, java.lang.String, adql.db.DBTable) + */ + @Override public DBColumn copy(final String dbName, final String adqlName, final DBTable dbTable){ - TAPColumn copy = new TAPColumn((adqlName == null) ? this.adqlName : adqlName, description, unit, ucd, utype); + TAPColumn copy = new TAPColumn((adqlName == null) ? this.adqlName : adqlName, datatype, description, unit, ucd, utype); copy.setDBName((dbName == null) ? this.dbName : dbName); copy.setTable(dbTable); - copy.setDatatype(datatype, size); copy.setIndexed(indexed); copy.setPrincipal(principal); copy.setStd(std); @@ -360,11 +766,22 @@ public class TAPColumn implements DBColumn { return copy; } + /** + *

    Provide a deep copy (included the other data) of this column.

    + * + *

    Warning: + * Since the type of the other data is not known, the copy of its value + * can not be done properly. So, this column and its copy will share the same other data object. + * If it is also needed to make a deep copy of this other data object, this function MUST be + * overridden. + * + * + * @return The deep copy of this column. + */ public DBColumn copy(){ - TAPColumn copy = new TAPColumn(adqlName, description, unit, ucd, utype); + TAPColumn copy = new TAPColumn(adqlName, datatype, description, unit, ucd, utype); copy.setDBName(dbName); copy.setTable(table); - copy.setDatatype(datatype, size); copy.setIndexed(indexed); copy.setPrincipal(principal); copy.setStd(std); @@ -378,7 +795,7 @@ public class TAPColumn implements DBColumn { return false; TAPColumn col = (TAPColumn)obj; - return col.getTable().equals(table) && col.getName().equals(adqlName); + return col.getTable().equals(table) && col.getADQLName().equals(adqlName); } @Override diff --git a/src/tap/metadata/TAPMetadata.java b/src/tap/metadata/TAPMetadata.java index 9dda1b9..580b966 100644 --- a/src/tap/metadata/TAPMetadata.java +++ b/src/tap/metadata/TAPMetadata.java @@ -16,47 +16,127 @@ package tap.metadata; * You should have received a copy of the GNU Lesser General Public License * along with TAPLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.io.IOException; import java.io.PrintWriter; - import java.util.ArrayList; -import java.util.HashMap; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.Map; import java.util.NoSuchElementException; import javax.servlet.ServletConfig; import javax.servlet.ServletException; - import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import adql.db.DBTable; +import tap.metadata.TAPTable.TableType; import tap.resource.Capabilities; import tap.resource.TAPResource; import tap.resource.VOSIResource; +import uk.ac.starlink.votable.VOSerializer; +import uws.ClientAbortException; +import uws.UWSToolBox; +import adql.db.DBTable; +import adql.db.DBType; +import adql.db.DBType.DBDatatype; +/** + *

    Let listing all schemas, tables and columns available in a TAP service. + * This list also corresponds to the TAP resource "/tables".

    + * + *

    + * Only schemas are stored in this object. So that's why only schemas can be added and removed + * from this class. However, {@link TAPSchema} objects are listing tables, whose the object + * representation is listing columns. So to add tables, you must first embed them in a schema. + *

    + * + *

    + * All metadata have two names: one to use in ADQL queries and the other to use when really querying + * the database. This is very useful to hide the real complexity of the database and propose + * a simpler view of the query-able data. It is particularly useful if a schema does not exist in the + * database but has been added in the TAP schema for more logical separation on the user point of view. + * In a such case, the schema would have an ADQL name but no DB name (NULL value ; which is possible only + * with {@link TAPSchema} objects). + *

    + * + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (03/2015) + */ public class TAPMetadata implements Iterable, VOSIResource, TAPResource { + /** Resource name of the TAP metadata. This name is also used - in this class - in the TAP URL to identify this resource. + * Here it corresponds to the following URI: ".../tables". */ public static final String RESOURCE_NAME = "tables"; + /** List of all schemas available through the TAP service. */ protected final Map schemas; + + /** Part of the TAP URI which identify this TAP resource. + * By default, it is the resource name ; so here, the corresponding TAP URI would be: "/tables". */ protected String accessURL = getName(); + /** + *

    Build an empty list of metadata.

    + * + *

    Note: + * By default, a TAP service must have at least a TAP_SCHEMA schema which contains a set of 5 tables + * (schemas, tables, columns, keys and key_columns). This schema is not created here by default + * because it can be customized by the service implementor. Besides, the DB name may be different. + * However, you can easily get this schema thanks to the function {@link #getStdSchema(boolean)} + * which returns the standard definition of this schema (including all tables and columns described + * by the standard). For a standard definition of this schema, you can then write the following: + *

    + *
    +	 * TAPMetadata meta = new TAPMetadata();
    +	 * meta.addSchema(TAPMetadata.getStdSchema());
    +	 * 
    + *

    + * Of course, this schema (and its tables and their columns) can be customized after if needed. + * Otherwise, if you want customize just some part of this schema, you can also use the function + * {@link #getStdTable(STDTable)} to get just the standard definition of some of its tables, either + * to customize them or to merely get them and keep them like they are. + *

    + */ public TAPMetadata(){ - schemas = new HashMap(); + schemas = new LinkedHashMap(); } + /** + *

    Add the given schema inside this TAP metadata set.

    + * + *

    Note: + * If the given schema is NULL, nothing will be done. + *

    + * + * @param s The schema to add. + */ public final void addSchema(TAPSchema s){ - if (s != null && s.getName() != null) - schemas.put(s.getName(), s); + if (s != null && s.getADQLName() != null) + schemas.put(s.getADQLName(), s); } + /** + *

    Build a new {@link TAPSchema} object with the given ADQL name. + * Then, add it inside this TAP metadata set.

    + * + *

    Note: + * The built {@link TAPSchema} object is returned, so that being modified afterwards if needed. + *

    + * + * @param schemaName ADQL name of the schema to create and add inside this TAP metadata set. + * + * @return The created and added schema, + * or NULL if the given schema is NULL or an empty string. + * + * @see TAPSchema#TAPSchema(String) + * @see #addSchema(TAPSchema) + */ public TAPSchema addSchema(String schemaName){ - if (schemaName == null) + if (schemaName == null || schemaName.trim().length() <= 0) return null; TAPSchema s = new TAPSchema(schemaName); @@ -64,6 +144,24 @@ public class TAPMetadata implements Iterable, VOSIResource, TAPResour return s; } + /** + *

    Build a new {@link TAPSchema} object with the given ADQL name. + * Then, add it inside this TAP metadata set.

    + * + *

    Note: + * The built {@link TAPSchema} object is returned, so that being modified afterwards if needed. + *

    + * + * @param schemaName ADQL name of the schema to create and add inside this TAP metadata set. + * @param description Description of the new schema. MAY be NULL + * @param utype UType associating the new schema with a data-model. MAY be NULL + * + * @return The created and added schema, + * or NULL if the given schema is NULL or an empty string. + * + * @see TAPSchema#TAPSchema(String, String, String) + * @see #addSchema(TAPSchema) + */ public TAPSchema addSchema(String schemaName, String description, String utype){ if (schemaName == null) return null; @@ -73,6 +171,17 @@ public class TAPMetadata implements Iterable, VOSIResource, TAPResour return s; } + /** + *

    Tell whether there is a schema with the given ADQL name.

    + * + *

    Important note: + * This function is case sensitive! + *

    + * + * @param schemaName ADQL name of the schema whose the existence must be checked. + * + * @return true if a schema with the given ADQL name exists, false otherwise. + */ public final boolean hasSchema(String schemaName){ if (schemaName == null) return false; @@ -80,6 +189,18 @@ public class TAPMetadata implements Iterable, VOSIResource, TAPResour return schemas.containsKey(schemaName); } + /** + *

    Search for a schema having the given ADQL name.

    + * + *

    Important note: + * This function is case sensitive! + *

    + * + * @param schemaName ADQL name of the schema to search. + * + * @return The schema having the given ADQL name, + * or NULL if no such schema can be found. + */ public final TAPSchema getSchema(String schemaName){ if (schemaName == null) return null; @@ -87,14 +208,44 @@ public class TAPMetadata implements Iterable, VOSIResource, TAPResour return schemas.get(schemaName); } + /** + * Get the number of schemas contained in this TAP metadata set. + * + * @return Number of all schemas. + */ public final int getNbSchemas(){ return schemas.size(); } + /** + * Tell whether this TAP metadata set contains no schema. + * + * @return true if this TAP metadata set has no schema, + * false if it contains at least one schema. + */ public final boolean isEmpty(){ return schemas.isEmpty(); } + /** + *

    Remove the schema having the given ADQL name.

    + * + *

    Important note: + * This function is case sensitive! + *

    + * + *

    WARNING: + * If the goal of this function's call is to delete definitely the specified schema + * from the metadata, you SHOULD also call {@link TAPTable#removeAllForeignKeys()} on the + * removed table. Indeed, foreign keys of this table would still link the removed table + * with other tables AND columns of the whole metadata set. + *

    + * + * @param schemaName ADQL name of the schema to remove from this TAP metadata set. + * + * @return The removed schema, + * or NULL if no such schema can be found. + */ public final TAPSchema removeSchema(String schemaName){ if (schemaName == null) return null; @@ -102,6 +253,9 @@ public class TAPMetadata implements Iterable, VOSIResource, TAPResour return schemas.remove(schemaName); } + /** + * Remove all schemas of this metadata set. + */ public final void removeAllSchemas(){ schemas.clear(); } @@ -111,10 +265,27 @@ public class TAPMetadata implements Iterable, VOSIResource, TAPResour return schemas.values().iterator(); } + /** + * Get the list of all tables available in this TAP metadata set. + * + * @return An iterator over the list of all tables contained in this TAP metadata set. + */ public Iterator getTables(){ - return new TableIterator(this); + return new TAPTableIterator(this); } + /** + *

    Tell whether this TAP metadata set contains the specified table.

    + * + *

    Note: + * This function is case sensitive! + *

    + * + * @param schemaName ADQL name of the schema owning the table to search. + * @param tableName ADQL name of the table to search. + * + * @return true if the specified table exists, false otherwise. + */ public boolean hasTable(String schemaName, String tableName){ TAPSchema s = getSchema(schemaName); if (s != null) @@ -123,6 +294,17 @@ public class TAPMetadata implements Iterable, VOSIResource, TAPResour return false; } + /** + *

    Tell whether this TAP metadata set contains a table with the given ADQL name, whatever is its schema.

    + * + *

    Note: + * This function is case sensitive! + *

    + * + * @param tableName ADQL name of the table to search. + * + * @return true if the specified table exists, false otherwise. + */ public boolean hasTable(String tableName){ for(TAPSchema s : this) if (s.hasTable(tableName)) @@ -130,7 +312,19 @@ public class TAPMetadata implements Iterable, VOSIResource, TAPResour return false; } - // @Override + /** + *

    Search for the specified table in this TAP metadata set.

    + * + *

    Note: + * This function is case sensitive! + *

    + * + * @param schemaName ADQL name of the schema owning the table to search. + * @param tableName ADQL name of the table to search. + * + * @return The table which has the given ADQL name and which is inside the specified schema, + * or NULL if no such table can be found. + */ public TAPTable getTable(String schemaName, String tableName){ TAPSchema s = getSchema(schemaName); if (s != null) @@ -139,7 +333,19 @@ public class TAPMetadata implements Iterable, VOSIResource, TAPResour return null; } - // @Override + /** + *

    Search in this TAP metadata set for all tables whose the ADQL name matches the given one, + * whatever is their schema.

    + * + *

    Note: + * This function is case sensitive! + *

    + * + * @param tableName ADQL name of the tables to search. + * + * @return A list of all the tables which have the given ADQL name, + * or an empty list if no such table can be found. + */ public ArrayList getTable(String tableName){ ArrayList tables = new ArrayList(); for(TAPSchema s : this) @@ -148,6 +354,11 @@ public class TAPMetadata implements Iterable, VOSIResource, TAPResour return tables; } + /** + * Get the number of all tables contained in this TAP metadata set. + * + * @return Number of all its tables. + */ public int getNbTables(){ int nbTables = 0; for(TAPSchema s : this) @@ -155,11 +366,17 @@ public class TAPMetadata implements Iterable, VOSIResource, TAPResour return nbTables; } - public static class TableIterator implements Iterator { + /** + * Let iterating over the list of all tables contained in a given {@link TAPMetadata} object. + * + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (08/2014) + */ + protected static class TAPTableIterator implements Iterator { private Iterator it; private Iterator itTables; - public TableIterator(TAPMetadata tapSchema){ + public TAPTableIterator(TAPMetadata tapSchema){ it = tapSchema.iterator(); if (it.hasNext()) @@ -231,122 +448,269 @@ public class TAPMetadata implements Iterable, VOSIResource, TAPResour } @Override - public void init(ServletConfig config) throws ServletException{ - ; - } + public void init(ServletConfig config) throws ServletException{} @Override - public void destroy(){ - ; - } + public void destroy(){} @Override - public boolean executeResource(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{ + public boolean executeResource(HttpServletRequest request, HttpServletResponse response) throws IOException{ response.setContentType("application/xml"); + response.setCharacterEncoding(UWSToolBox.DEFAULT_CHAR_ENCODING); PrintWriter writer = response.getWriter(); + write(writer); + return false; + } + + /** + * Format in XML this whole metadata set and write it in the given writer. + * + * @param writer Stream in which the XML representation of this metadata must be written. + * + * @throws IOException If there is any error while writing the XML in the given writer. + * + * @since 2.0 + */ + public void write(final PrintWriter writer) throws IOException{ writer.println(""); - // TODO Change the xsi:schemaLocation attribute with a CDS URL ! - //writer.println(""); - writer.println(""); + /* TODO The XSD schema for VOSITables should be fixed soon! This schema should be changed here before the library is released! + * Note: the XSD schema at http://www.ivoa.net/xml/VOSITables/v1.0 contains an incorrect targetNamespace ("http://www.ivoa.net/xml/VOSICapabilities/v1.0"). + * In order to make this XML document valid, a custom location toward a correct XSD schema is used: http://vo.ari.uni-heidelberg.de/docs/schemata/VOSITables-v1.0.xsd */ + writer.println(""); for(TAPSchema s : schemas.values()) writeSchema(s, writer); - writer.println(""); + writer.println(""); - writer.flush(); - - return false; + UWSToolBox.flush(writer); } + /** + *

    Format in XML the given schema and then write it in the given writer.

    + * + *

    Written lines:

    + *
    +	 * <schema>
    +	 * 	<name>...</name>
    +	 * 	<title>...</title>
    +	 * 	<description>...</description>
    +	 * 	<utype>...</utype>
    +	 * 		// call #writeTable(TAPTable, PrintWriter) for each table
    +	 * </schema>
    +	 * 
    + * + *

    Note: + * When NULL an attribute or a field is not written. Here this rule concerns: description and utype. + *

    + * + * @param s The schema to format and to write in XML. + * @param writer Output in which the XML serialization of the given schema must be written. + * + * @throws IOException If the connection with the HTTP client has been either canceled or closed for another reason. + * + * @see #writeTable(TAPTable, PrintWriter) + */ private void writeSchema(TAPSchema s, PrintWriter writer) throws IOException{ final String prefix = "\t\t"; writer.println("\t"); - writeAtt(prefix, "name", s.getName(), writer); - writeAtt(prefix, "description", s.getDescription(), writer); - writeAtt(prefix, "utype", s.getUtype(), writer); + writeAtt(prefix, "name", s.getADQLName(), false, writer); + writeAtt(prefix, "title", s.getTitle(), true, writer); + writeAtt(prefix, "description", s.getDescription(), true, writer); + writeAtt(prefix, "utype", s.getUtype(), true, writer); + + int nbColumns = 0; + for(TAPTable t : s){ + + // write each table: + nbColumns += writeTable(t, writer); + + // flush the PrintWriter buffer when at least 30 tables have been read: + /* Note: the buffer may have already been flushed before automatically, + * but this manual flush is also checking whether any error has occurred while writing the previous characters. + * If so, a ClientAbortException (extension of IOException) is thrown in order to interrupt the writing of the + * metadata and thus, in order to spare server resources (and particularly memory if the metadata set is large). */ + if (nbColumns / 30 > 1){ + UWSToolBox.flush(writer); + nbColumns = 0; + } - for(TAPTable t : s) - writeTable(t, writer); + } writer.println("\t"); + + if (nbColumns > 0) + UWSToolBox.flush(writer); } - private void writeTable(TAPTable t, PrintWriter writer) throws IOException{ + /** + *

    Format in XML the given table and then write it in the given writer.

    + * + *

    Written lines:

    + *
    +	 * <table type="...">
    +	 * 	<name>...</name>
    +	 * 	<title>...</title>
    +	 * 	<description>...</description>
    +	 * 	<utype>...</utype>
    +	 * 		// call #writeColumn(TAPColumn, PrintWriter) for each column
    +	 * 		// call #writeForeignKey(TAPForeignKey, PrintWriter) for each foreign key
    +	 * </table>
    +	 * 
    + * + *

    Note 1: + * When NULL an attribute or a field is not written. Here this rule concerns: description and utype. + *

    + * + *

    Note 2: + * The PrintWriter buffer is flushed all the 10 columns. At that moment the writer is checked for errors. + * If the error flag is set, a {@link ClientAbortException} is thrown in order to stop the metadata writing. + * This is particularly useful if the metadata data is pretty large. + *

    + * + * @param t The table to format and to write in XML. + * @param writer Output in which the XML serialization of the given table must be written. + * + * @return The total number of written columns. + */ + private int writeTable(TAPTable t, PrintWriter writer){ final String prefix = "\t\t\t"; - writer.print("\t\t"); + writer.print("\t\t"); - writeAtt(prefix, "name", t.getFullName(), writer); - writeAtt(prefix, "description", t.getDescription(), writer); - writeAtt(prefix, "utype", t.getUtype(), writer); + if (t.isInitiallyQualified()) + writeAtt(prefix, "name", t.getADQLSchemaName() + "." + t.getADQLName(), false, writer); + else + writeAtt(prefix, "name", t.getADQLName(), false, writer); + writeAtt(prefix, "title", t.getTitle(), true, writer); + writeAtt(prefix, "description", t.getDescription(), true, writer); + writeAtt(prefix, "utype", t.getUtype(), true, writer); + int nbCol = 0; Iterator itCols = t.getColumns(); - while(itCols.hasNext()) + while(itCols.hasNext()){ writeColumn(itCols.next(), writer); + nbCol++; + } Iterator itFK = t.getForeignKeys(); while(itFK.hasNext()) writeForeignKey(itFK.next(), writer); writer.println("\t\t
    "); + + return nbCol; } - private void writeColumn(TAPColumn c, PrintWriter writer) throws IOException{ + /** + *

    Format in XML the given column and then write it in the given writer.

    + * + *

    Written lines:

    + *
    +	 * <column std="true|false"> // the value of this field is TAPColumn#isStd()
    +	 * 	<name>...</name>
    +	 * 	<description>...</description>
    +	 * 	<unit>...</unit>
    +	 * 	<utype>...</utype>
    +	 * 	<ucd>...</ucd>
    +	 * 	<dataType xsi:type="vod:TAPType" size="...">...</dataType>
    +	 * 	<flag>indexed</flag> // if TAPColumn#isIndexed()
    +	 * 	<flag>primary</flag> // if TAPColumn#isPrincipal()
    +	 * </column>
    +	 * 
    + * + *

    Note: + * When NULL an attribute or a field is not written. Here this rule concerns: description, unit, utype, ucd and flags. + *

    + * + * @param c The column to format and to write in XML. + * @param writer Output in which the XML serialization of the given column must be written. + */ + private void writeColumn(TAPColumn c, PrintWriter writer){ final String prefix = "\t\t\t\t"; - writer.print("\t\t\t"); + writer.print("\t\t\t"); - writeAtt(prefix, "name", c.getName(), writer); - writeAtt(prefix, "description", c.getDescription(), writer); - writeAtt(prefix, "unit", c.getUnit(), writer); - writeAtt(prefix, "utype", c.getUtype(), writer); - writeAtt(prefix, "ucd", c.getUcd(), writer); + writeAtt(prefix, "name", c.getADQLName(), false, writer); + writeAtt(prefix, "description", c.getDescription(), true, writer); + writeAtt(prefix, "unit", c.getUnit(), true, writer); + writeAtt(prefix, "ucd", c.getUcd(), true, writer); + writeAtt(prefix, "utype", c.getUtype(), true, writer); if (c.getDatatype() != null){ writer.print(prefix); writer.print("= 0){ + if (c.getDatatype().length > 0){ writer.print(" size=\""); - writer.print(c.getArraySize()); + writer.print(c.getDatatype().length); writer.print("\""); } writer.print('>'); - writer.print(c.getDatatype().toUpperCase()); + writer.print(VOSerializer.formatText(c.getDatatype().type.toString().toUpperCase())); writer.println(""); } if (c.isIndexed()) - writeAtt(prefix, "flag", "indexed", writer); + writeAtt(prefix, "flag", "indexed", true, writer); if (c.isPrincipal()) - writeAtt(prefix, "flag", "primary", writer); + writeAtt(prefix, "flag", "primary", true, writer); + if (c.isNullable()) + writeAtt(prefix, "flag", "nullable", true, writer); writer.println("\t\t\t"); } - private void writeForeignKey(TAPForeignKey fk, PrintWriter writer) throws IOException{ + /** + *

    Format in XML the given foreign key and then write it in the given writer.

    + * + *

    Written lines:

    + *
    +	 * <foreignKey>
    +	 * 	<targetTable>...</targetTable>
    +	 * 	<description>...</description>
    +	 * 	<utype>...</utype>
    +	 * 	<fkColumn>
    +	 * 		<fromColumn>...</fromColumn>
    +	 * 		<targetColumn>...</targetColumn>
    +	 * 	</fkColumn>
    +	 * 	...
    +	 * </foreignKey>
    +	 * 
    + * + *

    Note: + * When NULL an attribute or a field is not written. Here this rule concerns: description and utype. + *

    + * + * @param fk The foreign key to format and to write in XML. + * @param writer Output in which the XML serialization of the given foreign key must be written. + */ + private void writeForeignKey(TAPForeignKey fk, PrintWriter writer){ final String prefix = "\t\t\t\t"; writer.println("\t\t\t"); - writeAtt(prefix, "targetTable", fk.getTargetTable().getFullName(), writer); - writeAtt(prefix, "description", fk.getDescription(), writer); - writeAtt(prefix, "utype", fk.getUtype(), writer); + writeAtt(prefix, "targetTable", fk.getTargetTable().getFullName(), false, writer); + writeAtt(prefix, "description", fk.getDescription(), true, writer); + writeAtt(prefix, "utype", fk.getUtype(), true, writer); final String prefix2 = prefix + "\t"; for(Map.Entry entry : fk){ writer.print(prefix); writer.println(""); - writeAtt(prefix2, "fromColumn", entry.getKey(), writer); - writeAtt(prefix2, "targetColumn", entry.getValue(), writer); + writeAtt(prefix2, "fromColumn", entry.getKey(), false, writer); + writeAtt(prefix2, "targetColumn", entry.getValue(), false, writer); writer.print(prefix); writer.println(""); } @@ -354,11 +718,205 @@ public class TAPMetadata implements Iterable, VOSIResource, TAPResour writer.println("\t\t\t"); } - private void writeAtt(String prefix, String attributeName, String attributeValue, PrintWriter writer) throws IOException{ - if (attributeValue != null){ + /** + * Write the specified metadata attribute as a simple XML node. + * + * @param prefix Prefix of the XML node. (generally, space characters) + * @param attributeName Name of the metadata attribute to write (= Name of the XML node). + * @param attributeValue Value of the metadata attribute (= Value of the XML node). + * @param isOptionalAttr true if the attribute to write is optional (in this case, if the value is NULL or an empty string, the whole attribute item won't be written), + * false otherwise (here, if the value is NULL or an empty string, the XML item will be written with an empty string as value). + * @param writer Output in which the XML node must be written. + */ + private void writeAtt(String prefix, String attributeName, String attributeValue, boolean isOptionalAttr, PrintWriter writer){ + if (attributeValue != null && attributeValue.trim().length() > 0){ StringBuffer xml = new StringBuffer(prefix); - xml.append('<').append(attributeName).append('>').append(attributeValue).append("'); + xml.append('<').append(attributeName).append('>').append(VOSerializer.formatText(attributeValue)).append("'); writer.println(xml.toString()); + }else if (!isOptionalAttr) + writer.println("<" + attributeName + ">"); + } + + /** + *

    + * Get the definition of the whole standard TAP_SCHEMA. Thus, all standard TAP_SCHEMA tables + * (with all their columns) are also included in this object. + *

    + * + *

    Note: + * This function create the {@link TAPSchema} and all its {@link TAPTable}s objects on the fly. + *

    + * + * @param isSchemaSupported false if the DB name must be prefixed by "TAP_SCHEMA_", true otherwise. + * + * @return The whole TAP_SCHEMA definition. + * + * @see STDSchema#TAPSCHEMA + * @see STDTable + * @see #getStdTable(STDTable) + * + * @since 2.0 + */ + public static final TAPSchema getStdSchema(final boolean isSchemaSupported){ + TAPSchema tap_schema = new TAPSchema(STDSchema.TAPSCHEMA.toString(), "Set of tables listing and describing the schemas, tables and columns published in this TAP service.", null); + if (!isSchemaSupported) + tap_schema.setDBName(null); + for(STDTable t : STDTable.values()){ + TAPTable table = getStdTable(t); + if (!isSchemaSupported) + table.setDBName(STDSchema.TAPSCHEMA.label + "_" + table.getADQLName()); + tap_schema.addTable(table); + } + return tap_schema; + } + + /** + *

    Get the definition of the specified standard TAP table.

    + * + *

    Important note: + * The returned table is not linked at all with a schema, on the contrary of {@link #getStdSchema(boolean)} which returns tables linked with the returned schema. + * So, you may have to linked this table to schema (by using {@link TAPSchema#addTable(TAPTable)}) whose the ADQL name is TAP_SCHEMA after calling this function. + *

    + * + *

    Note: + * This function create the {@link TAPTable} object on the fly. + *

    + * + * @param tableId ID of the TAP table to return. + * + * @return The corresponding table definition (with no schema). + * + * @since 2.0 + */ + public static final TAPTable getStdTable(final STDTable tableId){ + switch(tableId){ + + case SCHEMAS: + TAPTable schemas = new TAPTable(STDTable.SCHEMAS.toString(), TableType.table, "List of schemas published in this TAP service.", null); + schemas.setInitiallyQualifed(true); + schemas.addColumn("schema_name", new DBType(DBDatatype.VARCHAR), "schema name, possibly qualified", null, null, null, true, true, true); + schemas.addColumn("description", new DBType(DBDatatype.VARCHAR), "brief description of schema", null, null, null, false, false, true); + schemas.addColumn("utype", new DBType(DBDatatype.VARCHAR), "UTYPE if schema corresponds to a data model", null, null, null, false, false, true); + return schemas; + + case TABLES: + TAPTable tables = new TAPTable(STDTable.TABLES.toString(), TableType.table, "List of tables published in this TAP service.", null); + tables.setInitiallyQualifed(true); + tables.addColumn("schema_name", new DBType(DBDatatype.VARCHAR), "the schema name from TAP_SCHEMA.schemas", null, null, null, true, true, true); + tables.addColumn("table_name", new DBType(DBDatatype.VARCHAR), "table name as it should be used in queries", null, null, null, true, true, true); + tables.addColumn("table_type", new DBType(DBDatatype.VARCHAR), "one of: table, view", null, null, null, false, false, true); + tables.addColumn("description", new DBType(DBDatatype.VARCHAR), "brief description of table", null, null, null, false, false, true); + tables.addColumn("utype", new DBType(DBDatatype.VARCHAR), "UTYPE if table corresponds to a data model", null, null, null, false, false, true); + return tables; + + case COLUMNS: + TAPTable columns = new TAPTable(STDTable.COLUMNS.toString(), TableType.table, "List of columns of all tables listed in TAP_SCHEMA.TABLES and published in this TAP service.", null); + columns.setInitiallyQualifed(true); + columns.addColumn("table_name", new DBType(DBDatatype.VARCHAR), "table name from TAP_SCHEMA.tables", null, null, null, true, true, true); + columns.addColumn("column_name", new DBType(DBDatatype.VARCHAR), "column name", null, null, null, true, true, true); + columns.addColumn("description", new DBType(DBDatatype.VARCHAR), "brief description of column", null, null, null, false, false, true); + columns.addColumn("unit", new DBType(DBDatatype.VARCHAR), "unit in VO standard format", null, null, null, false, false, true); + columns.addColumn("ucd", new DBType(DBDatatype.VARCHAR), "UCD of column if any", null, null, null, false, false, true); + columns.addColumn("utype", new DBType(DBDatatype.VARCHAR), "UTYPE of column if any", null, null, null, false, false, true); + columns.addColumn("datatype", new DBType(DBDatatype.VARCHAR), "ADQL datatype as in section 2.5", null, null, null, false, false, true); + columns.addColumn("size", new DBType(DBDatatype.INTEGER), "length of variable length datatypes", null, null, null, false, false, true); + columns.addColumn("principal", new DBType(DBDatatype.INTEGER), "a principal column; 1 means true, 0 means false", null, null, null, false, false, true); + columns.addColumn("indexed", new DBType(DBDatatype.INTEGER), "an indexed column; 1 means true, 0 means false", null, null, null, false, false, true); + columns.addColumn("std", new DBType(DBDatatype.INTEGER), "a standard column; 1 means true, 0 means false", null, null, null, false, false, true); + return columns; + + case KEYS: + TAPTable keys = new TAPTable(STDTable.KEYS.toString(), TableType.table, "List all foreign keys but provides just the tables linked by the foreign key. To know which columns of these tables are linked, see in TAP_SCHEMA.key_columns using the key_id.", null); + keys.setInitiallyQualifed(true); + keys.addColumn("key_id", new DBType(DBDatatype.VARCHAR), "unique key identifier", null, null, null, true, true, true); + keys.addColumn("from_table", new DBType(DBDatatype.VARCHAR), "fully qualified table name", null, null, null, false, false, true); + keys.addColumn("target_table", new DBType(DBDatatype.VARCHAR), "fully qualified table name", null, null, null, false, false, true); + keys.addColumn("description", new DBType(DBDatatype.VARCHAR), "description of this key", null, null, null, false, false, true); + keys.addColumn("utype", new DBType(DBDatatype.VARCHAR), "utype of this key", null, null, null, false, false, true); + return keys; + + case KEY_COLUMNS: + TAPTable key_columns = new TAPTable(STDTable.KEY_COLUMNS.toString(), TableType.table, "List all foreign keys but provides just the columns linked by the foreign key. To know the table of these columns, see in TAP_SCHEMA.keys using the key_id.", null); + key_columns.setInitiallyQualifed(true); + key_columns.addColumn("key_id", new DBType(DBDatatype.VARCHAR), "unique key identifier", null, null, null, true, true, true); + key_columns.addColumn("from_column", new DBType(DBDatatype.VARCHAR), "key column name in the from_table", null, null, null, false, false, true); + key_columns.addColumn("target_column", new DBType(DBDatatype.VARCHAR), "key column name in the target_table", null, null, null, false, false, true); + return key_columns; + + default: + return null; + } + } + + /** + *

    Tell whether the given table name is a standard TAP table.

    + * + *

    Note: + * This function is case sensitive. Indeed TAP_SCHEMA tables are defined by the TAP standard by a given case. + * Thus, this case is expected here. + *

    + * + * @param tableName Unqualified table name. + * + * @return The corresponding {@link STDTable} or NULL if the given table is not part of the TAP standard. + * + * @since 2.0 + */ + public static final STDTable resolveStdTable(String tableName){ + if (tableName == null || tableName.trim().length() == 0) + return null; + + for(STDTable t : STDTable.values()){ + if (t.label.equals(tableName)) + return t; + } + + return null; + } + + /** + * Enumeration of all schemas defined in the TAP standard. + * + * @author Grégory Mantelet (ARI) + * @version 2.0 (07/2014) + * @since 2.0 + */ + public enum STDSchema{ + TAPSCHEMA("TAP_SCHEMA"), UPLOADSCHEMA("TAP_UPLOAD"); + + /** Real name of the schema. */ + public final String label; + + private STDSchema(final String name){ + this.label = name; + } + + @Override + public String toString(){ + return label; + } + } + + /** + * Enumeration of all tables of TAP_SCHEMA. + * + * @author Grégory Mantelet (ARI) + * @version 2.0 (07/2014) + * @since 2.0 + */ + public enum STDTable{ + SCHEMAS("schemas"), TABLES("tables"), COLUMNS("columns"), KEYS("keys"), KEY_COLUMNS("key_columns"); + + /** Real name of the table. */ + public final String label; + + private STDTable(final String name){ + this.label = name; + } + + @Override + public String toString(){ + return label; } } diff --git a/src/tap/metadata/TAPSchema.java b/src/tap/metadata/TAPSchema.java index faced37..182c421 100644 --- a/src/tap/metadata/TAPSchema.java +++ b/src/tap/metadata/TAPSchema.java @@ -16,37 +16,127 @@ package tap.metadata; * You should have received a copy of the GNU Lesser General Public License * along with TAPLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Astronomisches Rechen Institut (ARI) */ -import java.util.HashMap; +import java.awt.List; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.Map; +import tap.metadata.TAPTable.TableType; + +/** + *

    Represent a schema as described by the IVOA standard in the TAP protocol definition.

    + * + *

    + * This object representation has exactly the same fields as the column of the table TAP_SCHEMA.schemas. + * But it also provides a way to add other data. For instance, if information not listed in the standard + * may be stored here, they can be using the function {@link #setOtherData(Object)}. This object can be + * a single value (integer, string, ...), but also a {@link Map}, {@link List}, etc... + *

    + * + *

    Note: + * On the contrary to {@link TAPColumn} and {@link TAPTable}, a {@link TAPSchema} object MAY have no DB name. + * But by default, at the creation the DB name is the ADQL name. Once created, it is possible to set the DB + * name with {@link #setDBName(String)}. This DB name MAY be qualified, BUT MUST BE without double quotes. + *

    + * + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (02/2015) + */ public class TAPSchema implements Iterable { + /** Name that this schema MUST have in ADQL queries. */ private final String adqlName; + /** Name that this schema have in the database. + * Note: It MAY be NULL. By default, it is the ADQL name. */ private String dbName = null; + /** Descriptive, human-interpretable name of the schema. + * Note: Standard TAP schema field ; MAY be NULL. + * @since 2.0 */ + private String title = null; + + /** Description of this schema. + * Note: Standard TAP schema field ; MAY be NULL. */ private String description = null; + /** UType describing the scientific content of this schema. + * Note: Standard TAP schema field ; MAY be NULL. */ private String utype = null; + /** Let add some information in addition of the ones of the TAP protocol. + * Note: This object can be anything: an {@link Integer}, a {@link String}, a {@link Map}, a {@link List}, ... + * Its content is totally free and never used or checked. */ protected Object otherData = null; + /** List all tables contained inside this schema. */ protected final Map tables; + /** + *

    Build a {@link TAPSchema} instance with the given ADQL name.

    + * + *

    Note: + * The DB name is set by default with the ADQL name. To set the DB name, + * you MUST call then {@link #setDBName(String)}. + *

    + * + *

    Note: + * If the given ADQL name is prefixed (= it has some text separated by a '.' before the schema name), + * this prefix will be removed. Only the part after the '.' character will be kept. + *

    + * + * @param schemaName Name that this schema MUST have in ADQL queries. CAN'T be NULL ; this name can never be changed after. + */ public TAPSchema(String schemaName){ - adqlName = schemaName; + if (schemaName == null || schemaName.trim().length() == 0) + throw new NullPointerException("Missing schema name!"); + int indPrefix = schemaName.lastIndexOf('.'); + adqlName = (indPrefix >= 0) ? schemaName.substring(indPrefix + 1).trim() : schemaName.trim(); dbName = adqlName; - tables = new HashMap(); + tables = new LinkedHashMap(); } + /** + *

    Build a {@link TAPSchema} instance with the given ADQL name and description.

    + * + *

    Note: + * The DB name is set by default with the ADQL name. To set the DB name, + * you MUST call then {@link #setDBName(String)}. + *

    + * + *

    Note: + * If the given ADQL name is prefixed (= it has some text separated by a '.' before the schema name), + * this prefix will be removed. Only the part after the '.' character will be kept. + *

    + * + * @param schemaName Name that this schema MUST have in ADQL queries. CAN'T be NULL ; this name can never be changed after. + * @param description Description of this schema. MAY be NULL + */ public TAPSchema(String schemaName, String description){ this(schemaName, description, null); } + /** + *

    Build a {@link TAPSchema} instance with the given ADQL name, description and UType.

    + * + *

    Note: + * The DB name is set by default with the ADQL name. To set the DB name, + * you MUST call then {@link #setDBName(String)}. + *

    + * + *

    Note: + * If the given ADQL name is prefixed (= it has some text separated by a '.' before the schema name), + * this prefix will be removed. Only the part after the '.' character will be kept. + *

    + * + * @param schemaName Name that this schema MUST have in ADQL queries. CAN'T be NULL ; this name can never be changed after. + * @param description Description of this schema. MAY be NULL + * @param utype UType associating this schema with a data-model. MAY be NULL + */ public TAPSchema(String schemaName, String description, String utype){ this(schemaName); this.description = description; @@ -54,68 +144,165 @@ public class TAPSchema implements Iterable { } /** - * @return The name. + * Get the ADQL name (the name this schema MUST have in ADQL queries). + * + * @return Its ADQL name. + * @see #getADQLName() + * @deprecated Does not do anything special: just call {@link #getADQLName()}. */ + @Deprecated public final String getName(){ return getADQLName(); } + /** + * Get the name this schema MUST have in ADQL queries. + * + * @return Its ADQL name. CAN'T be NULL + */ public final String getADQLName(){ return adqlName; } + /** + * Get the name this schema MUST have in the database. + * + * @return Its DB name. MAY be NULL + */ public final String getDBName(){ return dbName; } + /** + * Set the name this schema MUST have in the database. + * + * @param name Its new DB name. MAY be NULL + */ public final void setDBName(String name){ name = (name != null) ? name.trim() : name; - dbName = (name == null || name.length() == 0) ? adqlName : name; + dbName = name; + } + + /** + * Get the title of this schema. + * + * @return Its title. MAY be NULL + * + * @since 2.0 + */ + public final String getTitle(){ + return title; + } + + /** + * Set the title of this schema. + * + * @param title Its new title. MAY be NULL + * + * @since 2.0 + */ + public final void setTitle(final String title){ + this.title = title; } /** - * @return The description. + * Get the description of this schema. + * + * @return Its description. MAY be NULL */ public final String getDescription(){ return description; } /** - * @param description The description to set. + * Set the description of this schema. + * + * @param description Its new description. MAY be NULL */ public final void setDescription(String description){ this.description = description; } /** - * @return The utype. + * Get the UType associating this schema with a data-model. + * + * @return Its UType. MAY be NULL */ public final String getUtype(){ return utype; } /** - * @param utype The utype to set. + * Set the UType associating this schema with a data-model. + * + * @param utype Its new UType. MAY be NULL */ public final void setUtype(String utype){ this.utype = utype; } + /** + *

    Get the other (piece of) information associated with this schema.

    + * + *

    Note: + * By default, NULL is returned, but it may be any kind of value ({@link Integer}, + * {@link String}, {@link Map}, {@link List}, ...). + *

    + * + * @return The other (piece of) information. MAY be NULL + */ public Object getOtherData(){ return otherData; } + /** + * Set the other (piece of) information associated with this schema. + * + * @param data Another information about this schema. MAY be NULL + */ public void setOtherData(Object data){ otherData = data; } + /** + *

    Add the given table inside this schema.

    + * + *

    Note: + * If the given table is NULL, nothing will be done. + *

    + * + *

    Important note: + * By adding the given table inside this schema, it + * will be linked with this schema using {@link TAPTable#setSchema(TAPSchema)}. + * In this function, if the table was already linked with another {@link TAPSchema}, + * the former link is removed using {@link TAPSchema#removeTable(String)}. + *

    + * + * @param newTable Table to add inside this schema. + */ public final void addTable(TAPTable newTable){ - if (newTable != null && newTable.getName() != null){ - tables.put(newTable.getName(), newTable); + if (newTable != null && newTable.getADQLName() != null){ + tables.put(newTable.getADQLName(), newTable); newTable.setSchema(this); } } + /** + *

    Build a {@link TAPTable} object whose the ADQL and DB name will the given one. + * Then, add this table inside this schema.

    + * + *

    Note: + * The built {@link TAPTable} object is returned, so that being modified afterwards if needed. + *

    + * + * @param tableName ADQL name (and indirectly also the DB name) of the table to create and add. + * + * @return The created and added {@link TAPTable} object, + * or NULL if the given name is NULL or an empty string. + * + * @see TAPTable#TAPTable(String) + * @see #addTable(TAPTable) + */ public TAPTable addTable(String tableName){ if (tableName == null) return null; @@ -125,15 +312,46 @@ public class TAPSchema implements Iterable { return t; } - public TAPTable addTable(String tableName, String tableType, String description, String utype){ + /** + *

    Build a {@link TAPTable} object whose the ADQL and DB name will the given one. + * Then, add this table inside this schema.

    + * + *

    Note: + * The built {@link TAPTable} object is returned, so that being modified afterwards if needed. + *

    + * + * @param tableName ADQL name (and indirectly also the DB name) of the table to create and add. + * @param tableType Type of the new table. If NULL, "table" will be the type of the created table. + * @param description Description of the new table. MAY be NULL + * @param utype UType associating the new column with a data-model. MAY be NULL + * + * @return The created and added {@link TAPTable} object, + * or NULL if the given name is NULL or an empty string. + * + * @see TAPTable#TAPTable(String, TableType, String, String) + * @see #addTable(TAPTable) + */ + public TAPTable addTable(String tableName, TableType tableType, String description, String utype){ if (tableName == null) return null; TAPTable t = new TAPTable(tableName, tableType, description, utype); addTable(t); + return t; } + /** + *

    Tell whether this schema contains a table having the given ADQL name.

    + * + *

    Important note: + * This function is case sensitive! + *

    + * + * @param tableName Name of the table whose the existence in this schema must be checked. + * + * @return true if a table with the given ADQL name exists, false otherwise. + */ public final boolean hasTable(String tableName){ if (tableName == null) return false; @@ -141,6 +359,18 @@ public class TAPSchema implements Iterable { return tables.containsKey(tableName); } + /** + *

    Search for a table having the given ADQL name.

    + * + *

    Important note: + * This function is case sensitive! + *

    + * + * @param tableName ADQL name of the table to search. + * + * @return The table having the given ADQL name, + * or NULL if no such table can be found. + */ public final TAPTable getTable(String tableName){ if (tableName == null) return null; @@ -148,33 +378,79 @@ public class TAPSchema implements Iterable { return tables.get(tableName); } + /** + * Get the number of all tables contained inside this schema. + * + * @return Number of its tables. + */ public final int getNbTables(){ return tables.size(); } + /** + * Tell whether this schema contains no table. + * + * @return true if this schema contains no table, + * false if it has at least one table. + */ public final boolean isEmpty(){ return tables.isEmpty(); } + /** + *

    Remove the table having the given ADQL name.

    + * + *

    Important note: + * This function is case sensitive! + *

    + * + *

    Note: + * If the specified table is removed, its schema link is also deleted. + *

    + * + *

    WARNING: + * If the goal of this function's call is to delete definitely the specified table + * from the metadata, you SHOULD also call {@link TAPTable#removeAllForeignKeys()}. + * Indeed, foreign keys of the table would still link the removed table with other tables + * AND columns of the whole metadata set. + *

    + * + * @param tableName ADQL name of the table to remove from this schema. + * + * @return The removed table, + * or NULL if no table with the given ADQL name can be found. + */ public final TAPTable removeTable(String tableName){ if (tableName == null) return null; TAPTable removedTable = tables.remove(tableName); - if (removedTable != null){ + if (removedTable != null) removedTable.setSchema(null); - removedTable.removeAllForeignKeys(); - } return removedTable; } + /** + *

    Remove all the tables contained inside this schema.

    + * + *

    Note: + * When a table is removed, its schema link is also deleted. + *

    + * + *

    CAUTION: + * If the goal of this function's call is to delete definitely all the tables of this schema + * from the metadata, you SHOULD also call {@link TAPTable#removeAllForeignKeys()} + * on all tables before calling this function. + * Indeed, foreign keys of the tables would still link the removed tables with other tables + * AND columns of the whole metadata set. + *

    + */ public final void removeAllTables(){ Iterator> it = tables.entrySet().iterator(); while(it.hasNext()){ Map.Entry entry = it.next(); it.remove(); entry.getValue().setSchema(null); - entry.getValue().removeAllForeignKeys(); } } diff --git a/src/tap/metadata/TAPTable.java b/src/tap/metadata/TAPTable.java index 92fd803..f3030d0 100644 --- a/src/tap/metadata/TAPTable.java +++ b/src/tap/metadata/TAPTable.java @@ -16,70 +16,209 @@ package tap.metadata; * You should have received a copy of the GNU Lesser General Public License * along with TAPLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Astronomisches Rechen Institut (ARI) */ +import java.awt.List; import java.util.ArrayList; import java.util.Collection; -import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; +import tap.TAPException; import adql.db.DBColumn; import adql.db.DBTable; +import adql.db.DBType; +/** + *

    Represent a table as described by the IVOA standard in the TAP protocol definition.

    + * + *

    + * This object representation has exactly the same fields as the column of the table TAP_SCHEMA.tables. + * But it also provides a way to add other data. For instance, if information not listed in the standard + * may be stored here, they can be using the function {@link #setOtherData(Object)}. This object can be + * a single value (integer, string, ...), but also a {@link Map}, {@link List}, etc... + *

    + * + *

    Important note: + * A {@link TAPTable} object MUST always have a DB name. That's why by default, at the creation + * the DB name is the ADQL name. Once created, it is possible to set the DB name with {@link #setDBName(String)}. + * This DB name MUST be UNqualified and without double quotes. If a NULL or empty value is provided, + * nothing is done and the object keeps its former DB name. + *

    + * + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (02/2015) + */ public class TAPTable implements DBTable { + /** + * Different types of table according to the TAP protocol. + * The default one should be "table". + * + * @author Grégory Mantelet (ARI) + * @version 2.0 (08/2014) + * + * @since 2.0 + */ + public enum TableType{ + output, table, view; + } + + /** Name that this table MUST have in ADQL queries. */ private final String adqlName; + /**

    Indicate whether the ADQL name has been given at creation with a schema prefix or not.

    + *

    Note: This information is used only when writing TAP_SCHEMA.tables or when writing the output of the resource /tables.

    + * @since 2.0 */ + private boolean isInitiallyQualified; + + /** Name that this table have in the database. + * Note: It CAN'T be NULL. By default, it is the ADQL name. */ private String dbName = null; + /** The schema which owns this table. + * Note: It is NULL only at the construction. + * Then, this attribute is automatically set by a {@link TAPSchema} when adding this table inside it + * with {@link TAPSchema#addTable(TAPTable)}. */ private TAPSchema schema = null; - private String type = "table"; + /** Type of this table. + * Note: Standard TAP table field ; CAN NOT be NULL ; by default, it is "table". */ + private TableType type = TableType.table; + /** Descriptive, human-interpretable name of the table. + * Note: Standard TAP table field ; MAY be NULL. + * @since 2.0 */ + private String title = null; + + /** Description of this table. + * Note: Standard TAP table field ; MAY be NULL. */ private String description = null; + /** UType associating this table with a data-model. + * Note: Standard TAP table field ; MAY be NULL. */ private String utype = null; + /** List of columns composing this table. + * Note: all columns of this list are linked to this table from the moment they are added inside it. */ protected final Map columns; + /** List of all foreign keys linking this table to others. */ protected final ArrayList foreignKeys; + /** Let add some information in addition of the ones of the TAP protocol. + * Note: This object can be anything: an {@link Integer}, a {@link String}, a {@link Map}, a {@link List}, ... + * Its content is totally free and never used or checked. */ protected Object otherData = null; + /** + *

    Build a {@link TAPTable} instance with the given ADQL name.

    + * + *

    Note: + * The DB name is set by default with the ADQL name. To set the DB name, + * you MUST call then {@link #setDBName(String)}. + * The table type is set by default to "table". + *

    + * + *

    Note: + * If the given ADQL name is prefixed (= it has some text separated by a '.' before the table name), + * this prefix will be removed. Only the part after the '.' character will be kept. + *

    + * + * @param tableName Name that this table MUST have in ADQL queries. CAN'T be NULL ; this name can never be changed after. + */ public TAPTable(String tableName){ if (tableName == null || tableName.trim().length() == 0) throw new NullPointerException("Missing table name !"); int indPrefix = tableName.lastIndexOf('.'); adqlName = (indPrefix >= 0) ? tableName.substring(indPrefix + 1).trim() : tableName.trim(); + isInitiallyQualified = (indPrefix >= 0); dbName = adqlName; columns = new LinkedHashMap(); foreignKeys = new ArrayList(); } - public TAPTable(String tableName, String tableType){ + /** + *

    Build a {@link TAPTable} instance with the given ADQL name and table type.

    + * + *

    Note: + * The DB name is set by default with the ADQL name. To set the DB name, + * you MUST call then {@link #setDBName(String)}. + *

    + * + *

    Note: + * The table type is set by calling the function {@link #setType(TableType)} which does not do + * anything if the given table type is NULL. + *

    + * + * @param tableName Name that this table MUST have in ADQL queries. CAN'T be NULL ; this name can never be changed after. + * @param tableType Type of this table. If NULL, "table" will be the type of this table. + * + * @see #setType(TableType) + */ + public TAPTable(String tableName, TableType tableType){ this(tableName); - type = tableType; + setType(tableType); } - public TAPTable(String tableName, String tableType, String description, String utype){ + /** + *

    Build a {@link TAPTable} instance with the given ADQL name, table type, description and UType.

    + * + *

    Note: + * The DB name is set by default with the ADQL name. To set the DB name, + * you MUST call then {@link #setDBName(String)}. + *

    + * + *

    Note: + * The table type is set by calling the function {@link #setType(TableType)} which does not do + * anything if the given table type is NULL. + *

    + * + * @param tableName Name that this table MUST have in ADQL queries. CAN'T be NULL ; this name can never be changed after. + * @param tableType Type of this table. If NULL, "table" will be the type of this table. + * @param description Description of this table. MAY be NULL. + * @param utype UType associating this table with a data-model. MAY be NULL + * + * @see #setType(TableType) + */ + public TAPTable(String tableName, TableType tableType, String description, String utype){ this(tableName, tableType); this.description = description; this.utype = utype; } + /** + *

    Get the qualified name of this table.

    + * + *

    Warning: + * The part of the returned full name won't be double quoted! + *

    + * + *

    Note: + * If this table is not attached to a schema, this function will just return + * the ADQL name of this table. + *

    + * + * @return Qualified ADQL name of this table. + */ public final String getFullName(){ if (schema != null) - return schema.getName() + "." + adqlName; + return schema.getADQLName() + "." + adqlName; else return adqlName; } /** - * @return The name. + * Get the ADQL name (the name this table MUST have in ADQL queries). + * + * @return Its ADQL name. + * @see #getADQLName() + * @deprecated Does not do anything special: just call {@link #getADQLName()}. */ + @Deprecated public final String getName(){ return getADQLName(); } @@ -89,14 +228,56 @@ public class TAPTable implements DBTable { return adqlName; } + /** + *

    Tells whether the ADQL name of this table must be qualified in the "table_name" column of TAP_SCHEMA.tables + * and in the /schema/table/name field of the resource /tables.

    + * + *

    Note: this value is set automatically by the constructor: "true" if the table name was qualified, + * "false" otherwise. It can be changed with the function {@link #setInitiallyQualifed(boolean)}, BUT by doing so + * you may generate a mismatch between the table name of TAP_SCHEMA.tables and the one of /tables.

    + * + * @return true if the table name must be qualified in TAP_SCHEMA.tables and in /tables, false otherwise. + * + * @since 2.0 + */ + public final boolean isInitiallyQualified(){ + return isInitiallyQualified; + } + + /** + *

    Let specifying whether the table name must be qualified in TAP_SCHEMA.tables and in the resource /tables.

    + * + *

    WARNING: Calling this function may generate a mismatch between the table name of TAP_SCHEMA.tables and + * the one of the resource /tables. So, be sure to change this flag before setting the content of TAP_SCHEMA.tables + * using {@link tap.db.JDBCConnection#setTAPSchema(TAPMetadata)}.

    + * + * @param mustBeQualified true if the table name in TAP_SCHEMA.tables and in the resource /tables must be qualified by the schema name, + * false otherwise. + * + * @since 2.0 + */ + public final void setInitiallyQualifed(final boolean mustBeQualified){ + isInitiallyQualified = mustBeQualified; + } + @Override public final String getDBName(){ return dbName; } + /** + *

    Change the name that this table MUST have in the database (i.e. in SQL queries).

    + * + *

    Note: + * If the given value is NULL or an empty string, nothing is done ; the DB name keeps is former value. + *

    + * + * @param name The new database name of this table. + */ public final void setDBName(String name){ name = (name != null) ? name.trim() : name; - dbName = (name == null || name.length() == 0) ? adqlName : name; + if (name != null && name.length() > 0) + dbName = name; } @Override @@ -111,87 +292,189 @@ public class TAPTable implements DBTable { @Override public final String getADQLSchemaName(){ - return schema.getADQLName(); + return schema == null ? null : schema.getADQLName(); } @Override public final String getDBSchemaName(){ - return schema.getDBName(); + return schema == null ? null : schema.getDBName(); } /** - * @return The schema. + * Get the schema that owns this table. + * + * @return Its schema. MAY be NULL */ public final TAPSchema getSchema(){ return schema; } /** - * @param schema The schema to set. + *

    Set the schema in which this schema is.

    + * + *

    Warning: + * For consistency reasons, this function SHOULD be called only by the {@link TAPSchema} + * that owns this table. + *

    + * + *

    Important note: + * If this table was already linked with another {@link TAPSchema} object, the previous link is removed + * here, but also in the schema (by calling {@link TAPSchema#removeTable(String)}). + *

    + * + * @param schema The schema that owns this table. */ - protected final void setSchema(TAPSchema schema){ + protected final void setSchema(final TAPSchema schema){ + if (this.schema != null && (schema == null || !schema.equals(this.schema))) + this.schema.removeTable(adqlName); this.schema = schema; } /** - * @return The type. + * Get the type of this table. + * + * @return Its type. */ - public final String getType(){ + public final TableType getType(){ return type; } /** - * @param type The type to set. + *

    Set the type of this table.

    + * + *

    Note: + * If the given type is NULL, nothing will be done ; the type of this table won't be changed. + *

    + * + * @param type Its new type. */ - public final void setType(String type){ - this.type = type; + public final void setType(TableType type){ + if (type != null) + this.type = type; } /** - * @return The description. + * Get the title of this table. + * + * @return Its title. MAY be NULL + * + * @since 2.0 + */ + public final String getTitle(){ + return title; + } + + /** + * Set the title of this table. + * + * @param title Its new title. MAY be NULL + * + * @since 2.0 + */ + public final void setTitle(final String title){ + this.title = title; + } + + /** + * Get the description of this table. + * + * @return Its description. MAY be NULL */ public final String getDescription(){ return description; } /** - * @param description The description to set. + * Set the description of this table. + * + * @param description Its new description. MAY be NULL */ public final void setDescription(String description){ this.description = description; } /** - * @return The utype. + * Get the UType associating this table with a data-model. + * + * @return Its UType. MAY be NULL */ public final String getUtype(){ return utype; } /** - * @param utype The utype to set. + * Set the UType associating this table with a data-model. + * + * @param utype Its new UType. MAY be NULL */ public final void setUtype(String utype){ this.utype = utype; } + /** + *

    Get the other (piece of) information associated with this table.

    + * + *

    Note: + * By default, NULL is returned, but it may be any kind of value ({@link Integer}, + * {@link String}, {@link Map}, {@link List}, ...). + *

    + * + * @return The other (piece of) information. MAY be NULL + */ public Object getOtherData(){ return otherData; } + /** + * Set the other (piece of) information associated with this table. + * + * @param data Another information about this table. MAY be NULL + */ public void setOtherData(Object data){ otherData = data; } - public final void addColumn(TAPColumn newColumn){ - if (newColumn != null && newColumn.getName() != null){ - columns.put(newColumn.getName(), newColumn); + /** + *

    Add a column to this table.

    + * + *

    Note: + * If the given column is NULL, nothing will be done. + *

    + * + *

    Important note: + * By adding the given column inside this table, it + * will be linked with this table using {@link TAPColumn#setTable(DBTable)}. + * In this function, if the column was already linked with another {@link TAPTable}, + * the former link is removed using {@link TAPTable#removeColumn(String)}. + *

    + * + * @param newColumn Column to add inside this table. + */ + public final void addColumn(final TAPColumn newColumn){ + if (newColumn != null && newColumn.getADQLName() != null){ + columns.put(newColumn.getADQLName(), newColumn); newColumn.setTable(this); } } + /** + *

    Build a {@link TAPColumn} object whose the ADQL and DB name will the given one. + * Then, add this column inside this table.

    + * + *

    Note: + * The built {@link TAPColumn} object is returned, so that being modified afterwards if needed. + *

    + * + * @param columnName ADQL name (and indirectly also the DB name) of the column to create and add. + * + * @return The created and added {@link TAPColumn} object, + * or NULL if the given name is NULL or an empty string. + * + * @see TAPColumn#TAPColumn(String) + * @see #addColumn(TAPColumn) + */ public final TAPColumn addColumn(String columnName){ - if (columnName == null) + if (columnName == null || columnName.trim().length() <= 0) return null; TAPColumn c = new TAPColumn(columnName); @@ -199,34 +482,68 @@ public class TAPTable implements DBTable { return c; } - public TAPColumn addColumn(String columnName, String description, String unit, String ucd, String utype){ - if (columnName == null) - return null; - - TAPColumn c = new TAPColumn(columnName, description, unit, ucd, utype); - addColumn(c); - return c; - } - - public TAPColumn addColumn(String columnName, String description, String unit, String ucd, String utype, String datatype, int size, boolean principal, boolean indexed, boolean std){ - if (columnName == null) + /** + *

    Build a {@link TAPColumn} object whose the ADQL and DB name will the given one. + * Then, add this column inside this table.

    + * + *

    Note: + * The built {@link TAPColumn} object is returned, so that being modified afterwards if needed. + *

    + * + * @param columnName ADQL name (and indirectly also the DB name) of the column to create and add. + * @param datatype Type of the new column's values. If NULL, VARCHAR will be the type of the created column. + * @param description Description of the new column. MAY be NULL + * @param unit Unit of the new column's values. MAY be NULL + * @param ucd UCD describing the scientific content of the new column. MAY be NULL + * @param utype UType associating the new column with a data-model. MAY be NULL + * + * @return The created and added {@link TAPColumn} object, + * or NULL if the given name is NULL or an empty string. + * + * @see TAPColumn#TAPColumn(String, DBType, String, String, String, String) + * @see #addColumn(TAPColumn) + */ + public TAPColumn addColumn(String columnName, DBType datatype, String description, String unit, String ucd, String utype){ + if (columnName == null || columnName.trim().length() <= 0) return null; - TAPColumn c = new TAPColumn(columnName, description, unit, ucd, utype); - c.setDatatype(datatype, size); - c.setPrincipal(principal); - c.setIndexed(indexed); - c.setStd(std); + TAPColumn c = new TAPColumn(columnName, datatype, description, unit, ucd, utype); addColumn(c); return c; } - public TAPColumn addColumn(String columnName, String description, String unit, String ucd, String utype, VotType votType, boolean principal, boolean indexed, boolean std){ - if (columnName == null) + /** + *

    Build a {@link TAPColumn} object whose the ADQL and DB name will the given one. + * Then, add this column inside this table.

    + * + *

    Note: + * The built {@link TAPColumn} object is returned, so that being modified afterwards if needed. + *

    + * + * @param columnName ADQL name (and indirectly also the DB name) of the column to create and add. + * @param datatype Type of the new column's values. If NULL, VARCHAR will be the type of the created column. + * @param description Description of the new column. MAY be NULL + * @param unit Unit of the new column's values. MAY be NULL + * @param ucd UCD describing the scientific content of the new column. MAY be NULL + * @param utype UType associating the new column with a data-model. MAY be NULL + * @param principal true if the new column should be returned by default, false otherwise. + * @param indexed true if the new column is indexed, false otherwise. + * @param std true if the new column is defined by a standard, false otherwise. + * + * @return The created and added {@link TAPColumn} object, + * or NULL if the given name is NULL or an empty string. + * + * @see TAPColumn#TAPColumn(String, DBType, String, String, String, String) + * @see TAPColumn#setPrincipal(boolean) + * @see TAPColumn#setIndexed(boolean) + * @see TAPColumn#setStd(boolean) + * @see #addColumn(TAPColumn) + */ + public TAPColumn addColumn(String columnName, DBType datatype, String description, String unit, String ucd, String utype, boolean principal, boolean indexed, boolean std){ + if (columnName == null || columnName.trim().length() <= 0) return null; - TAPColumn c = new TAPColumn(columnName, description, unit, ucd, utype); - c.setVotType(votType); + TAPColumn c = new TAPColumn(columnName, datatype, description, unit, ucd, utype); c.setPrincipal(principal); c.setIndexed(indexed); c.setStd(std); @@ -234,6 +551,17 @@ public class TAPTable implements DBTable { return c; } + /** + *

    Tell whether this table contains a column with the given ADQL name.

    + * + *

    Important note: + * This function is case sensitive. + *

    + * + * @param columnName ADQL name (case sensitive) of the column whose the existence must be checked. + * + * @return true if a column having the given ADQL name exists in this table, false otherwise. + */ public final boolean hasColumn(String columnName){ if (columnName == null) return false; @@ -241,6 +569,11 @@ public class TAPTable implements DBTable { return columns.containsKey(columnName); } + /** + * Get the list of all columns contained in this table. + * + * @return An iterator over the list of this table's columns. + */ public Iterator getColumns(){ return columns.values().iterator(); } @@ -253,7 +586,7 @@ public class TAPTable implements DBTable { if (colName != null && colName.length() > 0){ Collection collColumns = columns.values(); for(TAPColumn column : collColumns){ - if (column.getDBName().equalsIgnoreCase(colName)) + if (column.getDBName().equals(colName)) return column; } } @@ -261,6 +594,18 @@ public class TAPTable implements DBTable { } } + /** + *

    Search a column inside this table having the given ADQL name.

    + * + *

    Important note: + * This function is case sensitive. + *

    + * + * @param columnName ADQL name of the column to search. + * + * @return The matching column, + * or NULL if no column with this ADQL name has been found. + */ public final TAPColumn getColumn(String columnName){ if (columnName == null) return null; @@ -268,18 +613,64 @@ public class TAPTable implements DBTable { return columns.get(columnName); } + /** + *

    Tell whether this table contains a column with the given ADQL or DB name.

    + * + *

    Note: + * This functions is just calling {@link #getColumn(String, boolean)} and compare its result + * with NULL in order to check the existence of the specified column. + *

    + * + * @param colName ADQL or DB name that the column to search must have. + * @param byAdqlName true to search the column by ADQL name, false to search by DB name. + * + * @return true if a column has been found inside this table with the given ADQL or DB name, + * false otherwise. + * + * @see #getColumn(String, boolean) + */ public boolean hasColumn(String colName, boolean byAdqlName){ return (getColumn(colName, byAdqlName) != null); } + /** + * Get the number of columns composing this table. + * + * @return Number of its columns. + */ public final int getNbColumns(){ return columns.size(); } + /** + * Tell whether this table contains no column. + * + * @return true if this table is empty (no column), + * false if it contains at least one column. + */ public final boolean isEmpty(){ return columns.isEmpty(); } + /** + *

    Remove the specified column.

    + * + *

    Important note: + * This function is case sensitive! + *

    + * + *

    Note: + * If some foreign keys were associating the column to remove, + * they will be also deleted. + *

    + * + * @param columnName ADQL name of the column to remove. + * + * @return The removed column, + * or NULL if no column with the given ADQL name has been found. + * + * @see #deleteColumnRelations(TAPColumn) + */ public final TAPColumn removeColumn(String columnName){ if (columnName == null) return null; @@ -287,9 +678,15 @@ public class TAPTable implements DBTable { TAPColumn removedColumn = columns.remove(columnName); if (removedColumn != null) deleteColumnRelations(removedColumn); + return removedColumn; } + /** + * Delete all foreign keys having the given column in the sources or the targets list. + * + * @param col A column. + */ protected final void deleteColumnRelations(TAPColumn col){ // Remove the relation between the column and this table: col.setTable(null); @@ -306,6 +703,10 @@ public class TAPTable implements DBTable { } } + /** + * Remove all columns composing this table. + * Foreign keys will also be deleted. + */ public final void removeAllColumns(){ Iterator> it = columns.entrySet().iterator(); while(it.hasNext()){ @@ -315,7 +716,31 @@ public class TAPTable implements DBTable { } } - public final void addForeignKey(TAPForeignKey key) throws Exception{ + /** + *

    Add the given foreign key to this table.

    + * + *

    Note: + * This function will do nothing if the given foreign key is NULL. + *

    + * + *

    WARNING: + * The source table ({@link TAPForeignKey#getFromTable()}) of the given foreign key MUST be this table + * and the foreign key MUST be completely defined. + * If not, an exception will be thrown and the key won't be added. + *

    + * + *

    Note: + * If the given foreign key is added to this table, all the columns of this key will be + * linked to the foreign key using either {@link TAPColumn#addSource(TAPForeignKey)} or + * {@link TAPColumn#addTarget(TAPForeignKey)}. + *

    + * + * @param key Foreign key (whose the FROM table is this table) to add inside this table. + * + * @throws TAPException If the source table of the given foreign key is not this table + * or if the given key is not completely defined. + */ + public final void addForeignKey(TAPForeignKey key) throws TAPException{ if (key == null) return; @@ -323,57 +748,120 @@ public class TAPTable implements DBTable { final String errorMsgPrefix = "Impossible to add the foreign key \"" + keyId + "\" because "; if (key.getFromTable() == null) - throw new Exception(errorMsgPrefix + "no source table is specified !"); + throw new TAPException(errorMsgPrefix + "no source table is specified !"); if (!this.equals(key.getFromTable())) - throw new Exception(errorMsgPrefix + "the source table is not \"" + getName() + "\""); + throw new TAPException(errorMsgPrefix + "the source table is not \"" + getADQLName() + "\""); if (key.getTargetTable() == null) - throw new Exception(errorMsgPrefix + "no target table is specified !"); + throw new TAPException(errorMsgPrefix + "no target table is specified !"); if (key.isEmpty()) - throw new Exception(errorMsgPrefix + "it defines no relation !"); + throw new TAPException(errorMsgPrefix + "it defines no relation !"); if (foreignKeys.add(key)){ try{ TAPTable targetTable = key.getTargetTable(); for(Map.Entry relation : key){ if (!hasColumn(relation.getKey())) - throw new Exception(errorMsgPrefix + "the source column \"" + relation.getKey() + "\" doesn't exist in \"" + getName() + "\" !"); + throw new TAPException(errorMsgPrefix + "the source column \"" + relation.getKey() + "\" doesn't exist in \"" + getName() + "\" !"); else if (!targetTable.hasColumn(relation.getValue())) - throw new Exception(errorMsgPrefix + "the target column \"" + relation.getValue() + "\" doesn't exist in \"" + targetTable.getName() + "\" !"); + throw new TAPException(errorMsgPrefix + "the target column \"" + relation.getValue() + "\" doesn't exist in \"" + targetTable.getName() + "\" !"); else{ getColumn(relation.getKey()).addTarget(key); targetTable.getColumn(relation.getValue()).addSource(key); } } - }catch(Exception ex){ + }catch(TAPException ex){ foreignKeys.remove(key); throw ex; } } } - public TAPForeignKey addForeignKey(String keyId, TAPTable targetTable, Map columns) throws Exception{ + /** + *

    Build a foreign key using the ID, the target table and the given list of columns. + * Then, add the created foreign key to this table.

    + * + *

    Note: + * The source table of the created foreign key ({@link TAPForeignKey#getFromTable()}) will be this table. + *

    + * + *

    Note: + * If the given foreign key is added to this table, all the columns of this key will be + * linked to the foreign key using either {@link TAPColumn#addSource(TAPForeignKey)} or + * {@link TAPColumn#addTarget(TAPForeignKey)}. + *

    + * + * @return The created and added foreign key. + * + * @throws TAPException If the specified key is not completely or correctly defined. + * + * @see TAPForeignKey#TAPForeignKey(String, TAPTable, TAPTable, Map) + */ + public TAPForeignKey addForeignKey(String keyId, TAPTable targetTable, Map columns) throws TAPException{ TAPForeignKey key = new TAPForeignKey(keyId, this, targetTable, columns); addForeignKey(key); return key; } - public TAPForeignKey addForeignKey(String keyId, TAPTable targetTable, Map columns, String description, String utype) throws Exception{ + /** + *

    Build a foreign key using the ID, the target table, the given list of columns, the given description and the given UType. + * Then, add the created foreign key to this table.

    + * + *

    Note: + * The source table of the created foreign key ({@link TAPForeignKey#getFromTable()}) will be this table. + *

    + * + *

    Note: + * If the given foreign key is added to this table, all the columns of this key will be + * linked to the foreign key using either {@link TAPColumn#addSource(TAPForeignKey)} or + * {@link TAPColumn#addTarget(TAPForeignKey)}. + *

    + * + * @return The created and added foreign key. + * + * @throws TAPException If the specified key is not completely or correctly defined. + * + * @see TAPForeignKey#TAPForeignKey(String, TAPTable, TAPTable, Map, String, String) + */ + public TAPForeignKey addForeignKey(String keyId, TAPTable targetTable, Map columns, String description, String utype) throws TAPException{ TAPForeignKey key = new TAPForeignKey(keyId, this, targetTable, columns, description, utype); addForeignKey(key); return key; } + /** + * Get the list of all foreign keys associated whose the source is this table. + * + * @return An iterator over all its foreign keys. + */ public final Iterator getForeignKeys(){ return foreignKeys.iterator(); } + /** + * Get the number of all foreign keys whose the source is this table + * + * @return Number of all its foreign keys. + */ public final int getNbForeignKeys(){ return foreignKeys.size(); } + /** + *

    Remove the given foreign key from this table.

    + * + *

    Note: + * This function will also delete the link between the columns of the foreign key + * and the foreign key, using {@link #deleteRelations(TAPForeignKey)}. + *

    + * + * @param keyToRemove Foreign key to removed from this table. + * + * @return true if the key has been successfully removed, + * false otherwise. + */ public final boolean removeForeignKey(TAPForeignKey keyToRemove){ if (foreignKeys.remove(keyToRemove)){ deleteRelations(keyToRemove); @@ -382,6 +870,14 @@ public class TAPTable implements DBTable { return false; } + /** + *

    Remove all the foreign keys whose the source is this table.

    + * + *

    Note: + * This function will also delete the link between the columns of all the removed foreign keys + * and the foreign keys, using {@link #deleteRelations(TAPForeignKey)}. + *

    + */ public final void removeAllForeignKeys(){ Iterator it = foreignKeys.iterator(); while(it.hasNext()){ @@ -390,6 +886,13 @@ public class TAPTable implements DBTable { } } + /** + * Delete the link between all columns of the given foreign key + * and this foreign key. Thus, these columns won't be anymore source or target + * of this foreign key. + * + * @param key A foreign key whose links with its columns must be deleted. + */ protected final void deleteRelations(TAPForeignKey key){ for(Map.Entry relation : key){ TAPColumn col = key.getFromTable().getColumn(relation.getKey()); @@ -426,69 +929,17 @@ public class TAPTable implements DBTable { @Override public String toString(){ - return ((schema != null) ? (schema.getName() + ".") : "") + adqlName; - } - - public static void main(String[] args) throws Exception{ - TAPSchema schema1 = new TAPSchema("monSchema1"); - TAPSchema schema2 = new TAPSchema("monSchema2"); - - TAPTable tRef = schema1.addTable("ToRef"); - tRef.addColumn("monMachin"); - - TAPTable t = schema2.addTable("Test"); - t.addColumn("machin"); - t.addColumn("truc"); - HashMap mapCols = new HashMap(); - mapCols.put("machin", "monMachin"); - TAPForeignKey key = new TAPForeignKey("KeyID", t, tRef, mapCols); - t.addForeignKey(key); - mapCols = new HashMap(); - mapCols.put("truc", "monMachin"); - key = new TAPForeignKey("2ndKey", t, tRef, mapCols); - t.addForeignKey(key); - - printSchema(schema1); - printSchema(schema2); - - System.out.println(); - - schema2.removeTable("Test"); - printSchema(schema1); - printSchema(schema2); - } - - public static void printSchema(TAPSchema schema){ - System.out.println("*** SCHEMA \"" + schema.getName() + "\" ***"); - for(TAPTable t : schema) - printTable(t); - } - - public static void printTable(TAPTable t){ - System.out.println("TABLE: " + t + "\nNb Columns: " + t.getNbColumns() + "\nNb Relations: " + t.getNbForeignKeys()); - Iterator it = t.getColumns(); - while(it.hasNext()){ - TAPColumn col = it.next(); - System.out.print("\t- " + col + "( "); - Iterator keys = col.getTargets(); - while(keys.hasNext()) - for(Map.Entry relation : keys.next()) - System.out.print(">" + relation.getKey() + "/" + relation.getValue() + " "); - keys = col.getSources(); - while(keys.hasNext()) - for(Map.Entry relation : keys.next()) - System.out.print("<" + relation.getKey() + "/" + relation.getValue() + " "); - System.out.println(")"); - } + return ((schema != null) ? (schema.getADQLName() + ".") : "") + adqlName; } + @Override public DBTable copy(final String dbName, final String adqlName){ TAPTable copy = new TAPTable((adqlName == null) ? this.adqlName : adqlName); copy.setDBName((dbName == null) ? this.dbName : dbName); copy.setSchema(schema); Collection collColumns = columns.values(); for(TAPColumn col : collColumns) - copy.addColumn((TAPColumn)col.copy()); + copy.addColumn((TAPColumn)col.copy(col.getDBName(), col.getADQLName(), null)); copy.setDescription(description); copy.setOtherData(otherData); copy.setType(type); diff --git a/src/tap/metadata/TAPTypes.java b/src/tap/metadata/TAPTypes.java deleted file mode 100644 index 423b854..0000000 --- a/src/tap/metadata/TAPTypes.java +++ /dev/null @@ -1,359 +0,0 @@ -package tap.metadata; - -/* - * This file is part of TAPLibrary. - * - * TAPLibrary is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * TAPLibrary is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with TAPLibrary. If not, see . - * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) - */ - -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; -import java.util.Map.Entry; - -/** - *

    - * Gathers all types used by a TAP service and described in the IVOA document for TAP. - * This class lets "translating" a DB type into a VOTable field type and vice-versa. - * You can also add some DB type aliases, that's to say other other names for the existing DB types: - * smallint, integer, bigint, real, double, binary, varbinary, char, varchar, blob, clob, timestamp, point, region. - * For instance: TEXT <-> VARCHAR. - *

    - * - * @author Grégory Mantelet (CDS) - * @version 11/2011 - * - * @see VotType - */ -public final class TAPTypes { - - private static final Map dbTypes; - private static final Map dbTypeAliases; - private static final Map votTypes; - - public static final String SMALLINT = "SMALLINT"; - public static final String INTEGER = "INTEGER"; - public static final String BIGINT = "BIGINT"; - public static final String REAL = "REAL"; - public static final String DOUBLE = "DOUBLE"; - public static final String BINARY = "BINARY"; - public static final String VARBINARY = "VARBINARY"; - public static final String CHAR = "CHAR"; - public static final String VARCHAR = "VARCHAR"; - public static final String BLOB = "BLOB"; - public static final String CLOB = "CLOB"; - public static final String TIMESTAMP = "TIMESTAMP"; - public static final String POINT = "POINT"; - public static final String REGION = "REGION"; - - /** No array size. */ - public static final int NO_SIZE = -1; - - /** Means '*' (i.e. char(*)). */ - public static final int STAR_SIZE = -12345; - - static{ - dbTypes = new HashMap(14); - votTypes = new HashMap(7); - - VotType type = new VotType("short", 1, null); - dbTypes.put(SMALLINT, type); - votTypes.put(type, SMALLINT); - - type = new VotType("int", 1, null); - dbTypes.put(INTEGER, type); - votTypes.put(type, INTEGER); - - type = new VotType("long", 1, null); - dbTypes.put(BIGINT, type); - votTypes.put(type, BIGINT); - - type = new VotType("float", 1, null); - dbTypes.put(REAL, type); - votTypes.put(type, REAL); - - type = new VotType("double", 1, null); - dbTypes.put(DOUBLE, type); - votTypes.put(type, DOUBLE); - - dbTypes.put(BINARY, new VotType("unsignedByte", 1, null)); - - type = new VotType("unsignedByte", STAR_SIZE, null); - dbTypes.put(VARBINARY, type); - votTypes.put(type, VARBINARY); - - dbTypes.put(CHAR, new VotType("char", 1, null)); - - type = new VotType("char", STAR_SIZE, null); - dbTypes.put(VARCHAR, type); - votTypes.put(type, VARCHAR); - - type = new VotType("unsignedByte", STAR_SIZE, "adql:BLOB"); - dbTypes.put(BLOB, type); - votTypes.put(type, BLOB); - - type = new VotType("char", STAR_SIZE, "adql:CLOB"); - dbTypes.put(CLOB, type); - votTypes.put(type, CLOB); - - type = new VotType("char", STAR_SIZE, "adql:TIMESTAMP"); - dbTypes.put(TIMESTAMP, type); - votTypes.put(type, TIMESTAMP); - - type = new VotType("char", STAR_SIZE, "adql:POINT"); - dbTypes.put(POINT, type); - votTypes.put(type, POINT); - - type = new VotType("char", STAR_SIZE, "adql:REGION"); - dbTypes.put(REGION, type); - votTypes.put(type, REGION); - - dbTypeAliases = new HashMap(8); - // PostgreSQL data types: - dbTypeAliases.put("INT2", SMALLINT); - dbTypeAliases.put("INT", INTEGER); - dbTypeAliases.put("INT4", INTEGER); - dbTypeAliases.put("INT8", BIGINT); - dbTypeAliases.put("FLOAT4", REAL); - dbTypeAliases.put("FLOAT8", DOUBLE); - dbTypeAliases.put("TEXT", VARCHAR); - dbTypeAliases.put("SPOINT", POINT); - } - - /** - * Gets all DB types. - * @return An iterator on DB type name. - */ - public static final Iterator getDBTypes(){ - return dbTypes.keySet().iterator(); - } - - /** - * Gets all DB type aliases. - * @return An iterator on Entry<String,String> whose the key is the alias and the value is its corresponding DB type. - */ - public static final Iterator> getDBTypeAliases(){ - return dbTypeAliases.entrySet().iterator(); - } - - /** - * Gets all VOTable types. - * @return An iterator on {@link VotType}. - */ - public static final Iterator getVotTypes(){ - return votTypes.keySet().iterator(); - } - - /** - *

    Gets the VOTable type corresponding to the given DB type (or a DB type alias).

    - * Important: - *
      - *
    • Spaces before and after the DB type are automatically removed,
    • - *
    • The DB type is automatically formatted in UPPER-CASE,
    • - *
    • Nothing is done if the given DB type is null or empty.
    • - *
    - * - * @param dbType A DB type (ex: SMALLINT, INTEGER, VARCHAR, POINT, ...) - * - * @return The corresponding VOTable type or null if not found. - */ - public static final VotType getVotType(String dbType){ - if (dbType == null) - return null; - - // Normalize the type name (upper case and with no leading and trailing spaces): - dbType = dbType.trim().toUpperCase(); - if (dbType.length() == 0) - return null; - - // Search the corresponding VOTable type: - VotType votType = dbTypes.get(dbType); - // If no match, try again considering the given type as an alias: - if (votType == null) - votType = dbTypes.get(dbTypeAliases.get(dbType)); - - return votType; - } - - /** - *

    Gets the VOTable type (with the given arraysize) corresponding to the given DB type (or a DB type alias).

    - * Important: - *
      - *
    • Spaces before and after the DB type are automatically removed,
    • - *
    • The DB type is automatically formatted in UPPER-CASE,
    • - *
    • Nothing is done if the given DB type is null or empty,
    • - *
    • The given arraysize is used only if the found VOTable type is not special (that's to say: xtype is null).
    • - *
    - * - * @param dbType A DB type (ex: SMALLINT, INTEGER, VARCHAR, POINT, ...) - * @param arraysize Arraysize to set in the found VOTable type. - * - * @return The corresponding VOTable type or null if not found. - */ - public static final VotType getVotType(String dbType, int arraysize){ - VotType votType = getVotType(dbType); - - // If there is a match, set the arraysize: - if (votType != null && votType.xtype == null && arraysize > 0) - votType = new VotType(votType.datatype, arraysize, null); - - return votType; - } - - /** - * - *

    Gets the DB type corresponding to the given DB type alias.

    - * Important: - *
      - *
    • Spaces before and after the DB type are automatically removed,
    • - *
    • The DB type is automatically formatted in UPPER-CASE,
    • - *
    • If the given DB type is not alias but directly a DB type, it is immediately return.
    • - *
    - * - * @param dbTypeAlias A DB type alias. - * - * @return The corresponding DB type or null if not found. - */ - public static final String getDBType(String dbTypeAlias){ - if (dbTypeAlias == null) - return null; - - // Normalize the type name: - dbTypeAlias = dbTypeAlias.trim().toUpperCase(); - if (dbTypeAlias.length() == 0) - return null; - - // Get the corresponding DB type: - if (dbTypes.containsKey(dbTypeAlias)) - return dbTypeAlias; - else - return dbTypeAliases.get(dbTypeAlias); - } - - /** - * - *

    Gets the DB type corresponding to the given VOTable field type.

    - * Important: - *
      - *
    • The research is made only on the following fields: datatype and xtype,
    • - *
    • Case insensitive research.
    • - *
    - * - * @param type A VOTable type. - * - * @return The corresponding DB type or null if not found. - */ - public static final String getDBType(final VotType type){ - if (type == null) - return null; - return votTypes.get(type); - } - - /** - *

    Adds, replaces or removes a DB type alias.

    - * Important: - *
      - *
    • Spaces before and after the DB type are automatically removed,
    • - *
    • The DB type is automatically formatted in UPPER-CASE,
    • - *
    • The same "normalizations" are done on the given alias (so the case sensitivity is ignored),
    • - *
    • Nothing is done if the given alias is null or empty,
    • - *
    • If the given DB type is null, the given alias is removed,
    • - *
    • Nothing is done if the given DB type (!= null) does not match with a known DB type.
    • - *
    - * - * @param alias A DB type alias (ex: spoint) - * @param dbType A DB type (ex: POINT). - * - * @return true if the association has been updated, false otherwise. - */ - public static final boolean putDBTypeAlias(String alias, String dbType){ - if (alias == null) - return false; - - // Normalize the given alias: - alias = alias.trim().toUpperCase(); - if (alias.length() == 0) - return false; - - // Check the existence of the given DB type: - if (dbType != null){ - dbType = dbType.trim().toUpperCase(); - if (dbType.length() == 0) - return false; - else if (!dbTypes.containsKey(dbType)) - return false; - } - - // Update the map of aliases: - if (dbType == null) - dbTypeAliases.remove(alias); - else - dbTypeAliases.put(alias, dbType); - - return true; - } - - /** SELF TEST */ - public final static void main(final String[] args) throws Exception{ - System.out.println("***** DB TYPES *****"); - Iterator itDB = TAPTypes.getDBTypes(); - while(itDB.hasNext()) - System.out.println("\t- " + itDB.next()); - - System.out.println("\n***** DB TYPE ALIASES *****"); - Iterator> itAliases = TAPTypes.getDBTypeAliases(); - while(itAliases.hasNext()){ - Entry e = itAliases.next(); - System.out.println("\t- " + e.getKey() + " = " + e.getValue()); - } - - System.out.println("\n***** VOTABLE TYPES *****"); - Iterator itVot = TAPTypes.getVotTypes(); - while(itVot.hasNext()) - System.out.println("\t- " + itVot.next()); - - byte[] buffer = new byte[1024]; - int nbRead = 0; - String type = null; - - System.out.print("\nDB Type ? "); - nbRead = System.in.read(buffer); - type = new String(buffer, 0, nbRead); - System.out.println(TAPTypes.getVotType(type)); - - int arraysize = 1; - String xtype = null; - VotType votType = null; - System.out.print("\nVOTable datatype ? "); - nbRead = System.in.read(buffer); - type = (new String(buffer, 0, nbRead)).trim(); - System.out.print("VOTable arraysize ? "); - nbRead = System.in.read(buffer); - try{ - arraysize = Integer.parseInt((new String(buffer, 0, nbRead)).trim()); - }catch(NumberFormatException nfe){ - arraysize = STAR_SIZE; - } - System.out.print("VOTable xtype ? "); - nbRead = System.in.read(buffer); - xtype = (new String(buffer, 0, nbRead)).trim(); - if (xtype != null && xtype.length() == 0) - xtype = null; - votType = new VotType(type, arraysize, xtype); - System.out.println(TAPTypes.getDBType(votType)); - } - -} diff --git a/src/tap/metadata/TableSetParser.java b/src/tap/metadata/TableSetParser.java new file mode 100644 index 0000000..0d6e1a5 --- /dev/null +++ b/src/tap/metadata/TableSetParser.java @@ -0,0 +1,905 @@ +package tap.metadata; + +/* + * This file is part of TAPLibrary. + * + * TAPLibrary is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * TAPLibrary is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with TAPLibrary. If not, see . + * + * Copyright 2015 - Astronomisches Rechen Institut (ARI) + */ + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamConstants; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; + +import org.xml.sax.helpers.DefaultHandler; + +import tap.TAPException; +import tap.data.VOTableIterator; +import tap.metadata.TAPTable.TableType; +import adql.db.DBType; +import adql.db.DBType.DBDatatype; + +/** + *

    Let parse an XML document representing a table set, and return the corresponding {@link TAPMetadata} instance.

    + * + *

    Note 1: the table set must follow the syntax specified by the XML Schema http://www.ivoa.net/xml/VODataService.

    + *

    Note 2: only tags specified by VODataService are checked. If there is any other tag, they are merely ignored.

    + * + *

    Exceptions

    + * + *

    A {@link TAPException} is thrown in the following cases:

    + *
      + *
    • the root node is not "tableset"
    • + *
    • table name syntax ([schema.]table) is incorrect
    • + *
    • a single table name (just "table" without schema prefix) is ambiguous (that's to say, the same name is used for tables of different schemas)
    • + *
    • "name" node is missing in nodes "schema", "table" and "column"
    • + *
    • "targetTable" is missing in node "foreignKey"
    • + *
    • "fromColumn" or "targetColumn" is missing in node "fkColumn"
    • + *
    • "name" node is duplicated in the same node
    • + *
    • missing "xsi:type" as attribute in a "dataType" node
    • + *
    • unknown column datatype
    • + *
    + * + *

    Note: catalog prefixes are not supported in this parser.

    + * + *

    Datatype

    + * + *

    + * A column datatype may be specified either as a TAP or a VOTable datatype. Thus, the type of specification must be given with the attribute xsi:type of the + * node "dataType". For instance: + *

    + *
      + *
    • <dataType xsi:type="vs:VOTableType" arraysize="1">float</dataType> for a VOTable datatype
    • + *
    • <dataType xsi:type="vod:TAPType">VARCHAR</dataType> for a TAP datatype
    • + *
    + * + * @author Grégory Mantelet (ARI) + * @version 2.0 (02/2015) + * @since 2.0 + */ +public class TableSetParser extends DefaultHandler { + + /** XML namespace for the XML schema XMLSchema-instance. */ + protected final static String XSI_NAMESPACE = "http://www.w3.org/2001/XMLSchema-instance"; + + /** XML namespace for the XML schema VODataService. */ + protected final static String VODATASERVICE_NAMESPACE = "http://www.ivoa.net/xml/VODataService"; + + /** + *

    Intermediary representation of a Foreign Key.

    + * + *

    + * An instance of this class lets save all information provided in the XML document and needed to create the corresponding TAP metadata ({@link TAPForeignKey}) + * at the end of XML document parsing, once all available tables are listed. + *

    + * + * @author Grégory Mantelet (ARI) + * @version 2.0 (02/2015) + * @since 2.0 + * + * @see TableSetParser#parseFKey(XMLStreamReader) + * @see TableSetParser#parse(InputStream) + */ + protected static class ForeignKey { + /** Foreign key description */ + public String description = null; + /** UType associated with this foreign key. */ + public String utype = null; + /** Source table of the foreign key. + * Note: In the XML document, the foreign key is described inside its table ; + * hence the type of this attribute: TAPTable (it is indeed already known). */ + public TAPTable fromTable = null; + /** Target table of the foreign key. */ + public String targetTable = null; + /** Position of the "targetTable" node inside the XML document. + * Note: this attribute may be used only in case of error. */ + public String targetTablePosition = ""; + /** Columns associations. + * Keys are columns of the source table, whereas values are columns of the target table to associate with. */ + public Map keyColumns = new HashMap(); + } + + /** + * Parse the XML TableSet stored in the specified file. + * + * @param file The regular file containing the TableSet to parse. + * + * @return The corresponding TAP metadata. + * + * @throws IOException If any error occurs while reading the given file. + * @throws TAPException If any error occurs in the XML parsing or in the TAP metadata creation. + * + * @since {@link #parse(InputStream)} + */ + public TAPMetadata parse(final File file) throws IOException, TAPException{ + InputStream input = null; + try{ + input = new BufferedInputStream(new FileInputStream(file)); + return parse(input); + }finally{ + if (input != null){ + try{ + input.close(); + }catch(IOException ioe2){} + } + } + } + + /** + * Parse the XML TableSet stored in the given stream. + * + * @param input The stream containing the TableSet to parse. + * + * @return The corresponding TAP metadata. + * + * @throws IOException If any error occurs while reading the given stream. + * @throws TAPException If any error occurs in the XML parsing or in the TAP metadata creation. + * + * @see #parseSchema(XMLStreamReader, List) + */ + public TAPMetadata parse(final InputStream input) throws IOException, TAPException{ + TAPMetadata meta = null; + + XMLInputFactory factory = XMLInputFactory.newInstance(); + XMLStreamReader reader = null; + + try{ + // Create the XML streaming reader: + reader = factory.createXMLStreamReader(input); + + // Read the first XML tag => MUST BE : + int event = nextTag(reader); + if (event == XMLStreamConstants.START_ELEMENT && reader.getLocalName().equalsIgnoreCase("tableset")){ + + // Build the metadata object: + meta = new TAPMetadata(); + + // Prepare the listing of all foreign keys for a later resolution: + ArrayList allForeignKeys = new ArrayList(20); + + // Read the next XML tag => MUST BE : + while(reader.hasNext() && (event = nextTag(reader)) == XMLStreamConstants.START_ELEMENT){ + if (reader.getLocalName().equalsIgnoreCase("schema")){ + // fetch the schema description and content: + meta.addSchema(parseSchema(reader, allForeignKeys)); + } + } + + // Read the final XML tag => MUST BE : + if (event != XMLStreamConstants.END_ELEMENT || !reader.getLocalName().equalsIgnoreCase("tableset")){ + // throw an error if the tag is not the expected one: + throw new TAPException(getPosition(reader) + " XML tag mismatch: <" + (event == XMLStreamConstants.END_ELEMENT ? "/" : "") + reader.getLocalName() + ">! Expected: ."); + } + + // Resolve all ForeignKey objects into TAPForeignKeys and add them into the dedicated TAPTable: + long keyId = 0; + for(ForeignKey fk : allForeignKeys){ + // search for the target table: + TAPTable targetTable = searchTable(fk.targetTable, meta, fk.targetTablePosition); + // build and add the foreign key: + fk.fromTable.addForeignKey("" + (++keyId), targetTable, fk.keyColumns, fk.description, fk.utype); + } + + }else + throw new TAPException(getPosition(reader) + " Missing root tag: \"tableset\"!"); + + }catch(XMLStreamException xse){ + throw new TAPException(getPosition(reader) + " XML ERROR: " + xse.getMessage() + "!", xse); + } + + return meta; + } + + /* **************************** */ + /* INDIVIDUAL PARSING FUNCTIONS */ + /* **************************** */ + + /** + *

    Parse the XML representation of a TAP schema.

    + * + *

    Important: This function MUST be called just after the start element "schema" has been read!

    + * + *

    Attributes

    + * + *

    No attribute is expected in the start element "schema".

    + * + *

    Children

    + * + * Only the following nodes are taken into account ; the others are ignored: + *
      + *
    • name REQUIRED
    • + *
    • description {0..1}
    • + *
    • title {0..1}
    • + *
    • utype {0..1}
    • + *
    • table {*}
    • + *
    + * + * @param reader XML reader. + * @param allForeignKeys List to fill with all encountered foreign keys. + * note: these keys are not the final TAP meta, but a collection of all information found in the XML document. + * The final TAP meta will be created later, once all available tables and columns are available. + * @throws IllegalStateException If this function is called while the reader has not just read the START ELEMENT tag of "table". + * + * @return The corresponding TAP schema. + * + * @throws XMLStreamException If there is an error processing the underlying XML source. + * @throws TAPException If several "name" nodes are found, or if none such node is found ; exactly one "name" node must be found. + * + * @see #parseTable(XMLStreamReader, List) + */ + protected TAPSchema parseSchema(final XMLStreamReader reader, final List allForeignKeys) throws XMLStreamException, TAPException{ + // Ensure the reader has just read the START ELEMENT of schema: + if (reader.getEventType() != XMLStreamConstants.START_ELEMENT || reader.getLocalName() == null || !reader.getLocalName().equalsIgnoreCase("schema")) + throw new IllegalStateException(getPosition(reader) + " Illegal usage of TableSetParser.parseSchema(XMLStreamParser)! This function can be called only when the reader has just read the START ELEMENT tag \"schema\"."); + + TAPSchema schema = null; + String tag = null, name = null, description = null, title = null, utype = null; + ArrayList tables = new ArrayList(10); + + while(nextTag(reader) == XMLStreamConstants.START_ELEMENT){ + // Get the tag name: + tag = reader.getLocalName(); + + // Identify the current tag: + if (tag.equalsIgnoreCase("name")){ + if (name != null) + throw new TAPException(getPosition(reader) + " Only one \"name\" element can exist in a /tableset/schema!"); + name = getText(reader); + }else if (tag.equalsIgnoreCase("description")) + description = ((description != null) ? (description + "\n") : "") + getText(reader); + else if (tag.equalsIgnoreCase("table")){ + ArrayList keys = new ArrayList(2); + tables.add(parseTable(reader, keys)); + allForeignKeys.addAll(keys); + }else if (tag.equalsIgnoreCase("title")) + title = ((title != null) ? (title + "\n") : "") + getText(reader); + else if (tag.equalsIgnoreCase("utype")) + utype = getText(reader); + } + + // Only one info is required: the schema name! + if (name == null) + throw new TAPException(getPosition(reader) + " Missing schema \"name\"!"); + + // Build the schema: + schema = new TAPSchema(name, description, utype); + schema.setTitle(title); + for(TAPTable t : tables) + schema.addTable(t); + tables = null; + + return schema; + } + + /** + *

    Parse the XML representation of a TAP table.

    + * + *

    Important: This function MUST be called just after the start element "table" has been read!

    + * + *

    Attributes

    + * + * The attribute "type" may be provided in the start element "table". One of the following value is expected: + *
      + *
    • base_table or table
    • + *
    • output
    • + *
    • view
    • + *
    + * + *

    Children

    + * + * Only the following nodes are taken into account ; the others are ignored: + *
      + *
    • name REQUIRED
    • + *
    • description {0..1}
    • + *
    • title {0..1}
    • + *
    • utype {0..1}
    • + *
    • column {*}
    • + *
    • foreignKey {*}
    • + *
    + * + * @param reader XML reader. + * @param keys List to fill with all encountered foreign keys. + * note: these keys are not the final TAP meta, but a collection of all information found in the XML document. + * The final TAP meta will be created later, once all available tables and columns are available. + * + * @return The corresponding TAP table. + * + * @throws XMLStreamException If there is an error processing the underlying XML source. + * @throws TAPException If several "name" nodes are found, or if none such node is found ; exactly one "name" node must be found. + * @throws IllegalStateException If this function is called while the reader has not just read the START ELEMENT tag of "table". + * + * @see #parseColumn(XMLStreamReader) + * @see #parseFKey(XMLStreamReader) + */ + protected TAPTable parseTable(final XMLStreamReader reader, final List keys) throws XMLStreamException, TAPException{ + // Ensure the reader has just read the START ELEMENT of table: + if (reader.getEventType() != XMLStreamConstants.START_ELEMENT || reader.getLocalName() == null || !reader.getLocalName().equalsIgnoreCase("table")) + throw new IllegalStateException(getPosition(reader) + " Illegal usage of TableSetParser.parseTable(XMLStreamParser)! This function can be called only when the reader has just read the START ELEMENT tag \"table\"."); + + TAPTable table = null; + TableType type = TableType.table; + String tag = null, name = null, description = null, title = null, utype = null; + ArrayList columns = new ArrayList(10); + + // Get the table type (attribute "type") [OPTIONAL] : + if (reader.getAttributeCount() > 0){ + int indType = 0; + while(indType < reader.getAttributeCount() && !reader.getAttributeLocalName(indType).equalsIgnoreCase("type")) + indType++; + if (indType < reader.getAttributeCount() && reader.getAttributeLocalName(indType).equalsIgnoreCase("type")){ + String typeTxt = reader.getAttributeValue(indType); + if (typeTxt != null && typeTxt.trim().length() > 0){ + typeTxt = typeTxt.trim().toLowerCase(); + try{ + if (typeTxt.equals("base_table")) + type = TableType.table; + else + type = TableType.valueOf(typeTxt); + }catch(IllegalArgumentException iae){ + /* Note: If type unknown, the given value is ignored and the default type - TableType.table - is kept. */ + } + } + } + } + + // Fetch the other information (tags): + while(nextTag(reader) == XMLStreamConstants.START_ELEMENT){ + // Get the tag name: + tag = reader.getLocalName(); + + // Identify the current tag: + if (tag.equalsIgnoreCase("name")){ + if (name != null) + throw new TAPException(getPosition(reader) + " Only one \"name\" element can exist in a /tableset/schema/table!"); + name = getText(reader); + }else if (tag.equalsIgnoreCase("description")) + description = ((description != null) ? (description + "\n") : "") + getText(reader); + else if (tag.equalsIgnoreCase("column")){ + columns.add(parseColumn(reader)); + }else if (tag.equalsIgnoreCase("foreignKey")) + keys.add(parseFKey(reader)); + else if (tag.equalsIgnoreCase("title")) + title = ((title != null) ? (title + "\n") : "") + getText(reader); + else if (tag.equalsIgnoreCase("utype")) + utype = getText(reader); + } + + // Only one info is required: the table name! + if (name == null) + throw new TAPException(getPosition(reader) + " Missing table \"name\"!"); + + // Build the table: + table = new TAPTable(name, type, description, utype); + table.setTitle(title); + for(TAPColumn c : columns) + table.addColumn(c); + for(ForeignKey k : keys) + k.fromTable = table; + + return table; + } + + /** + *

    Parse the XML representation of a TAP column.

    + * + *

    Important: This function MUST be called just after the start element "column" has been read!

    + * + *

    Attributes

    + * + * The attribute "std" may be provided in the start element "column". One of the following value is expected: + *
      + *
    • false (default value if the attribute is omitted)
    • + *
    • true
    • + *
    + * + *

    Children

    + * + * Only the following nodes are taken into account ; the others are ignored: + *
      + *
    • name REQUIRED
    • + *
    • description {0..1}
    • + *
    • ucd {0..1}
    • + *
    • unit {0..1}
    • + *
    • utype {0..1}
    • + *
    • dataType {0..1}
    • + *
    • flag {*}, but only the values 'nullable', 'indexed' and 'primary' are currently supported by the library)
    • + *
    + * + * @param reader XML reader. + * + * @return The corresponding TAP column. + * + * @throws XMLStreamException If there is an error processing the underlying XML source. + * @throws TAPException If several "name" nodes are found, or if none such node is found ; exactly one "name" node must be found. + * @throws IllegalStateException If this function is called while the reader has not just read the START ELEMENT tag of "column". + * + * @see #parseDataType(XMLStreamReader) + */ + protected TAPColumn parseColumn(final XMLStreamReader reader) throws XMLStreamException, TAPException{ + // Ensure the reader has just read the START ELEMENT of column: + if (reader.getEventType() != XMLStreamConstants.START_ELEMENT || reader.getLocalName() == null || !reader.getLocalName().equalsIgnoreCase("column")) + throw new IllegalStateException(getPosition(reader) + " Illegal usage of TableSetParser.parseColumn(XMLStreamParser)! This function can be called only when the reader has just read the START ELEMENT tag \"column\"."); + + TAPColumn column = null; + boolean std = false, indexed = false, primary = false, nullable = false; + String tag = null, name = null, description = null, unit = null, ucd = null, utype = null; + DBType type = null; + + // Get the column STD flag (attribute "std") [OPTIONAL] : + if (reader.getAttributeCount() > 0){ + int indType = 0; + while(indType < reader.getAttributeCount() && !reader.getAttributeLocalName(indType).equalsIgnoreCase("std")) + indType++; + if (indType < reader.getAttributeCount() && reader.getAttributeLocalName(indType).equalsIgnoreCase("std")){ + String stdTxt = reader.getAttributeValue(indType); + if (stdTxt != null) + std = Boolean.parseBoolean(stdTxt.trim().toLowerCase()); + } + } + + // Fetch the other information (tags): + while(nextTag(reader) == XMLStreamConstants.START_ELEMENT){ + // Get the tag name: + tag = reader.getLocalName(); + + // Identify the current tag: + if (tag.equalsIgnoreCase("name")){ + if (name != null) + throw new TAPException(getPosition(reader) + " Only one \"name\" element can exist in a /tableset/schema/table/column!"); + name = getText(reader); + }else if (tag.equalsIgnoreCase("description")) + description = ((description != null) ? (description + "\n") : "") + getText(reader); + else if (tag.equalsIgnoreCase("dataType")) + type = parseDataType(reader); + else if (tag.equalsIgnoreCase("unit")) + unit = getText(reader); + else if (tag.equalsIgnoreCase("ucd")) + ucd = getText(reader); + else if (tag.equalsIgnoreCase("utype")) + utype = getText(reader); + else if (tag.equalsIgnoreCase("flag")){ + String txt = getText(reader); + if (txt != null){ + if (txt.equalsIgnoreCase("indexed")) + indexed = true; + else if (txt.equalsIgnoreCase("primary")) + primary = true; + else if (txt.equalsIgnoreCase("nullable")) + nullable = true; + } + } + } + + // Only one info is required: the table name! + if (name == null) + throw new TAPException(getPosition(reader) + " Missing column \"name\"!"); + + // Build the column: + column = new TAPColumn(name, type, description, unit, ucd, utype); + column.setStd(std); + column.setIndexed(indexed); + column.setPrincipal(primary); + column.setNullable(nullable); + + return column; + } + + /** + *

    Parse the XML representation of a column datatype.

    + * + *

    Important: This function MUST be called just after the start element "dataType" has been read!

    + * + *

    Attributes

    + * + * The attribute "xsi:type" (where xsi = http://www.w3.org/2001/XMLSchema-instance) MUST be provided. Only the following values are supported and accepted + * (below, vs = http://www.ivoa.net/xml/VODataService): + *
      + *
    • vs:VOTableType, and the following attributes may be also provided: + *
        + *
      • arraysize
      • + *
      • xtype
      • + *
    • + *
    • vs:TAPType, and the attribute "size" may be also provided
    • + *
    + * + *

    Children

    + * + * No child, but a text MUST be provided. Its value depends of the attribute "xsi:type": a VOTable datatype (e.g. char, float, short) if "xsi:type=vs:VOTableType", + * or a TAP type (e.g. VARCHAR, REAL, SMALLINT) if "xsi:type=vs:TAPType". Any other value will be rejected. + * + *

    IMPORTANT: All VOTable datatypes will be converted into TAPType automatically by the library.

    + * + * @param reader XML reader. + * + * @return The corresponding column datatype. + * + * @throws XMLStreamException If there is an error processing the underlying XML source. + * @throws TAPException If the attribute "xsi:type" is missing or incorrect, + * or if the datatype is unknown or not supported. + * @throws IllegalStateException If this function is called while the reader has not just read the START ELEMENT tag of "dataType". + * + * @see VOTableIterator#resolveVotType(String, String, String) + * @see DBType#DBType(DBDatatype, int) + */ + protected DBType parseDataType(final XMLStreamReader reader) throws XMLStreamException, TAPException{ + // Ensure the reader has just read the START ELEMENT of dataType: + if (reader.getEventType() != XMLStreamConstants.START_ELEMENT || reader.getLocalName() == null || !reader.getLocalName().equalsIgnoreCase("dataType")) + throw new IllegalStateException(getPosition(reader) + " Illegal usage of TableSetParser.parseDataType(XMLStreamParser)! This function can be called only when the reader has just read the START ELEMENT tag \"dataType\"."); + + String typeOfType = null, datatype = null, size = null, xtype = null, arraysize = null; + + /* Note: + * The 1st parameter of XMLStreamReader.getAttributeValue(String, String) should be the namespace of the attribute. + * If this value is NULL, the namespace condition is ignored. + * If it is an empty string - "" - an attribute without namespace will be searched. */ + + // Get the type of datatype : + typeOfType = reader.getAttributeValue(XSI_NAMESPACE, "type"); + + // Resolve the datatype: + if (typeOfType == null || typeOfType.trim().length() == 0) + throw new TAPException(getPosition(reader) + " Missing attribute \"xsi:type\" (where xsi = \"" + XSI_NAMESPACE + "\")! Expected attribute value: vs:VOTableType or vs:TAPType, where vs = " + VODATASERVICE_NAMESPACE + "."); + + // Separate the namespace and type parts: + String[] split = typeOfType.split(":"); + + // Ensure the number of parts is 2: + if (split.length != 2) + throw new TAPException(getPosition(reader) + " Unresolved type: \"" + typeOfType + "\"! Missing namespace prefix."); + // ...and ensure the namespace is the expected value: + else{ + String datatypeNamespace = reader.getNamespaceURI(split[0]); + if (datatypeNamespace == null) + throw new TAPException(getPosition(reader) + " Unresolved type: \"" + typeOfType + "\"! Unknown namespace."); + else if (!datatypeNamespace.startsWith(VODATASERVICE_NAMESPACE)) + throw new TAPException(getPosition(reader) + " Unsupported type: \"" + typeOfType + "\"! Expected: vs:VOTableType or vs:TAPType, where vs = " + VODATASERVICE_NAMESPACE + "."); + } + + // Get the other attributes: + size = reader.getAttributeValue("", "size"); + xtype = reader.getAttributeValue("", "xtype"); + arraysize = reader.getAttributeValue("", "arraysize"); + + // Get the datatype: + datatype = getText(reader); + if (datatype == null || datatype.trim().length() == 0) + throw new TAPException(getPosition(reader) + " Missing column datatype!"); + datatype = datatype.trim(); + + // Resolve the datatype in function of the value of xsi:type: + // CASE: VOTable + if (split[1].equalsIgnoreCase("VOTableType")) + return VOTableIterator.resolveVotType(datatype, arraysize, xtype).toTAPType(); + + // CASE: TAP type + else if (split[1].equalsIgnoreCase("TAPType")){ + // normalize the size attribute: + int colSize = -1; + if (size != null && size.trim().length() > 0){ + try{ + colSize = Integer.parseInt(size); + }catch(NumberFormatException nfe){} + } + // build and return the corresponding type: + try{ + return new DBType(DBDatatype.valueOf(datatype.toUpperCase()), colSize); + }catch(IllegalArgumentException iae){ + throw new TAPException(getPosition(reader) + " Unknown TAPType: \"" + datatype + "\"!"); + } + } + // DEFAULT => Throw an exception! + else + throw new TAPException(getPosition(reader) + " Unsupported type: \"" + typeOfType + "\"! Expected: vs:VOTableType or vs:TAPType, where vs = " + VODATASERVICE_NAMESPACE + "."); + } + + /** + *

    Parse the XML representation of a TAP foreign key.

    + * + *

    Important: This function MUST be called just after the start element "foreignKey" has been read!

    + * + *

    Attributes

    + * + *

    No attribute is expected in the start element "foreignKey".

    + * + *

    Children

    + * + * Only the following nodes are taken into account ; the others are ignored: + *
      + *
    • targetTable REQUIRED
    • + *
    • description {0..1}
    • + *
    • utype {0..1}
    • + *
    • fkColumn {1..*} + *
        + *
      • fromColumn REQUIRED
      • + *
      • targetColumn REQUIRED
      • + *
    • + *
    + * + * @param reader XML reader. + * + * @return An object containing all information found in the XML node about the foreign key. + * + * @throws XMLStreamException If there is an error processing the underlying XML source. + * @throws TAPException If "targetTable" node is missing, + * or if no "fkColumn" is provided. + * @throws IllegalStateException If this function is called while the reader has not just read the START ELEMENT tag of "foreignKey". + * + * @see #parseDataType(XMLStreamReader) + */ + protected ForeignKey parseFKey(final XMLStreamReader reader) throws XMLStreamException, TAPException{ + // Ensure the reader has just read the START ELEMENT of foreignKey: + if (reader.getEventType() != XMLStreamConstants.START_ELEMENT || reader.getLocalName() == null || !reader.getLocalName().equalsIgnoreCase("foreignKey")) + throw new IllegalStateException(getPosition(reader) + " Illegal usage of TableSetParser.parseFKey(XMLStreamParser)! This function can be called only when the reader has just read the START ELEMENT tag \"foreignKey\"."); + + String tag; + ForeignKey fk = new ForeignKey(); + + // Fetch the other information (tags): + while(nextTag(reader) == XMLStreamConstants.START_ELEMENT){ + // Get the tag name: + tag = reader.getLocalName(); + + // Identify the current tag: + if (tag.equalsIgnoreCase("targetTable")){ + if (fk.targetTable != null) + throw new TAPException(getPosition(reader) + " Only one \"targetTable\" element can exist in a /tableset/schema/table/foreignKey!"); + fk.targetTable = getText(reader); + fk.targetTablePosition = getPosition(reader); + }else if (tag.equalsIgnoreCase("description")) + fk.description = getText(reader); + else if (tag.equalsIgnoreCase("utype")) + fk.utype = getText(reader); + else if (tag.equalsIgnoreCase("fkColumn")){ + String innerTag, fromCol = null, targetCol = null; + while(nextTag(reader) == XMLStreamConstants.START_ELEMENT){ + innerTag = reader.getLocalName(); + if (innerTag.equalsIgnoreCase("fromColumn")){ + if (fromCol != null) + throw new TAPException(getPosition(reader) + " Only one \"fromColumn\" element can exist in a /tableset/schema/table/foreignKey/fkColumn !"); + fromCol = getText(reader); + }else if (innerTag.equalsIgnoreCase("targetColumn")){ + if (targetCol != null) + throw new TAPException(getPosition(reader) + " Only one \"targetColumn\" element can exist in a /tableset/schema/table/foreignKey/fkColumn !"); + targetCol = getText(reader); + }else + goToEndTag(reader, reader.getLocalName()); + } + // Only two info are required: the source and the target columns! + if (fromCol == null) + throw new TAPException(getPosition(reader) + " Missing \"fromColumn\"!"); + else if (targetCol == null) + throw new TAPException(getPosition(reader) + " Missing \"targetColumn\"!"); + else + fk.keyColumns.put(fromCol, targetCol); + }else + goToEndTag(reader, tag); + } + + // Check the last read tag is the END ELEMENT of a foreignKey node: + if (reader.getEventType() != XMLStreamConstants.END_ELEMENT) + throw new TAPException(getPosition(reader) + " Unexpected tag! An END ELEMENT tag for foreignKey was expected."); + else if (!reader.getLocalName().equalsIgnoreCase("foreignKey")) + throw new TAPException(getPosition(reader) + " Unexpected node end tag: ! An END ELEMENT tag for foreignKey was expected."); + + // The target table name is required! + if (fk.targetTable == null) + throw new TAPException(getPosition(reader) + " Missing \"targetTable\"!"); + // At least one columns association is expected! + else if (fk.keyColumns.size() == 0) + throw new TAPException(getPosition(reader) + " Missing at least one \"fkColumn\"!"); + + return fk; + } + + /* ***************** */ + /* UTILITY FUNCTIONS */ + /* ***************** */ + + /** + *

    Get the current position of the given reader.

    + * + *

    + * This position is returned as a string having the following syntax: "[l.x,c.y]" + * (where x is the line number and y the column number ; x and y start at 1 ; x and y + * are both -1 if the end of the XML document has been reached). + *

    + * + *

    Note: + * The column position is generally just after the read element (node start/end tag, characters). + * However, with CHARACTERS items, this column position may be 2 characters after the real end. + *

    + * + * @param reader XML reader whose the current position must be returned. + * + * @return A string representing the current reader position. + */ + protected final String getPosition(final XMLStreamReader reader){ + return "[l." + reader.getLocation().getLineNumber() + ",c." + reader.getLocation().getColumnNumber() + "]"; + } + + /** + * Skip every elements until a START ELEMENT or an END ELEMENT is reached. + * + * @param reader XML reader. + * + * @return The event of the last read tag. Here, either {@link XMLStreamConstants#START_ELEMENT} or {@link XMLStreamConstants#END_ELEMENT}. + * + * @throws XMLStreamException If there is an error processing the underlying XML source. + */ + protected final int nextTag(final XMLStreamReader reader) throws XMLStreamException{ + int event = -1; + do{ + event = reader.next(); + }while(event != XMLStreamConstants.START_ELEMENT && event != XMLStreamConstants.END_ELEMENT); + return event; + } + + /** + *

    Skip all tags from the current position to the end of the specified node.

    + * + *

    IMPORTANT: + * This function MUST be called ONLY IF the reader is inside the node whose the end tag is searched. + * It may be in a child of this node or not, but the most important is to be inside it. + *

    + * + *

    Note: + * No tag will be read if the given startNode is NULL or an empty string. + *

    + * + * @param reader XML reader. + * @param startNode Name of the node whose the end must be reached. + * + * @throws XMLStreamException If there is an error processing the underlying XML source. + * @throws TAPException If the name of the only corresponding end element does not match the given one, + * or if the END ELEMENT can not be found (2 possible reasons for that: + * 1/ malformed XML document, 2/ this function has been called before the START ELEMENT has been read). + */ + protected final void goToEndTag(final XMLStreamReader reader, final String startNode) throws XMLStreamException, TAPException{ + if (startNode == null || startNode.trim().length() <= 0) + return; + else if (reader.getEventType() == XMLStreamConstants.END_ELEMENT && reader.getLocalName().equalsIgnoreCase(startNode)) + return; + + int level = 0, event; + while(reader.hasNext()){ + event = reader.next(); + switch(event){ + case XMLStreamConstants.START_ELEMENT: + level++; + break; + case XMLStreamConstants.END_ELEMENT: + if (level <= 0 && reader.getLocalName().equalsIgnoreCase(startNode)) // "level <= 0" because the reader may be inside a child of the node whose the end is searched. + return; + else + level--; + } + } + + /* If no matching END ELEMENT, then either the XML document is malformed + * or #goToEndTag(...) has been called before the corresponding START ELEMENT has been read: */ + throw new TAPException(getPosition(reader) + " Malformed XML document: missing an END TAG !"); + } + + /** + *

    Get the text of the current node.

    + * + *

    + * This function iterates while the next tags are of type CHARACTERS. + * Consequently, the next tag (start or end element) is already read when returning this function. + *

    + * + *

    + * All CHARACTERS elements are concatenated. + * All leading and trailing space characters (\r \n \t and ' ') of every lines are deleted ; only the last or the first \n or \r are kept. + *

    + * + *

    Note: + * This function is also skipping all COMMENT elements. This is particularly useful if a COMMENT is splitting a node text content ; + * in such case, the comment is ignored and both divided text are concatenated. + *

    + * + * @param reader XML reader. + * + * @return The whole text content of the current node. + * + * @throws XMLStreamException If there is an error processing the underlying XML source. + */ + protected final String getText(final XMLStreamReader reader) throws XMLStreamException{ + StringBuffer txt = new StringBuffer(); + while(reader.next() == XMLStreamConstants.CHARACTERS || reader.getEventType() == XMLStreamConstants.COMMENT){ + if (reader.getEventType() == XMLStreamConstants.CHARACTERS){ + if (reader.getText() != null) + txt.append(reader.getText().replaceAll("[ \\t]+([\\n\\r]+)", "$1").replaceAll("([\\n\\r]+)[ \\t]+", "$1")); + } + }; + return txt.toString().trim(); + } + + /** + *

    Search for the specified table in the given TAP metadata.

    + * + *

    Note: This function is not case sensitive.

    + * + * @param tableName Name of the table to search. The table name MAY be prefixed by a schema name (e.g. "mySchema.myTable"). + * @param meta All fetched TAP metadata. + * @param position Position of the table name in the XML document. This parameter is ONLY used in case of error. + * + * @return The corresponding TAP table. + * + * @throws TAPException If the table name syntax ([schema.]table) is incorrect, + * or if several tables match to the specified table name (which is not prefixed by a schema name), + * or if no match can be found. + */ + protected final TAPTable searchTable(final String tableName, final TAPMetadata meta, final String position) throws TAPException{ + // Extract the schema name and normalize the table name: + String schema = null, table = tableName.trim(); + if (tableName.indexOf('.') >= 0){ + // get the schema name: + schema = tableName.substring(0, tableName.indexOf('.')).trim(); + // test that the schema name is not null: + if (schema.length() == 0) + throw new TAPException(position + " Incorrect full table name - \"" + tableName + "\": empty schema name!"); + // test that the remaining table name is not null: + else if (tableName.substring(schema.length() + 1).trim().length() == 0) + throw new TAPException(position + " Incorrect full table name - \"" + tableName + "\": empty table name!"); + // test there is no more '.' separator in the remaining table name: + else if (tableName.indexOf('.', schema.length() + 1) >= 0) + throw new TAPException(position + " Incorrect full table name - \"" + tableName + "\": only a schema and a table name can be specified (expected syntax: \"schema.table\")\"!"); + // get the table name: + table = tableName.substring(schema.length() + 1).trim(); + } + + // Find all matching tables: + ArrayList founds = new ArrayList(1); + StringBuffer foundsAsTxt = new StringBuffer(); + TAPTable t; + Iterator allTables = meta.getTables(); + while(allTables.hasNext()){ + // get the table to test: + t = allTables.next(); + if (t == null) + continue; + // store it if the schema and table names match: + if ((schema == null || t.getADQLSchemaName().equalsIgnoreCase(schema)) && t.getADQLName().equalsIgnoreCase(table)){ + // update the result array: + founds.add(t); + // update the text list: + if (foundsAsTxt.length() > 0) + foundsAsTxt.append(", "); + foundsAsTxt.append(t.getADQLSchemaName()).append('.').append(t.getADQLName()); + } + } + + if (founds.size() == 0) + throw new TAPException(position + " Unknown table: \"" + tableName + "\"!"); + else if (founds.size() > 1) + throw new TAPException(position + " Unresolved table: \"" + tableName + "\"! Several tables have the same name but in different schemas (here: " + foundsAsTxt.toString() + "). You must prefix this table name by a schema name (expected syntax: \"schema.table\")."); + else + return founds.get(0); + } + +} diff --git a/src/tap/metadata/VotType.java b/src/tap/metadata/VotType.java index 27daede..3f27046 100644 --- a/src/tap/metadata/VotType.java +++ b/src/tap/metadata/VotType.java @@ -16,10 +16,14 @@ package tap.metadata; * You should have received a copy of the GNU Lesser General Public License * along with TAPLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ -import cds.savot.writer.SavotWriter; +import tap.TAPException; +import uk.ac.starlink.votable.VOSerializer; +import adql.db.DBType; +import adql.db.DBType.DBDatatype; /** *

    Describes a full VOTable type. Thus it includes the following field attributes:

    @@ -29,26 +33,188 @@ import cds.savot.writer.SavotWriter; *
  • xtype.
  • *
* - * @author Grégory Mantelet (CDS) - * @version 11/2011 + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (02/2015) */ public final class VotType { - public final String datatype; - /** A negative or null value means "*" (that's to say: an undetermined arraysize). */ - public int arraysize; + /** + * All possible values for a VOTable datatype (i.e. boolean, short, char, ...). + * + * @author Grégory Mantelet (ARI) - gmantele@ari.uni-heidelberg.de + * @version 2.0 (01/2015) + * @since 2.0 + */ + public static enum VotDatatype{ + BOOLEAN("boolean"), BIT("bit"), UNSIGNEDBYTE("unsignedByte"), SHORT("short"), INT("int"), LONG("long"), CHAR("char"), UNICODECHAR("unicodeChar"), FLOAT("float"), DOUBLE("double"), FLOATCOMPLEX("floatComplex"), DOUBLECOMPLEX("doubleComplex"); + + private final String strExpr; + + private VotDatatype(final String str){ + strExpr = (str == null || str.trim().length() == 0) ? name() : str; + } + + @Override + public String toString(){ + return strExpr; + } + } + + /** Special VOTable type (XType) for TAP/DB type BLOB. + * @since 2.0*/ + public final static String XTYPE_BLOB = "adql:BLOB"; + /** Special VOTable type (XType) for TAP/DB type CLOB. + * @since 2.0 */ + public final static String XTYPE_CLOB = "adql:CLOB"; + /** Special VOTable type (XType) for TAP/DB type TIMESTAMP. + * @since 2.0 */ + public final static String XTYPE_TIMESTAMP = "adql:TIMESTAMP"; + /** Special VOTable type (XType) for TAP/DB type POINT. + * @since 2.0 */ + public final static String XTYPE_POINT = "adql:POINT"; + /** Special VOTable type (XType) for TAP/DB type REGION. + * @since 2.0 */ + public final static String XTYPE_REGION = "adql:REGION"; + + /** VOTable datatype + * @since 2.0 */ + public final VotDatatype datatype; + /** Arraysize string of a VOTable field element. */ + public final String arraysize; + /** Special type specification (i.e. POINT, TIMESTAMP, ...). */ public final String xtype; /** - * @param datatype A datatype (ex: char, int, long, ...). Null value forbidden - * @param arraysize A non-null positive integer. (any value ≤ 0 will be considered as an undetermined arraysize). - * @param xtype A special type (ex: adql:POINT, adql:TIMESTAMP, ...). Null value allowed. + * Build a VOTable field type. + * + * @param datatype A datatype. Null value forbidden + * @param arraysize VOTable arraysize string (may be NULL). + */ + public VotType(final VotDatatype datatype, final String arraysize){ + this(datatype, arraysize, null); + } + + /** + * Build a VOTable field type. + * + * @param datatype A datatype. Null value forbidden + * @param arraysize VOTable arraysize string (may be NULL). + * @param xtype A special type (ex: adql:POINT, adql:TIMESTAMP, ...). (may be NULL). */ - public VotType(final String datatype, final int arraysize, final String xtype){ + public VotType(final VotDatatype datatype, final String arraysize, final String xtype){ + // set the datatype: if (datatype == null) - throw new NullPointerException("Null VOTable datatype !"); - this.datatype = datatype; - this.arraysize = arraysize; - this.xtype = xtype; + throw new NullPointerException("missing VOTable datatype !"); + else + this.datatype = datatype; + + // set the array-size: + if (arraysize != null && arraysize.trim().length() > 0) + this.arraysize = arraysize.trim(); + else + this.arraysize = null; + + // set the xtype: + if (xtype != null && xtype.trim().length() > 0) + this.xtype = xtype.trim(); + else + this.xtype = null; + } + + /** + * Build a {@link VotType} object by converting the given {@link DBType}. + * + * @param tapType {@link DBType} to convert. + */ + public VotType(final DBType tapType){ + switch(tapType.type){ + case SMALLINT: + this.datatype = VotDatatype.SHORT; + this.arraysize = "1"; + this.xtype = null; + break; + + case INTEGER: + this.datatype = VotDatatype.INT; + this.arraysize = "1"; + this.xtype = null; + break; + + case BIGINT: + this.datatype = VotDatatype.LONG; + this.arraysize = "1"; + this.xtype = null; + break; + + case REAL: + this.datatype = VotDatatype.FLOAT; + this.arraysize = "1"; + this.xtype = null; + break; + + case DOUBLE: + this.datatype = VotDatatype.DOUBLE; + this.arraysize = "1"; + this.xtype = null; + break; + + case CHAR: + this.datatype = VotDatatype.CHAR; + this.arraysize = Integer.toString(tapType.length > 0 ? tapType.length : 1); + this.xtype = null; + break; + + case BINARY: + this.datatype = VotDatatype.UNSIGNEDBYTE; + this.arraysize = Integer.toString(tapType.length > 0 ? tapType.length : 1); + this.xtype = null; + break; + + case VARBINARY: + /* TODO HOW TO MANAGE VALUES WHICH WHERE ORIGINALLY NUMERIC ARRAYS ? + * (cf the IVOA document TAP#Upload: votable numeric arrays should be converted into VARBINARY...no more array information and particularly the datatype) + */ + this.datatype = VotDatatype.UNSIGNEDBYTE; + this.arraysize = (tapType.length > 0 ? tapType.length + "*" : "*"); + this.xtype = null; + break; + + case BLOB: + this.datatype = VotDatatype.UNSIGNEDBYTE; + this.arraysize = "*"; + this.xtype = VotType.XTYPE_BLOB; + break; + + case CLOB: + this.datatype = VotDatatype.CHAR; + this.arraysize = "*"; + this.xtype = VotType.XTYPE_CLOB; + break; + + case TIMESTAMP: + this.datatype = VotDatatype.CHAR; + this.arraysize = "*"; + this.xtype = VotType.XTYPE_TIMESTAMP; + break; + + case POINT: + this.datatype = VotDatatype.CHAR; + this.arraysize = "*"; + this.xtype = VotType.XTYPE_POINT; + break; + + case REGION: + this.datatype = VotDatatype.CHAR; + this.arraysize = "*"; + this.xtype = VotType.XTYPE_REGION; + break; + + case VARCHAR: + default: + this.datatype = VotDatatype.CHAR; + this.arraysize = (tapType.length > 0 ? tapType.length + "*" : "*"); + this.xtype = null; + break; + } } @Override @@ -56,13 +222,7 @@ public final class VotType { if (obj == null) return false; try{ - VotType vot = (VotType)obj; - if (datatype.equalsIgnoreCase(vot.datatype)){ - if (xtype == null) - return (vot.xtype == null); - else - return xtype.equalsIgnoreCase(vot.xtype); - } + return toString().equals(obj); }catch(ClassCastException cce){ ; } @@ -71,23 +231,147 @@ public final class VotType { @Override public int hashCode(){ - return datatype.toLowerCase().hashCode(); + return datatype.toString().hashCode(); } @Override public String toString(){ - StringBuffer str = new StringBuffer("datatype=\""); - str.append(datatype).append('"'); + StringBuffer str = new StringBuffer(VOSerializer.formatAttribute("datatype", datatype.toString())); + str.deleteCharAt(0); - if (arraysize == TAPTypes.STAR_SIZE) - str.append(" arraysize=\"*\""); - else if (arraysize != TAPTypes.NO_SIZE && arraysize > 0) - str.append(" arraysize=\"").append(SavotWriter.encodeAttribute("" + arraysize)).append('"'); + if (arraysize != null) + str.append(VOSerializer.formatAttribute("arraysize", arraysize)); if (xtype != null) - str.append(" xtype=\"").append(SavotWriter.encodeAttribute(xtype)).append('"'); + str.append(VOSerializer.formatAttribute("xtype", xtype)); return str.toString(); } + /** + * Convert this VOTable type definition into a TAPColumn type. + * + * @return The corresponding {@link DBType}. + * + * @throws TAPException If the conversion is impossible (particularly if the array-size refers to a multi-dimensional array ; only 1D arrays are allowed). + */ + public DBType toTAPType() throws TAPException{ + + /* Stop immediately if the arraysize refers to a multi-dimensional array: + * (Note: 'x' is the dimension separator of the VOTable attribute 'arraysize') */ + if (arraysize != null && arraysize.indexOf('x') >= 0) + throw new TAPException("failed conversion of a VOTable datatype: multi-dimensional arrays (" + datatype + "[" + arraysize + "]) are not allowed!"); + + // Convert the VOTable datatype into TAP datatype: + switch(datatype){ + /* NUMERIC TYPES */ + case SHORT: + case BOOLEAN: + return convertNumericType(DBDatatype.SMALLINT); + + case INT: + return convertNumericType(DBDatatype.INTEGER); + + case LONG: + return convertNumericType(DBDatatype.BIGINT); + + case FLOAT: + return convertNumericType(DBDatatype.REAL); + + case DOUBLE: + return convertNumericType(DBDatatype.DOUBLE); + + /* BINARY TYPES */ + case UNSIGNEDBYTE: + // BLOB exception: + if (xtype != null && xtype.equalsIgnoreCase(XTYPE_BLOB)) + return new DBType(DBDatatype.BLOB); + + // Or else, just (var)binary: + else + return convertVariableLengthType(DBDatatype.VARBINARY, DBDatatype.BINARY); + + /* CHARACTER TYPES */ + case CHAR: + default: + /* Special type cases: */ + if (xtype != null){ + if (xtype.equalsIgnoreCase(VotType.XTYPE_CLOB)) + return new DBType(DBDatatype.CLOB); + else if (xtype.equalsIgnoreCase(VotType.XTYPE_TIMESTAMP)) + return new DBType(DBDatatype.TIMESTAMP); + else if (xtype.equalsIgnoreCase(VotType.XTYPE_POINT)) + return new DBType(DBDatatype.POINT); + else if (xtype.equalsIgnoreCase(VotType.XTYPE_REGION)) + return new DBType(DBDatatype.REGION); + } + + // Or if not known or missing, just a (var)char: + return convertVariableLengthType(DBDatatype.VARCHAR, DBDatatype.CHAR); + } + } + + /** + *

Convert this numeric {@link VotType} object into a corresponding {@link DBType} whose the datatype is provided in parameter.

+ * + *

+ * Thus, just the arraysize must be managed here. If there is no arraysize or if equals to '1', the given datatype will be used. + * Otherwise, it is ignored and a {@link DBType} with VARBINARY is returned. + *

+ * + * @param tapDatatype TAP datatype corresponding to this {@link VotType} (only when arraysize != '*' and 'n'). + * + * @return The corresponding {@link DBType}. + */ + protected DBType convertNumericType(final DBDatatype tapDatatype){ + // If no arraysize: + if (arraysize == null || arraysize.equals("1")) + return new DBType(tapDatatype); + + // If only one dimension: + else + return new DBType(DBDatatype.VARBINARY); + + /* Note: The test of multi-dimensional array should have been already done at the beginning of #toTAPType(). */ + } + + /** + *

+ * Convert this variable length {@link VotType} (unsignedByte and char) object into a corresponding {@link DBType} + * whose the variable length and fixed length versions are given in parameters. + *

+ * + *

Thus, just the arraysize must be managed here. The following cases are taken into account:

+ *
    + *
  • No arraysize or '*': variable length type (i.e. VARCHAR, VARBINARY),
  • + *
  • 'n*': variable length type with the maximal length (i.e. VARCHAR(n), VARBINARY(n)),
  • + *
  • 'n': fixed length type with the exact length (i.e. CHAR(n), BINARY(n)).
  • + *
+ * + * @param varType Variable length type (i.e. VARCHAR, VARBINARY). + * @param fixedType Fixed length type (i.e. CHAR, BINARY). + * + * @return The corresponding {@link DBType}. + * + * @throws TAPException If the arraysize is not valid (that's to say, different from the following syntaxes: NULL, '*', 'n' or 'n*' (where n is a positive and not-null integer)). + */ + protected DBType convertVariableLengthType(final DBDatatype varType, final DBDatatype fixedType) throws TAPException{ + try{ + // no arraysize or '*' => VARCHAR or VARBINARY + if (arraysize == null || arraysize.equals("*")) + return new DBType(varType); + + // 'n*' => VARCHAR(n) or VARBINARY(n) + else if (arraysize.charAt(arraysize.length() - 1) == '*') + return new DBType(varType, Integer.parseInt(arraysize.substring(0, arraysize.length() - 1))); + + // 'n' => CHAR(n) or BINARY(n) + else + return new DBType(fixedType, Integer.parseInt(arraysize)); + + }catch(NumberFormatException nfe){ + throw new TAPException("failed conversion of a VOTable datatype: non-numeric arraysize (" + arraysize + ")!"); + } + } + } diff --git a/src/tap/parameters/DALIUpload.java b/src/tap/parameters/DALIUpload.java new file mode 100644 index 0000000..fdf001d --- /dev/null +++ b/src/tap/parameters/DALIUpload.java @@ -0,0 +1,601 @@ +package tap.parameters; + +/* + * This file is part of TAPLibrary. + * + * TAPLibrary is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * TAPLibrary is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with TAPLibrary. If not, see . + * + * Copyright 2014 - Astronomisches Rechen Institut (ARI) + */ + +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import tap.TAPException; +import tap.TAPJob; +import uws.UWSException; +import uws.service.file.UWSFileManager; +import uws.service.file.UnsupportedURIProtocolException; +import uws.service.request.RequestParser; +import uws.service.request.UploadFile; + +/** + *

Description of an uploaded content specified using the DALI/TAP syntax.

+ * + *

How to access the upload content?

+ * + *

+ * This parameter is either a reference to a distant content and is then specified by a URI, + * or a pointer to the stored version of a file submitted inline in a HTTP request. In both cases, + * this class lets access the upload content with the function {@link #open()}. + *

+ * + *

How to get {@link DALIUpload} objects from HTTP request parameters?

+ * + *

+ * The static function {@link #getDALIUploads(Map, boolean, UWSFileManager)} should be used in order to + * extract the {@link DALIUpload} items specified in a list of request parameters. + *

+ *

Note: + * It is recommended to provide these parameters as a map generated by a {@link RequestParser}. + * If not, you should ensure that values of the map associated to the "UPLOAD" parameter(s) are {@link String}s, {@link String}[]s, + * {@link DALIUpload}s, {@link DALIUpload}[]s or {@link Object}[] containing {@link String}s and/or {@link DALIUpload}s. + * Besides, the request parameters referenced using the syntax "param:{param-name}" must be instances of only {@link UploadFile} + * or an array of {@link Object}s containing at least one {@link UploadFile} instance (if several are found, just the last one will be used). + *

+ *

+ * Calling this function will also modify a little the given list of parameters by rewriting the "UPLOAD" parameter and + * removing unreferenced uploaded files (from the list and from the file-system). + *

+ * + *

Reminder about the "UPLOAD" parameter

+ * + *

+ * The IVOA standards DAL and TAP define both the same special parameter: "UPLOAD" (not case-sensitive). + *

+ * + *

+ * This parameter lists all upload items. A such item can be either an inline file or a reference to a distant file. + * In both cases, it is specified as a URI. The parameter "UPLOAD" sets also a label/name to this item. + * The syntax to use for a single item is the following: "{label},{URI}". Several items can be provided, but there is + * a slight difference between DALI and TAP in the way to do it. DALI says that multiple uploads MUST be done + * by several submit of a single "UPLOAD" parameter with the syntax described above. TAP says that multiple uploads CAN + * be done in one "UPLOAD" parameter by separating each item with a semicolon (;). For instance: + *

+ *
    + *
  • In TAP: "UPLOAD=tableA,param:foo;tableB,http://..." => only 1 parameter for 2 uploads
  • + *
  • In DALI: "UPLOAD=tableA,param:foo" and "UPLOAD=tableB,http://..." => 2 parameters, one for each upload
  • + *
+ * + *

Note: + * The drawback of the TAP method is: what happens when a URI contains a semicolon? URI can indeed contain a such character + * and in this case the parsing becomes more tricky, or even impossible in some cases. In such cases, it is strongly + * recommended to either encode the URI (so the ";" becomes "%3B") or to forbid the TAP syntax. This latter can be + * done by setting the second parameter of {@link #getDALIUploads(Map, boolean, UWSFileManager)} to false. + *

+ * + * @author Grégory Mantelet (ARI) + * @version 2.0 (12/2014) + * @since 2.0 + * + * @see RequestParser + */ +public class DALIUpload { + + /**

Pointer to the stored version of the file submitted inline in a HTTP request.

+ *

Note: + * If NULL, this {@link DALIUpload} is then a "byReference" upload, meaning that its content is distant + * and can be accessed only with the URI {@link #uri}. + *

*/ + public final UploadFile file; + + /**

URI toward a distant resource.

+ *

Note: + * If NULL, this {@link DALIUpload} corresponds to a file submitted inline in a HTTP request. + * Its content has then been stored by this service and can be accessed using the pointer {@link #file}. + *

*/ + public final URI uri; + + /**

Name to use in the service to label this upload.

+ *

Note: + * In a TAP service, this label is the name of the table to create in the database + * when creating the corresponding table inside it. + *

*/ + public final String label; + + /** The file manager to use when a stream will be opened toward the given URI. + * It should know how to access it, because the URI can use a URL scheme (http, https, ftp) but also another scheme + * unknown by the library (e.g. ivo, vos). */ + protected final UWSFileManager fileManager; + + /** + *

Build a {@link DALIUpload} whose the content has been submitted inline in an HTTP request.

+ * + *

+ * A such upload has been specified by referencing another HTTP request parameter containing an inline file. + * The used syntax was then: "{label},param:{param-name}". + *

+ * + * @param label Label of the DALIUpload (i.e. {label} inside an "UPLOAD" parameter value "{label},{URI}"). + * Note: If NULL, the file name will be used as label. + * @param file Pointer to the uploaded file. + */ + public DALIUpload(final String label, final UploadFile file){ + if (file == null) + throw new NullPointerException("Missing UploadFile! => Can not build a DaliUpload instance."); + + this.label = (label == null) ? file.paramName : label; + this.file = file; + this.uri = null; + this.fileManager = null; + } + + /** + *

Build a {@link DALIUpload} whose the content is distant and specified by a URI.

+ * + *

+ * A such upload has been specified by referencing a URI (whose the scheme is different from "param"). + * The used syntax was then: "{label},{URI}". + *

+ * + * @param label Label of the DALIUpload (i.e. {label} inside an "UPLOAD" parameter value "{label},{URI}"). Note: If NULL, the URI will be used as label. + * @param uri URI toward a distant file. The scheme of this URI must be different from "param". This scheme is indeed reserved by the DALI syntax to reference a HTTP request parameter containing an inline file. + * @param fileManager The file manager to use when a stream will be opened toward the given URI. This file manager should know how to access it, + * because the URI can use a URL scheme (http, https, ftp) but also another scheme unknown by the library (e.g. ivo, vos). + */ + public DALIUpload(final String label, final URI uri, final UWSFileManager fileManager){ + if (uri == null) + throw new NullPointerException("Missing URI! => Can not build a DaliUpload instance."); + else if (uri.getScheme() != null && uri.getScheme().equalsIgnoreCase("param")) + throw new IllegalArgumentException("Wrong URI scheme: \"param\" is reserved to reference a HTTP request parameter! If used, the content of this parameter must be stored in a file, then the parameter must be represented by an UploadFile and integrated into a DALIUpload with the other constructor."); + else if (uri.getScheme() != null && uri.getScheme().equalsIgnoreCase("file")) + throw new IllegalArgumentException("Wrong URI scheme: \"file\" is forbidden!"); + else if (fileManager == null) + throw new NullPointerException("Missing File Manager! => Can not build a DaliUpload instance."); + + this.label = (label == null) ? uri.toString() : label; + this.uri = uri; + this.file = null; + this.fileManager = fileManager; + } + + /** + * Tell whether this upload is actually a reference toward a distant resource. + * + * @return true if this upload is referenced by a URI, + * false if the upload has been submitted inline in the HTTP request. + */ + public boolean isByReference(){ + return (file == null); + } + + /** + * Open a stream to the content of this upload. + * + * @return An InputStream. + * + * @throws UnsupportedURIProtocolException If the URI of this upload item is using a protocol not supported by this service implementation. + * @throws IOException If the stream can not be opened. + */ + public InputStream open() throws UnsupportedURIProtocolException, IOException{ + if (file == null) + return fileManager.openURI(uri); + else + return file.open(); + } + + @Override + public String toString(){ + return label + "," + (file != null ? "param:" + file.paramName : uri.toString()); + } + + /* ****************************** */ + /* EXTRACTION OF DALI/TAP UPLOADS */ + /* ****************************** */ + + /**

Regular expression of an UPLOAD parameter as defined by DALI (REC-DALI-1.0-20131129).

+ *

Note: + * In DALI, multiple uploads must be done by posting several UPLOAD parameters. + * It is not possible to provide directly a list of parameters as in TAP. + * However, the advantage of the DALI method is to allow ; in URI (while ; is the + * parameter separator in TAP). + *

*/ + protected static final String DALI_UPLOAD_REGEXP = "[^,]+,\\s*(param:.+|.+)"; + + /**

Regular expression of an UPLOAD parameter as defined by TAP (REC-TAP-1.0).

+ *

Note: + * In TAP, multiple uploads may be done by POSTing only one UPLOAD parameter + * whose the value is a list of DALI UPLOAD parameters, separated by a ; + *

*/ + protected static final String TAP_UPLOAD_REGEXP = DALI_UPLOAD_REGEXP + "(\\s*;\\s*" + DALI_UPLOAD_REGEXP + ")*"; + + /** + *

Get all uploads specified in the DALI parameter "UPLOAD" from the given request parameters.

+ * + *

Note: + * This function is case INsensitive for the "UPLOAD" parameter. + *

+ *

WARNING: + * Calling this function modifies the given map ONLY IF the "UPLOAD" parameter (whatever is its case) is found. + * In such case, the following modifications are applied: + *

+ *
    + *
  • + * All "UPLOAD" parameters will be removed and then added again in the map with their corresponding {@link DALIUpload} item (not any more a String). + *
  • + *
  • + * If allowTAPSyntax is true, several uploads may be specified in the same "UPLOAD" parameter value. + * For more clarity for the user (once the parameters listed), this list of uploads will be split in the same number of "UPLOAD" parameters. + * That's to say, there will be only one "UPLOAD" item in the Map, but its value will be an array containing every specified uploads: + * an array of {@link DALIUpload} objects. + *
  • + *
  • + * If there is at least one "UPLOAD" parameter, all uploaded files (parameters associated with instances of {@link UploadFile}) will be removed + * from the map (and also from the file system). They are indeed not useful for a DALI service since all interesting uploads have already been + * listed. + *
  • + *
+ * + *

Note: + * This function can be called several times on the same map. After a first call, this function will just gathers into a List + * all found {@link DALIUpload} objects. Of course, only uploads specified in the "UPLOAD" parameter(s) will be returned and others will be removed + * as explained above. + *

+ * + *

DALI and TAP syntax

+ *

+ * The "UPLOAD" parameter lists all files to consider as uploaded. + * The syntax for one item is the following: "{name},{uri}", where {uri} is "param:{param-ref}" when the file is provided + * inline in the parameter named {param-ref}, otherwise, it can be any valid URI (http:..., ftp:..., vos:..., ivo:..., etc...). + *

+ * + *

+ * The parameter allowTAPSyntax lets switch between the DALI and TAP syntax. + * The only difference between them, is in the way to list multiple uploads. In TAP, they can be given as a semicolon separated + * list in a single parameter, whereas in DALI, there must be submitted as several individual parameters. For instance: + *

+ *
    + *
  • In TAP: "UPLOAD=tableA,param:foo;tableB,http://..." => only 1 parameter
  • + *
  • In DALI: "UPLOAD=tableA,param:foo" and "UPLOAD=tableB,http://..." => 2 parameters
  • + *
+ * + *

Note: + * Because of the possible presence of a semicolon in a URI (which is also used as separator of uploads in the TAP syntax), + * there could be a problem while splitting the uploads specified in "UPLOAD". In that case, it is strongly recommended to + * either encode the URI (in UTF-8) (i.e. ";" becomes "%3B") or to merely restrict the syntax to the DALI one. In this last case, + * the parameter "allowTAPSyntax" should be set to false and then all parameters should be submitted individually. + *

+ * + * @param requestParams All parameters extracted from an HTTP request by a {@link RequestParser}. + * @param allowTAPSyntax true to allow a list of several upload items in one "UPLOAD" parameter value (each item separated by a semicolon), + * false to forbid it (and so, multiple upload items shall be submitted individually). + * @param fileManager The file manager to use in order to build a {@link DALIUpload} objects from a URI. + * (a link to the file manager will be set in the {@link DALIUpload} object in order to open it + * whenever it will asked after its creation) + * + * @return List of all uploads specified with the DALI or TAP syntax. + * + * @throws TAPException If the syntax of an "UPLOAD" parameter is wrong. + * + * @see RequestParser#parse(javax.servlet.http.HttpServletRequest) + */ + public final static List getDALIUploads(final Map requestParams, final boolean allowTAPSyntax, final UWSFileManager fileManager) throws TAPException{ + + // 1. Get all "UPLOAD" parameters and build/get their corresponding DALIUpload(s): + ArrayList uploads = new ArrayList(3); + ArrayList usedFiles = new ArrayList(3); + Iterator> it = requestParams.entrySet().iterator(); + Map.Entry entry; + Object value; + while(it.hasNext()){ + entry = it.next(); + + // If the parameter is an "UPLOAD" one: + if (entry.getKey() != null && entry.getKey().toLowerCase().equals(TAPJob.PARAM_UPLOAD)){ + // get its value: + value = entry.getValue(); + + if (value != null){ + // CASE DALIUpload: just add the upload item inside the list: + if (value instanceof DALIUpload){ + DALIUpload upl = (DALIUpload)value; + uploads.add(upl); + if (!upl.isByReference()) + usedFiles.add(upl.file.paramName); + } + // CASE String: it must be parsed and transformed into a DALIUpload item which will be then added inside the list: + else if (value instanceof String) + fetchDALIUploads(uploads, usedFiles, (String)value, requestParams, allowTAPSyntax, fileManager); + + // CASE Array: + else if (value.getClass().isArray()){ + Object[] objects = (Object[])value; + for(Object o : objects){ + if (o != null){ + if (o instanceof DALIUpload) + uploads.add((DALIUpload)o); + else if (o instanceof String) + fetchDALIUploads(uploads, usedFiles, (String)o, requestParams, allowTAPSyntax, fileManager); + } + } + } + } + + // remove this "UPLOAD" parameter ; if it was not NULL, it will be added again in the map but as DALIUpload item(s) after this loop: + it.remove(); + } + } + + // 2. Remove all other files of the request parameters ONLY IF there was a not-NULL "UPLOAD" parameter: + if (uploads.size() > 0){ + it = requestParams.entrySet().iterator(); + while(it.hasNext()){ + entry = it.next(); + value = entry.getValue(); + if (value == null) + it.remove(); + else if (value instanceof UploadFile && !usedFiles.contains(entry.getKey())){ + try{ + ((UploadFile)value).deleteFile(); + }catch(IOException ioe){} + it.remove(); + }else if (value.getClass().isArray()){ + Object[] objects = (Object[])value; + int cnt = objects.length; + for(int i = 0; i < objects.length; i++){ + if (objects[i] == null){ + objects[i] = null; + cnt--; + }else if (objects[i] instanceof UploadFile && !usedFiles.contains(entry.getKey())){ + try{ + ((UploadFile)objects[i]).deleteFile(); + }catch(IOException ioe){} + objects[i] = null; + cnt--; + } + } + if (cnt == 0) + it.remove(); + } + } + } + + // 3. Re-add a new "UPLOAD" parameter gathering all extracted DALI Uploads: + if (uploads.size() > 0) + requestParams.put("UPLOAD", uploads.toArray(new DALIUpload[uploads.size()])); + + return uploads; + } + + /** + *

Fetch all uploads specified in the DALI/TAP "UPLOAD" parameter. + * The fetched {@link DALIUpload}s are added in the given {@link ArrayList}.

+ * + *

Note: A DALI upload can be either a URI or an inline file (specified as "param:{param-ref}").

+ * + * @param uploads List of {@link DALIUpload}s. to update. + * @param usedFiles List of the the names of the referenced file parameters. to update. + * @param uploadParam Value of the "UPLOAD" parameter. + * @param parameters List of all extracted parameters (including {@link UploadFile}(s)). + * @param allowTAPSyntax true to allow a list of several upload items in one "UPLOAD" parameter value (each item separated by a semicolon), + * false to forbid it (and so, multiple upload items shall be submitted individually). + * @param fileManager The file manager to use in order to build a {@link DALIUpload} objects from a URI. + * (a link to the file manager will be set in the {@link DALIUpload} object in order to open it + * whenever it will asked after its creation) + * + * @throws TAPException If the syntax of the given "UPLOAD" parameter is incorrect. + */ + protected static void fetchDALIUploads(final ArrayList uploads, final ArrayList usedFiles, String uploadParam, final Map parameters, final boolean allowTAPSyntax, final UWSFileManager fileManager) throws TAPException{ + if (uploadParam == null || uploadParam.trim().length() <= 0) + return; + + // TAP SYNTAX (list of DALI UPLOAD items, separated by a semicolon): + if (allowTAPSyntax && uploadParam.matches("([^,]+,.+);([^,]+,.+)")){ + Pattern p = Pattern.compile("([^,]+,.+);([^,]+,.+)"); + Matcher m = p.matcher(uploadParam); + while(m != null && m.matches()){ + // Fetch the last UPLOAD item: + DALIUpload upl = fetchDALIUpload(m.group(2), parameters, fileManager); + uploads.add(upl); + if (!upl.isByReference()) + usedFiles.add(upl.file.paramName); + + // Prepare the fetching of the other DALI parameters: + if (m.group(1) != null) + m = p.matcher(uploadParam = m.group(1)); + } + } + + // DALI SYNTAX (only one UPLOAD item): + if (uploadParam.matches("[^,]+,.+")){ + // Fetch the single UPLOAD item: + DALIUpload upl = fetchDALIUpload(uploadParam, parameters, fileManager); + uploads.add(upl); + if (!upl.isByReference()) + usedFiles.add(upl.file.paramName); + } + + // /!\ INCORRECT SYNTAX /!\ + else + throw new TAPException("Wrong DALI syntax for the parameter UPLOAD \"" + uploadParam + "\"!", UWSException.BAD_REQUEST); + } + + /** + * Fetch the single upload item (a pair with the syntax: "{label},{URI}". + * + * @param uploadParam Value of the "UPLOAD" parameter. A single upload item is expected ; that's to say something like "{label},{URI}". + * @param parameters List of extracted parameters. The fetched LOB must be added as a new parameter in this map. MUST not be NULL + * @param fileManager The file manager to use in order to build a {@link DALIUpload} objects from a URI. + * (a link to the file manager will be set in the {@link DALIUpload} object in order to open it + * whenever it will asked after its creation) + * + * @return The corresponding {@link DALIUpload} object. + * + * @throws TAPException If the syntax of the given "UPLOAD" parameter is incorrect. + * + * @see #parseDALIParam(String) + * @see #buildDALIUpload(String, String, Map, UWSFileManager) + */ + protected static DALIUpload fetchDALIUpload(final String uploadParam, final Map parameters, final UWSFileManager fileManager) throws TAPException{ + if (uploadParam.matches("[^,]+,.+")){ + // Check and extract the pair parts ([0]=label, [1]=URI): + String[] parts = parseDALIParam(uploadParam); + + // Build the corresponding DALIUpload: + return buildDALIUpload(parts[0], parts[1], parameters, fileManager); + }else + throw new TAPException("Wrong DALI syntax for the parameter UPLOAD \"" + uploadParam + "\"!", UWSException.BAD_REQUEST); + } + + /** + *

Extract the two parts (label and URI) of the given DALI parameter, and then, check their syntax.

+ * + *

Important note: + * It MUST be ensured before calling this function that the given DALI parameter is not NULL + * and contains at least one comma (,). + *

+ * + *

+ * The first comma found in the given string will be the separator of the two parts + * of the given DALI parameter: {label},{URI} + *

+ * + *

+ * The label part - {label} - must start with one letter and may be followed by a letter, + * a digit or an underscore. The corresponding regular expression is: [a-zA-Z][a-zA-Z0-9_]* + *

+ * + *

+ * The URI part - {URI} - must start with a scheme, followed by a colon (:) and then by several characters + * (no restriction). A scheme must start with one letter and may be followed by a letter, + * a digit, a plus (+), a dot (.) or an hyphen/minus (-). The corresponding regular expression is: + * [a-zA-Z][a-zA-Z0-9\+\.-]* + *

+ * + * @param definition MUST BE A PAIR label,value + * + * @return An array of exactly 2 items: [0]=upload label/name, [1]=an URI. (note: the special DALI syntax "param:..." is also a valid URI) + * + * @throws TAPException If the given upload definition is not following the valid DALI syntax. + */ + protected static String[] parseDALIParam(final String definition) throws TAPException{ + // Locate the separator: + int sep = definition.indexOf(','); + if (sep <= 0) + throw new TAPException("A DALI parameter must be a pair whose the items are separated by a colon!", UWSException.INTERNAL_SERVER_ERROR); + + // Extract the two parts: {label},{uri} + String[] parts = new String[]{definition.substring(0, sep),definition.substring(sep + 1)}; + + // Check the label: + if (!parts[0].matches("[a-zA-Z][a-zA-Z0-9_]*")) + throw new TAPException("Wrong uploaded item name syntax: \"" + parts[0] + "\"! An uploaded item must have a label respecting the 'regular_identifier' production of ADQL 2.0 (regular expression: [a-zA-Z][a-zA-Z0-9_]*).", UWSException.BAD_REQUEST); + // Check the URI: + else if (!parts[1].matches("[a-zA-Z][a-zA-Z0-9\\+\\.\\-]*:.+")) + throw new TAPException("Bad URI syntax: \"" + parts[1] + "\"! A URI must start with: \":\", where =\"[a-zA-Z][a-zA-Z0-9+.-]*\".", UWSException.BAD_REQUEST); + + return parts; + } + + /** + *

Build a {@link DALIUpload} corresponding to the specified URI.

+ * + *

+ * If the URI starts, case-insensitively, with "param:", it is then a reference to another request parameter containing a file content. + * In this case, the file content has been already stored inside a local file and represented by an {@link UploadFile} instance in the map. + *

+ * + *

+ * If the URI does not start with "param:", the DALI upload is considered as a reference to a distant file which can be accessed using this URI. + * Any URI scheme is allowed here, but the given file manager should be able to interpret it and open a stream toward the referenced resource + * whenever it will be asked. + *

+ * + *

Note: + * If the URI is not a parameter reference (i.e. started by "param:"), it will be decoded using {@link URLDecoder#decode(String, String)} + * (character encoding: UTF-8). + *

+ * + * @param label Label of the {@link DALIUpload} to build. + * @param uri URI of the LOB. MUST be NOT-NULL + * @param parameters All parameters extracted from an HTTP request by a {@link RequestParser}. + * @param fileManager The file manager to use in order to build a {@link DALIUpload} objects from a URI. + * (a link to the file manager will be set in the {@link DALIUpload} object in order to open it + * whenever it will asked after its creation) + * + * @return The corresponding {@link DALIUpload} object. + * + * @throws TAPException If the parameter reference is broken or if the given URI has a wrong syntax. + */ + protected final static DALIUpload buildDALIUpload(final String label, String uri, final Map parameters, final UWSFileManager fileManager) throws TAPException{ + // FILE case: + if (uri.toLowerCase().startsWith("param:")){ + + // get the specified parameter name: + uri = uri.substring(6); + + // get the corresponding file: + Object obj = parameters.get(uri); + + /* a map value can be an array of objects in case several parameters have the same name ; + * in this case, we just keep the last instance of UploadFile: */ + if (obj != null && obj.getClass().isArray()){ + Object[] objects = (Object[])obj; + obj = null; + for(Object o : objects){ + if (o != null && o instanceof UploadFile) + obj = o; + } + } + + // ensure the type of the retrieved parameter is correct: + if (obj == null) + throw new TAPException("Missing file parameter to upload: \"" + uri + "\"!", UWSException.BAD_REQUEST); + else if (!(obj instanceof UploadFile)) + throw new TAPException("Incorrect parameter type \"" + uri + "\": a file was expected!", UWSException.BAD_REQUEST); + + // build the LOB: + return new DALIUpload(label, (UploadFile)obj); + } + + // URI case: + else{ + // extract the URI as it is given: + uri = uri.trim(); + if (uri.toLowerCase().startsWith("file:")) + throw new TAPException("Wrong URI scheme in the upload specification labeled \"" + label + "\": \"file\" is forbidden!", UWSException.BAD_REQUEST); + // decode it in case there is any illegal character: + try{ + uri = URLDecoder.decode(uri, "UTF-8"); + }catch(UnsupportedEncodingException uee){} + try{ + // build the LOB: + return new DALIUpload(label, new URI(uri), fileManager); + }catch(URISyntaxException e){ + throw new TAPException("Incorrect URI syntax: \"" + uri + "\"!", UWSException.BAD_REQUEST); + } + } + } + +} diff --git a/src/tap/parameters/FormatController.java b/src/tap/parameters/FormatController.java index 46f2d2f..0092251 100644 --- a/src/tap/parameters/FormatController.java +++ b/src/tap/parameters/FormatController.java @@ -16,7 +16,8 @@ package tap.parameters; * You should have received a copy of the GNU Lesser General Public License * along with TAPLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.util.Iterator; @@ -25,15 +26,33 @@ import tap.ServiceConnection; import tap.TAPJob; import tap.formatter.OutputFormat; import uws.UWSException; -import uws.UWSExceptionFactory; import uws.job.parameters.InputParamController; -public class FormatController< R > implements InputParamController { +/** + *

Let controlling the format of all job result in a TAP service. + * The default values are provided by the service connection.

+ * + *

Note: + * By default, the format can be modified by anyone without any limitation. + *

+ * + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (09/2014) + */ +public class FormatController implements InputParamController { + + /** Connection to the service which knows the maximum and default value of this parameter. */ + protected final ServiceConnection service; - protected final ServiceConnection service; + /** Indicates whether the output limit of jobs can be modified. */ protected boolean allowModification = true; - public FormatController(final ServiceConnection service){ + /** + * Build a controller for the Format parameter. + * + * @param service Connection to the TAP service. + */ + public FormatController(final ServiceConnection service){ this.service = service; } @@ -42,6 +61,11 @@ public class FormatController< R > implements InputParamController { return allowModification; } + /** + * Lets indicating whether the format parameter can be modified. + * + * @param allowModif true if the format can be modified, false otherwise. + */ public final void allowModification(final boolean allowModif){ this.allowModification = allowModif; } @@ -54,7 +78,7 @@ public class FormatController< R > implements InputParamController { @Override public Object check(Object format) throws UWSException{ if (format == null) - return null; + return getDefault(); if (format instanceof String){ String strFormat = ((String)format).trim(); @@ -62,18 +86,24 @@ public class FormatController< R > implements InputParamController { return getDefault(); if (service.getOutputFormat(strFormat) == null) - throw new UWSException(UWSException.BAD_REQUEST, "Unknown output format (=" + strFormat + ") ! This TAP service can format query results ONLY in the following formats:" + getAllowedFormats() + "."); + throw new UWSException(UWSException.BAD_REQUEST, "Unknown value for the parameter \"format\": \"" + strFormat + "\". It should be " + getAllowedFormats()); else return strFormat; }else - throw UWSExceptionFactory.badFormat(null, TAPJob.PARAM_FORMAT, format.toString(), format.getClass().getName(), "A String equals to one of the following values: " + getAllowedFormats() + "."); + throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, "Wrong type for the parameter \"format\": class \"" + format.getClass().getName() + "\"! It should be a String."); } - public final String getAllowedFormats(){ - Iterator> itFormats = service.getOutputFormats(); - StringBuffer allowedFormats = new StringBuffer(); + /** + * Get a list of all allowed output formats (for each, the main MIME type + * but also the short type representation are given). + * + * @return List of all output formats. + */ + protected final String getAllowedFormats(){ + Iterator itFormats = service.getOutputFormats(); + StringBuffer allowedFormats = new StringBuffer("a String value among: "); int i = 0; - OutputFormat formatter; + OutputFormat formatter; while(itFormats.hasNext()){ formatter = itFormats.next(); allowedFormats.append((i == 0) ? "" : ", ").append(formatter.getMimeType()); @@ -81,7 +111,10 @@ public class FormatController< R > implements InputParamController { allowedFormats.append(" (or ").append(formatter.getShortMimeType()).append(')'); i++; } - return allowedFormats.toString(); + if (i > 0) + return allowedFormats.toString(); + else + return "a String value."; } } diff --git a/src/tap/parameters/MaxRecController.java b/src/tap/parameters/MaxRecController.java index 08f29b6..9424a0a 100644 --- a/src/tap/parameters/MaxRecController.java +++ b/src/tap/parameters/MaxRecController.java @@ -17,53 +17,82 @@ package tap.parameters; * along with TAPLibrary. If not, see . * * Copyright 2012-2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomisches Rechen Institute (ARI) + * Astronomisches Rechen Institut (ARI) */ import tap.ServiceConnection; import tap.ServiceConnection.LimitUnit; import tap.TAPJob; import uws.UWSException; -import uws.UWSExceptionFactory; import uws.job.parameters.InputParamController; /** - * The logic of the output limit is set in this class. Here it is: + *

Let controlling the maximum number of rows that can be output by a TAP service. + * The maximum and default values are provided by the service connection.

* - * - If no value is specified by the TAP client, none is returned. - * - If no default value is provided, no default limitation is set (={@link TAPJob#UNLIMITED_MAX_REC}). - * - If no maximum value is provided, there is no output limit (={@link TAPJob#UNLIMITED_MAX_REC}). + *

Note: + * By default, this parameter can be modified by anyone without any limitation. + * The default and maximum value is set by default to {@link TAPJob#UNLIMITED_MAX_REC}. + *

* - * @author Grégory Mantelet (CDS;ARI) - gmantele@ari.uni-heidelberg.de - * @version 1.1 (03/2014) + *

Note: + * The special value 0 means that just the metadata of the result must be returned. + * Considering the meaning of this value, it will not be considered as an {@link TAPJob#UNLIMITED_MAX_REC}, + * but like a valid value. The maximum value can then be also 0. + *

+ * + *

The logic of the output limit is set in this class. Here it is:

+ *
    + *
  • If no value is specified by the TAP client, the default value is returned.
  • + *
  • If no default value is provided, the maximum output limit is returned.
  • + *
  • If no maximum value is provided, there is no limit (={@link TAPJob#UNLIMITED_MAX_REC}).
  • + *
+ * + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (03/2015) */ public class MaxRecController implements InputParamController { - protected final ServiceConnection service; + /** Connection to the service which knows the maximum and default value of this parameter. */ + protected final ServiceConnection service; /** Indicates whether the output limit of jobs can be modified. */ protected boolean allowModification = true; - public MaxRecController(final ServiceConnection service){ + /** + * Build a controller for the MaxRec parameter. + * + * @param service Connection to the TAP service. + */ + public MaxRecController(final ServiceConnection service){ this.service = service; - allowModification(allowModification); } @Override public final Object getDefault(){ - // If a default output limit is set by the TAP service connection, return it: + // Get the default output limit: + int defaultLimit = TAPJob.UNLIMITED_MAX_REC; if (service.getOutputLimit() != null && service.getOutputLimit().length >= 2 && service.getOutputLimitType() != null && service.getOutputLimitType().length == service.getOutputLimit().length){ - if (service.getOutputLimit()[0] > 0 && service.getOutputLimitType()[0] == LimitUnit.rows) - return service.getOutputLimit()[0]; + if (service.getOutputLimit()[0] >= 0 && service.getOutputLimitType()[0] == LimitUnit.rows) + defaultLimit = service.getOutputLimit()[0]; } - // Otherwise, return no limitation: - return TAPJob.UNLIMITED_MAX_REC; + + // Get the maximum output limit, for comparison: + int maxLimit = getMaxOutputLimit(); + + // Ensure the default limit is less or equal the maximum limit: + return (defaultLimit < 0 || (maxLimit >= 0 && defaultLimit > maxLimit)) ? maxLimit : defaultLimit; } + /** + * Get the maximum number of rows that can be output. + * + * @return Maximum output limit. + */ public final int getMaxOutputLimit(){ // If a maximum output limit is set by the TAP service connection, return it: if (service.getOutputLimit() != null && service.getOutputLimit().length >= 2 && service.getOutputLimitType() != null && service.getOutputLimitType().length == service.getOutputLimit().length){ - if (service.getOutputLimit()[1] > 0 && service.getOutputLimitType()[1] == LimitUnit.rows) + if (service.getOutputLimit()[1] >= 0 && service.getOutputLimitType()[1] == LimitUnit.rows) return service.getOutputLimit()[1]; } // Otherwise, there is no limit: @@ -74,7 +103,7 @@ public class MaxRecController implements InputParamController { public Object check(Object value) throws UWSException{ // If no limit is provided by the TAP client, none is returned: if (value == null) - return null; + return getDefault(); // Parse the provided limit: int maxOutputLimit = getMaxOutputLimit(); @@ -86,17 +115,17 @@ public class MaxRecController implements InputParamController { try{ maxRec = Integer.parseInt(strValue); }catch(NumberFormatException nfe){ - throw UWSExceptionFactory.badFormat(null, TAPJob.PARAM_MAX_REC, strValue, null, "An integer value between " + TAPJob.UNLIMITED_MAX_REC + " and " + maxOutputLimit + " (Default value: " + defaultOutputLimit + ")."); + throw new UWSException(UWSException.BAD_REQUEST, "Wrong format for the parameter \"maxrec\": \"" + strValue + "\"! It should be a integer value between " + TAPJob.UNLIMITED_MAX_REC + " and " + maxOutputLimit + " (Default value: " + defaultOutputLimit + ")."); } }else - throw UWSExceptionFactory.badFormat(null, TAPJob.PARAM_MAX_REC, null, value.getClass().getName(), "An integer value between " + TAPJob.UNLIMITED_MAX_REC + " and " + maxOutputLimit + " (Default value: " + defaultOutputLimit + ")."); + throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, "Wrong type for the parameter \"maxrec\": class \"" + value.getClass().getName() + "\"! It should be an integer or a string containing only an integer value."); // A negative output limit is considered as an unlimited output limit: - if (maxRec < TAPJob.UNLIMITED_MAX_REC) + if (maxRec < 0) maxRec = TAPJob.UNLIMITED_MAX_REC; // If the limit is greater than the maximum one, an exception is thrown: - if (maxRec == TAPJob.UNLIMITED_MAX_REC || maxRec > maxOutputLimit) + if (maxRec < 0 || (maxOutputLimit >= 0 && maxRec > maxOutputLimit)) maxRec = maxOutputLimit; return maxRec; diff --git a/src/tap/parameters/TAPDestructionTimeController.java b/src/tap/parameters/TAPDestructionTimeController.java index 33444bd..076cc05 100644 --- a/src/tap/parameters/TAPDestructionTimeController.java +++ b/src/tap/parameters/TAPDestructionTimeController.java @@ -16,7 +16,8 @@ package tap.parameters; * You should have received a copy of the GNU Lesser General Public License * along with TAPLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.text.ParseException; @@ -24,21 +25,44 @@ import java.util.Calendar; import java.util.Date; import tap.ServiceConnection; -import tap.TAPJob; - +import uws.ISO8601Format; import uws.UWSException; -import uws.UWSExceptionFactory; - -import uws.job.UWSJob; -import uws.job.parameters.InputParamController; import uws.job.parameters.DestructionTimeController.DateField; +import uws.job.parameters.InputParamController; +/** + *

Let controlling the destruction time of all jobs managed by a TAP service. + * The maximum and default values are provided by the service connection.

+ * + *

Note: + * By default, the destruction time can be modified by anyone without any limitation. + * There is no default value (that means jobs may stay forever). + *

+ * + *

The logic of the destruction time is set in this class. Here it is:

+ *
    + *
  • If no value is specified by the UWS client, the default value is returned.
  • + *
  • If no default value is provided, the maximum destruction date is returned.
  • + *
  • If no maximum value is provided, there is no destruction.
  • + *
+ * + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (11/2014) + */ public class TAPDestructionTimeController implements InputParamController { - protected final ServiceConnection service; + /** Connection to the service which knows the maximum and default value of this parameter. */ + protected final ServiceConnection service; + + /** Indicates whether the execution duration of jobs can be modified. */ protected boolean allowModification = true; - public TAPDestructionTimeController(final ServiceConnection service){ + /** + * Build a controller for the Destruction parameter. + * + * @param service Connection to the TAP service. + */ + public TAPDestructionTimeController(final ServiceConnection service){ this.service = service; } @@ -47,10 +71,21 @@ public class TAPDestructionTimeController implements InputParamController { return allowModification; } + /** + * Let indicate whether the destruction time of any managed job can be modified. + * + * @param allowModif true if the destruction time can be modified, false otherwise. + */ public final void allowModification(final boolean allowModif){ allowModification = allowModif; } + /** + * Get the default period during which a job is kept. + * After this period, the job should be destroyed. + * + * @return The default retention period, -1 if none is provided. + */ public final int getDefaultRetentionPeriod(){ if (service.getRetentionPeriod() != null && service.getRetentionPeriod().length >= 2){ if (service.getRetentionPeriod()[0] > 0) @@ -61,19 +96,31 @@ public class TAPDestructionTimeController implements InputParamController { @Override public final Object getDefault(){ + // Get the default period and ensure it is always less or equal the maximum period, if any: int defaultPeriod = getDefaultRetentionPeriod(); + int maxPeriod = getMaxRetentionPeriod(); + if (defaultPeriod <= 0 || (maxPeriod > 0 && defaultPeriod > maxPeriod)) + defaultPeriod = maxPeriod; + + // Build and return the date: if (defaultPeriod > 0){ Calendar date = Calendar.getInstance(); try{ date.add(DateField.SECOND.getFieldIndex(), defaultPeriod); return date.getTime(); - }catch(ArrayIndexOutOfBoundsException ex){ - return null; - } - }else - return null; + }catch(ArrayIndexOutOfBoundsException ex){} + } + + // If no default period is specified or if an exception occurs, the maximum destruction time must be returned: + return getMaxDestructionTime(); } + /** + * Get the maximum period during which a job is kept. + * After this period, the job should be destroyed. + * + * @return The maximum retention period, -1 if none is provided. + */ public final int getMaxRetentionPeriod(){ if (service.getRetentionPeriod() != null && service.getRetentionPeriod().length >= 2){ if (service.getRetentionPeriod()[1] > 0) @@ -82,42 +129,54 @@ public class TAPDestructionTimeController implements InputParamController { return -1; } + /** + * Gets the maximum destruction time: either computed with an interval of time or obtained directly by a maximum destruction time. + * + * @return The maximum destruction time (null means that jobs may stay forever). + */ public final Date getMaxDestructionTime(){ + // Get the maximum period: int maxPeriod = getMaxRetentionPeriod(); + + // Build and return the maximum destruction date: if (maxPeriod > 0){ Calendar date = Calendar.getInstance(); try{ date.add(DateField.SECOND.getFieldIndex(), maxPeriod); return date.getTime(); - }catch(ArrayIndexOutOfBoundsException ex){ - return null; - } - }else - return null; + }catch(ArrayIndexOutOfBoundsException ex){} + } + + // If no maximum period is specified or if an exception occurs, NULL must be returned: + return null; } @Override public Object check(Object value) throws UWSException{ + // If NULL value, return the default value: if (value == null) - return null; + return getDefault(); + // Parse the given date: Date date = null; if (value instanceof Date) date = (Date)value; else if (value instanceof String){ String strValue = (String)value; try{ - date = UWSJob.dateFormat.parse(strValue); + date = ISO8601Format.parseToDate(strValue); }catch(ParseException pe){ - throw UWSExceptionFactory.badFormat(null, TAPJob.PARAM_DESTRUCTION_TIME, strValue, null, "A date not yet expired."); + throw new UWSException(UWSException.BAD_REQUEST, pe, "Wrong date format for the parameter \"destruction\": \"" + strValue + "\"! A date must be formatted in the ISO8601 format (\"yyyy-MM-dd'T'hh:mm:ss[.sss]['Z'|[+|-]hh:mm]\", fields inside brackets are optional)."); } }else - throw UWSExceptionFactory.badFormat(null, TAPJob.PARAM_DESTRUCTION_TIME, value.toString(), value.getClass().getName(), "A date not yet expired."); + throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, "Wrong type for the parameter \"destruction\": class \"" + value.getClass().getName() + "\"! It should be a Date or a string containing a date formatted in ISO8601 (\"yyyy-MM-dd'T'hh:mm:ss[.sss]['Z'|[+|-]hh:mm]\", fields inside brackets are optional)."); + // Ensure the date is before the maximum destruction time (from now): Date maxDate = getMaxDestructionTime(); if (maxDate != null && date.after(maxDate)) - throw new UWSException(UWSException.BAD_REQUEST, "The TAP service limits the DESTRUCTION INTERVAL (since now) to " + getMaxRetentionPeriod() + " s !"); + date = maxDate; + // Return the parsed date return date; } diff --git a/src/tap/parameters/TAPExecutionDurationController.java b/src/tap/parameters/TAPExecutionDurationController.java index 68f5797..0f3985c 100644 --- a/src/tap/parameters/TAPExecutionDurationController.java +++ b/src/tap/parameters/TAPExecutionDurationController.java @@ -16,23 +16,48 @@ package tap.parameters; * You should have received a copy of the GNU Lesser General Public License * along with TAPLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import tap.ServiceConnection; import tap.TAPJob; - import uws.UWSException; -import uws.UWSExceptionFactory; - import uws.job.parameters.InputParamController; +/** + *

Let controlling the execution duration of all jobs managed by a TAP service. + * The maximum and default values are provided by the service connection.

+ * + *

Note: + * By default, the execution duration can be modified by anyone without any limitation. + * The default value is {@link TAPJob#UNLIMITED_DURATION}. + *

+ * + *

The logic of the execution duration is set in this class. Here it is:

+ *
    + *
  • If no value is specified by the TAP client, the default value is returned.
  • + *
  • If no default value is provided, the maximum duration is returned.
  • + *
  • If no maximum value is provided, there is no limit (={@link TAPJob#UNLIMITED_DURATION}).
  • + *
+ * + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (11/2014) + */ public class TAPExecutionDurationController implements InputParamController { - protected final ServiceConnection service; + /** Connection to the service which knows the maximum and default value of this parameter. */ + protected final ServiceConnection service; + + /** Indicate whether the execution duration of jobs can be modified. */ protected boolean allowModification = true; - public TAPExecutionDurationController(final ServiceConnection service){ + /** + * Build a controller for the ExecutionDuration parameter. + * + * @param service Connection to the TAP service. + */ + public TAPExecutionDurationController(final ServiceConnection service){ this.service = service; } @@ -41,19 +66,32 @@ public class TAPExecutionDurationController implements InputParamController { return allowModification; } + /** + * Let indicate whether the execution duration of any managed job can be modified. + * + * @param allowModif true if the execution duration can be modified, false otherwise. + */ public final void allowModification(final boolean allowModif){ allowModification = allowModif; } @Override public final Object getDefault(){ - if (service.getExecutionDuration() != null && service.getExecutionDuration().length >= 2){ - if (service.getExecutionDuration()[0] > 0) - return service.getExecutionDuration()[0]; - } - return TAPJob.UNLIMITED_DURATION; + // Get the default value from the service connection: + long defaultVal = TAPJob.UNLIMITED_DURATION; + if (service.getExecutionDuration() != null && service.getExecutionDuration().length >= 2) + defaultVal = service.getExecutionDuration()[0]; + + // The default value is also limited by the maximum value if any: + long maxVal = getMaxDuration(); + return (defaultVal <= 0 || (maxVal > 0 && defaultVal > maxVal)) ? maxVal : defaultVal; } + /** + * Gets the maximum execution duration. + * + * @return The maximum execution duration (0 or less mean an unlimited duration). + */ public final long getMaxDuration(){ if (service.getExecutionDuration() != null && service.getExecutionDuration().length >= 2){ if (service.getExecutionDuration()[1] > 0) @@ -63,28 +101,36 @@ public class TAPExecutionDurationController implements InputParamController { } @Override - public Object check(Object value) throws UWSException{ + public Object check(final Object value) throws UWSException{ + // If no value, return the default one: if (value == null) - return null; + return getDefault(); - long defaultDuration = ((Long)getDefault()).longValue(), maxDuration = getMaxDuration(); - Long duration; + // Get the default and maximum durations for comparison: + long defaultDuration = (Long)getDefault(), maxDuration = getMaxDuration(); + // Parse the given duration: + Long duration; if (value instanceof Long) duration = (Long)value; + else if (value instanceof Integer) + duration = (long)((Integer)value).intValue(); else if (value instanceof String){ try{ duration = Long.parseLong((String)value); }catch(NumberFormatException nfe){ - throw UWSExceptionFactory.badFormat(null, TAPJob.PARAM_EXECUTION_DURATION, value.toString(), null, "A long value between " + TAPJob.UNLIMITED_DURATION + " and " + maxDuration + " (Default value: " + defaultDuration + ")."); + throw new UWSException(UWSException.BAD_REQUEST, "Wrong format for the parameter \"executionduration\": \"" + value.toString() + "\"! It should be a long numeric value between " + TAPJob.UNLIMITED_DURATION + " and " + maxDuration + " (Default value: " + defaultDuration + ")."); } }else - throw UWSExceptionFactory.badFormat(null, TAPJob.PARAM_EXECUTION_DURATION, null, value.getClass().getName(), "A long value between " + TAPJob.UNLIMITED_DURATION + " and " + maxDuration + " (Default value: " + defaultDuration + ")."); + throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, "Wrong type for the parameter \"executionduration\": class \"" + value.getClass().getName() + "\"! It should be long or a string containing only a long value."); - if (duration < TAPJob.UNLIMITED_DURATION) + // A negative value must be considered as an unlimited duration: + if (duration <= 0) duration = TAPJob.UNLIMITED_DURATION; - else if (maxDuration > TAPJob.UNLIMITED_DURATION && duration > maxDuration) - throw new UWSException(UWSException.BAD_REQUEST, "The TAP service limits the execution duration to maximum " + maxDuration + " seconds !"); + + // Ensure the given value is less than the maximum duration: + if (maxDuration > 0 && (duration > maxDuration || duration <= 0)) + duration = maxDuration; return duration; } diff --git a/src/tap/parameters/TAPParameters.java b/src/tap/parameters/TAPParameters.java index f45c833..e591092 100644 --- a/src/tap/parameters/TAPParameters.java +++ b/src/tap/parameters/TAPParameters.java @@ -16,219 +16,224 @@ package tap.parameters; * You should have received a copy of the GNU Lesser General Public License * along with TAPLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ -import java.io.File; -import java.io.IOException; - -import java.util.Collection; -import java.util.Date; -import java.util.Enumeration; +import java.util.Arrays; import java.util.HashMap; +import java.util.Iterator; +import java.util.List; import java.util.Map; +import java.util.Map.Entry; import javax.servlet.http.HttpServletRequest; -import com.oreilly.servlet.MultipartRequest; -import com.oreilly.servlet.multipart.FileRenamePolicy; - import tap.ServiceConnection; import tap.TAPException; import tap.TAPJob; - -import tap.upload.TableLoader; - import uws.UWSException; - import uws.job.parameters.InputParamController; import uws.job.parameters.StringParamController; import uws.job.parameters.UWSParameters; /** - * This class describes all defined parameters of a TAP request. + * This class lets list and describe all standard TAP parameters + * submitted by a TAP client to this TAP service. * - * @author Grégory Mantelet (CDS) - * @version 06/2012 + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (12/2014) */ public class TAPParameters extends UWSParameters { + /** All the TAP parameters. */ + protected static final List TAP_PARAMETERS = Arrays.asList(new String[]{TAPJob.PARAM_REQUEST,TAPJob.PARAM_LANGUAGE,TAPJob.PARAM_VERSION,TAPJob.PARAM_FORMAT,TAPJob.PARAM_QUERY,TAPJob.PARAM_MAX_REC,TAPJob.PARAM_UPLOAD}); + /** - * All the TAP parameters. + * Create an empty list of parameters. + * + * @param service Description of the TAP service in which the parameters are created and will be used. */ - protected static final String[] TAP_PARAMETERS = new String[]{TAPJob.PARAM_REQUEST,TAPJob.PARAM_LANGUAGE,TAPJob.PARAM_VERSION,TAPJob.PARAM_FORMAT,TAPJob.PARAM_QUERY,TAPJob.PARAM_MAX_REC,TAPJob.PARAM_UPLOAD}; - - /** Part of HTTP content type header. */ - public static final String MULTIPART = "multipart/"; - - /** All the tables to upload. If NULL, there is no tables to upload. */ - protected TableLoader[] tablesToUpload = null; - - @SuppressWarnings({"unchecked"}) - public TAPParameters(final ServiceConnection service){ - this(service, (Collection)null, null); + public TAPParameters(final ServiceConnection service){ + super(TAP_PARAMETERS, buildDefaultControllers(service)); } - public TAPParameters(final ServiceConnection service, final Collection expectedAdditionalParams, final Map inputParamControllers){ - super(expectedAdditionalParams, inputParamControllers); - initDefaultTAPControllers(service); - } - - public TAPParameters(final HttpServletRequest request, final ServiceConnection service) throws UWSException, TAPException{ - this(request, service, null, null); + /** + * Create a {@link TAPParameters} instance whose the parameters must be extracted from the given {@link HttpServletRequest}. + * + * @param request HTTP request containing the parameters to gather inside this class. + * @param service Description of the TAP service in which the parameters are created and will be used. + * + * @throws TAPException If any error occurs while extracting the DALIParameters OR while setting a parameter. + * + * @see #getParameters(HttpServletRequest) + */ + public TAPParameters(final HttpServletRequest request, final ServiceConnection service) throws TAPException{ + this(service, getParameters(request)); } - @SuppressWarnings("unchecked") - public TAPParameters(final HttpServletRequest request, final ServiceConnection service, final Collection expectedAdditionalParams, final Map inputParamControllers) throws UWSException, TAPException{ - this(service, expectedAdditionalParams, inputParamControllers); - MultipartRequest multipart = null; + /** + * Create a {@link TAPParameters} instance whose the parameters are given in parameter. + * + * @param service Description of the TAP service. Limits of the standard TAP parameters are listed in it. + * @param params List of parameters to load inside this object. + * + * @throws TAPException If any error occurs while extracting the DALIParameters OR while setting a parameter. + */ + public TAPParameters(final ServiceConnection service, final Map params) throws TAPException{ + super(TAP_PARAMETERS, buildDefaultControllers(service)); - // Multipart HTTP parameters: - if (isMultipartContent(request)){ - if (!service.uploadEnabled()) - throw new TAPException("Request error ! This TAP service has no Upload capability !"); + if (params != null && !params.isEmpty()){ + // Deal with the UPLOAD parameter(s): + DALIUpload.getDALIUploads(params, true, service.getFileManager()); - File uploadDir = service.getFileManager().getUploadDirectory(); + // Load all parameters: + Iterator> it = params.entrySet().iterator(); + Entry entry; try{ - multipart = new MultipartRequest(request, (uploadDir != null) ? uploadDir.getAbsolutePath() : null, service.getMaxUploadSize(), new FileRenamePolicy(){ - @Override - public File rename(File file){ - return new File(file.getParentFile(), (new Date()).toString() + "_" + file.getName()); - } - }); - Enumeration e = multipart.getParameterNames(); - while(e.hasMoreElements()){ - String param = e.nextElement(); - set(param, multipart.getParameter(param)); + while(it.hasNext()){ + entry = it.next(); + set(entry.getKey(), entry.getValue()); } - }catch(IOException ioe){ - throw new TAPException("Error while reading the Multipart content !", ioe); - }catch(IllegalArgumentException iae){ - String confError = iae.getMessage(); - if (service.getMaxUploadSize() <= 0) - confError = "The maximum upload size (see ServiceConnection.getMaxUploadSize() must be positive !"; - else if (uploadDir == null) - confError = "Missing upload directory (see TAPFileManager.getUploadDirectory()) !"; - throw new TAPException("Incorrect Upload capability configuration ! " + confError, iae); - } - - }// Classic HTTP parameters (GET or POST): - else{ - // Extract and identify each pair (key,value): - Enumeration e = request.getParameterNames(); - while(e.hasMoreElements()){ - String name = e.nextElement(); - set(name, request.getParameter(name)); + }catch(UWSException ue){ + throw new TAPException(ue); } } - - // Identify the tables to upload, if any: - String uploadParam = getUpload(); - if (service.uploadEnabled() && uploadParam != null) - tablesToUpload = buildLoaders(uploadParam, multipart); } - public TAPParameters(final ServiceConnection service, final Map params) throws UWSException, TAPException{ - this(service, params, null, null); - } - - public TAPParameters(final ServiceConnection service, final Map params, final Collection expectedAdditionalParams, final Map inputParamControllers) throws UWSException, TAPException{ - super(params, expectedAdditionalParams, inputParamControllers); - initDefaultTAPControllers(service); - } - - @Override - protected final HashMap getDefaultControllers(){ - return new HashMap(10); - } - - protected < R > void initDefaultTAPControllers(final ServiceConnection service){ - if (!mapParamControllers.containsKey(TAPJob.PARAM_EXECUTION_DURATION)) - mapParamControllers.put(TAPJob.PARAM_EXECUTION_DURATION, new TAPExecutionDurationController(service)); - - if (!mapParamControllers.containsKey(TAPJob.PARAM_DESTRUCTION_TIME)) - mapParamControllers.put(TAPJob.PARAM_DESTRUCTION_TIME, new TAPDestructionTimeController(service)); - - if (!mapParamControllers.containsKey(TAPJob.PARAM_REQUEST)) - mapParamControllers.put(TAPJob.PARAM_REQUEST, new StringParamController(TAPJob.PARAM_REQUEST, null, new String[]{TAPJob.REQUEST_DO_QUERY,TAPJob.REQUEST_GET_CAPABILITIES}, true)); - - if (!mapParamControllers.containsKey(TAPJob.PARAM_LANGUAGE)) - mapParamControllers.put(TAPJob.PARAM_LANGUAGE, new StringParamController(TAPJob.PARAM_LANGUAGE, TAPJob.LANG_ADQL, null, true)); - - if (!mapParamControllers.containsKey(TAPJob.PARAM_VERSION)) - mapParamControllers.put(TAPJob.PARAM_VERSION, new StringParamController(TAPJob.PARAM_VERSION, TAPJob.VERSION_1_0, new String[]{TAPJob.VERSION_1_0}, true)); - - if (!mapParamControllers.containsKey(TAPJob.PARAM_QUERY)) - mapParamControllers.put(TAPJob.PARAM_QUERY, new StringParamController(TAPJob.PARAM_QUERY)); - - if (!mapParamControllers.containsKey(TAPJob.PARAM_UPLOAD)) - mapParamControllers.put(TAPJob.PARAM_UPLOAD, new StringParamController(TAPJob.PARAM_UPLOAD)); - - if (!mapParamControllers.containsKey(TAPJob.PARAM_FORMAT)) - mapParamControllers.put(TAPJob.PARAM_FORMAT, new FormatController(service)); - - if (!mapParamControllers.containsKey(TAPJob.PARAM_MAX_REC)) - mapParamControllers.put(TAPJob.PARAM_MAX_REC, new MaxRecController(service)); - } - - @Override - protected String normalizeParamName(String name){ - if (name != null && !name.trim().isEmpty()){ - for(String tapParam : TAP_PARAMETERS){ - if (name.equalsIgnoreCase(tapParam)) - return tapParam; - } - } - return super.normalizeParamName(name); + /** + *

Build a map containing all controllers for all standard TAP parameters.

+ * + *

Note: + * All standard parameters, except UPLOAD. Indeed, since this parameter can be provided in several times (in one HTTP request) + * and needs to be interpreted immediately after initialization, no controller has been set for it. Its value will be actually + * tested in the constructor while interpreting it. + *

+ * + * @param service Description of the TAP service. + * + * @return Map of all default controllers. + * + * @since 2.0 + */ + protected static final Map buildDefaultControllers(final ServiceConnection service){ + Map controllers = new HashMap(10); + controllers.put(TAPJob.PARAM_EXECUTION_DURATION, new TAPExecutionDurationController(service)); + controllers.put(TAPJob.PARAM_DESTRUCTION_TIME, new TAPDestructionTimeController(service)); + controllers.put(TAPJob.PARAM_REQUEST, new StringParamController(TAPJob.PARAM_REQUEST, null, new String[]{TAPJob.REQUEST_DO_QUERY,TAPJob.REQUEST_GET_CAPABILITIES}, true)); + controllers.put(TAPJob.PARAM_LANGUAGE, new StringParamController(TAPJob.PARAM_LANGUAGE, TAPJob.LANG_ADQL, null, true)); + controllers.put(TAPJob.PARAM_VERSION, new StringParamController(TAPJob.PARAM_VERSION, TAPJob.VERSION_1_0, new String[]{TAPJob.VERSION_1_0}, true)); + controllers.put(TAPJob.PARAM_QUERY, new StringParamController(TAPJob.PARAM_QUERY)); + controllers.put(TAPJob.PARAM_FORMAT, new FormatController(service)); + controllers.put(TAPJob.PARAM_MAX_REC, new MaxRecController(service)); + return controllers; } - @Override - public String[] update(UWSParameters newParams) throws UWSException{ - if (newParams != null && !(newParams instanceof TAPParameters)) - throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, "Can not update a TAPParameters instance with only a UWSException !"); - - String[] updated = super.update(newParams); - for(String p : updated){ - if (p.equals(TAPJob.PARAM_UPLOAD)){ - tablesToUpload = ((TAPParameters)newParams).tablesToUpload; - break; + /** + *

Get the value of the given parameter, but as a String, whatever is its original type.

+ * + *

Basically, the different cases of conversion into String are the following:

+ *
    + *
  • NULL: NULL is returned.
  • + *
  • An array (of whatever is the items' type): a string in which each Object.toString() are concatenated ; each item is separated by a semicolon
  • + *
  • Anything else: Object.toString()
  • + *
+ * + * @param paramName Name of the parameter whose the value must be returned as a String. + * + * @return The string value of the specified parameter. + */ + protected final String getStringParam(final String paramName){ + // Get the parameter value as an Object: + Object value = params.get(paramName); + + // Convert this Object into a String: + // CASE: NULL + if (value == null) + return null; + + // CASE: ARRAY + else if (value.getClass().isArray()){ + StringBuffer buf = new StringBuffer(); + for(Object o : (Object[])value){ + if (buf.length() > 0) + buf.append(';'); + buf.append(o.toString()); } + return buf.toString(); } - return updated; - } - - protected final String getStringParam(final String paramName){ - return (params.get(paramName) != null) ? params.get(paramName).toString() : null; + // DEFAULT: + else + return value.toString(); } + /** + * Get the value of the standard TAP parameter "REQUEST". + * @return "REQUEST" value. + */ public final String getRequest(){ return getStringParam(TAPJob.PARAM_REQUEST); } + /** + * Get the value of the standard TAP parameter "LANG". + * @return "LANG" value. + */ public final String getLang(){ return getStringParam(TAPJob.PARAM_LANGUAGE); } + /** + * Get the value of the standard TAP parameter "VERSION". + * @return "VERSION" value. + */ public final String getVersion(){ return getStringParam(TAPJob.PARAM_VERSION); } + /** + * Get the value of the standard TAP parameter "FORMAT". + * @return "FORMAT" value. + */ public final String getFormat(){ return getStringParam(TAPJob.PARAM_FORMAT); } + /** + * Get the value of the standard TAP parameter "QUERY". + * @return "QUERY" value. + */ public final String getQuery(){ return getStringParam(TAPJob.PARAM_QUERY); } + /** + *

Get the value of the standard TAP parameter "UPLOAD".

+ *

Note: + * This parameter is generally a set of several Strings, each representing one table to upload. + * This function returns this set as a String in which each items are joined, semicolon separated, inside a single String. + *

+ * @return "UPLOAD" value. + */ public final String getUpload(){ return getStringParam(TAPJob.PARAM_UPLOAD); } - public final TableLoader[] getTableLoaders(){ - return tablesToUpload; + /** + * Get the list of all tables uploaded and defined by the standard TAP parameter "UPLOAD". + * + * @return Tables to upload in database at query execution. + */ + public final DALIUpload[] getUploadedTables(){ + return (DALIUpload[])get(TAPJob.PARAM_UPLOAD); } + /** + * Get the value of the standard TAP parameter "MAX_REC". + * This value is the maximum number of rows that the result of the query must contain. + * + * @return Maximum number of output rows. + */ public final Integer getMaxRec(){ Object value = params.get(TAPJob.PARAM_MAX_REC); if (value != null){ @@ -250,93 +255,26 @@ public class TAPParameters extends UWSParameters { } /** - * Utility method that determines whether the request contains multipart - * content. - * - * @param request The servlet request to be evaluated. Must be non-null. - * - * @return true if the request is multipart; - * false otherwise. - */ - public static final boolean isMultipartContent(HttpServletRequest request){ - if (!"post".equals(request.getMethod().toLowerCase())){ - return false; - } - String contentType = request.getContentType(); - if (contentType == null){ - return false; - } - if (contentType.toLowerCase().startsWith(MULTIPART)){ - return true; - } - return false; - } - - /** - * Builds as many TableLoader instances as tables to upload. - * - * @param upload The upload field (syntax: "tableName1,URI1 ; tableName2,URI2 ; ...", where URI may start by "param:" to indicate that the VOTable is inline). - * @param multipart The multipart content of the request if any. + *

Check the coherence between all TAP parameters.

* - * @return All table loaders (one per table to upload). + *

+ * This function does not test individually each parameters, but all of them as a coherent whole. + * Thus, the parameter REQUEST must be provided and if its value is "doQuery", the parameters LANG and QUERY must be also provided. + *

* - * @throws TAPException If the syntax of the "upload" field is incorrect. + * @throws TAPException If one required parameter is missing. */ - private TableLoader[] buildLoaders(final String upload, final MultipartRequest multipart) throws TAPException{ - if (upload == null || upload.trim().isEmpty()) - return new TableLoader[0]; - - String[] pairs = upload.split(";"); - TableLoader[] loaders = new TableLoader[pairs.length]; - - for(int i = 0; i < pairs.length; i++){ - String[] table = pairs[i].split(","); - if (table.length != 2) - throw new TAPException("Bad syntax ! An UPLOAD parameter must contain a list of pairs separated by a ';'. Each pair is composed of 2 parts, a table name and a URI separated by a ','."); - loaders[i] = new TableLoader(table[0], table[1], multipart); - } - - return loaders; - } - public void check() throws TAPException{ // Check that required parameters are not NON-NULL: String requestParam = getRequest(); if (requestParam == null) - throw new TAPException("The parameter \"" + TAPJob.PARAM_REQUEST + "\" must be provided and its value must be equal to \"" + TAPJob.REQUEST_DO_QUERY + "\" or \"" + TAPJob.REQUEST_GET_CAPABILITIES + "\" !"); + throw new TAPException("The parameter \"" + TAPJob.PARAM_REQUEST + "\" must be provided and its value must be equal to \"" + TAPJob.REQUEST_DO_QUERY + "\" or \"" + TAPJob.REQUEST_GET_CAPABILITIES + "\"!", UWSException.BAD_REQUEST); if (requestParam.equals(TAPJob.REQUEST_DO_QUERY)){ if (get(TAPJob.PARAM_LANGUAGE) == null) - throw new TAPException("The parameter \"" + TAPJob.PARAM_LANGUAGE + "\" must be provided if " + TAPJob.PARAM_REQUEST + "=" + TAPJob.REQUEST_DO_QUERY + " !"); + throw new TAPException("The parameter \"" + TAPJob.PARAM_LANGUAGE + "\" must be provided if " + TAPJob.PARAM_REQUEST + "=" + TAPJob.REQUEST_DO_QUERY + "!", UWSException.BAD_REQUEST); else if (get(TAPJob.PARAM_QUERY) == null) - throw new TAPException("The parameter \"" + TAPJob.PARAM_QUERY + "\" must be provided if " + TAPJob.PARAM_REQUEST + "=" + TAPJob.REQUEST_DO_QUERY + " !"); - } - - // Check the version if needed: - /*Object versionParam = get(TAPJob.PARAM_VERSION); - if (versionParam != null && !versionParam.equals("1") && !versionParam.equals("1.0")) - throw new TAPException("Version \""+versionParam+"\" of TAP not implemented !");*/ - - /*// Check format if needed: - if (format == null) - format = FORMAT_VOTABLE; - - // Check maxrec: - if (maxrec <= -1) - maxrec = defaultOutputLimit; - - if (maxOutputLimit > -1){ - if (maxrec > maxOutputLimit) - maxrec = maxOutputLimit; - else if (maxrec <= -1) - maxrec = maxOutputLimit; - }*/ - } - - public static final void deleteUploadedTables(final TableLoader[] loaders){ - if (loaders != null){ - for(TableLoader loader : loaders) - loader.deleteFile(); + throw new TAPException("The parameter \"" + TAPJob.PARAM_QUERY + "\" must be provided if " + TAPJob.PARAM_REQUEST + "=" + TAPJob.REQUEST_DO_QUERY + "!", UWSException.BAD_REQUEST); } } } diff --git a/src/tap/resource/ASync.java b/src/tap/resource/ASync.java index 38ba10c..7aef6e2 100644 --- a/src/tap/resource/ASync.java +++ b/src/tap/resource/ASync.java @@ -16,10 +16,12 @@ package tap.resource; * You should have received a copy of the GNU Lesser General Public License * along with TAPLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.io.IOException; + import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; @@ -27,21 +29,74 @@ import javax.servlet.http.HttpServletResponse; import tap.ServiceConnection; import tap.TAPException; +import tap.TAPJob; import uws.UWSException; import uws.job.JobList; +import uws.job.UWSJob; +import uws.job.manager.AbstractQueuedExecutionManager; +import uws.job.manager.QueuedExecutionManager; import uws.service.UWSService; import uws.service.backup.UWSBackupManager; +import uws.service.log.UWSLog; +import uws.service.log.UWSLog.LogLevel; +/** + *

Asynchronous resource of a TAP service.

+ * + *

+ * Requests sent to this resource are ADQL queries (plus some execution parameters) to execute asynchronously. + * Results and/or errors of the execution are stored on the server side and can be fetched by the user whenever he wants. + *

+ * + *

+ * This resource is actually another VO service: a UWS (Universal Worker Service pattern). + * That's why all requests sent to this resource are actually forwarded to an instance of {@link UWSService}. + * All the behavior of UWS described by the IVOA is already fully implemented by this implementation. + *

+ * + *

This resource is also representing the only jobs' list of this UWS service.

+ * + *

The UWS service is created and configured at the creation of this resource. Here are the list of the most important configured elements:

+ *
    + *
  • User identification: the user identifier is the same as the one used by the TAP service. It is provided by the given {@link ServiceConnection}.
  • + *
  • Jobs' lists: the /async resource of TAP contains only one jobs' list. Its name is "async" and is accessed directly when requesting the /async resource.
  • + *
  • Job execution management: an execution manager is created at the creation of this resource. It is queuing jobs when a maximum number of asynchronous jobs + * is already running. This maximum is provided by the TAP service description: {@link ServiceConnection#getNbMaxAsyncJobs()}. Jobs are also queued if no more DB + * connection is available ; when connection(s) will be available, this resource will be notified by {@link #freeConnectionAvailable()} so that the execution manager + * can be refreshed.
  • + *
  • Backup and Restoration: UWS jobs can be saved at any defined moment. It is particularly useful when an grave error occurs and merely when the service must be restarted. + * Then, at the creation of this resource, the jobs are restored. Thus, the restart has been transparent for the users: they did not lose any job + * (except those at the origin of the grave error maybe).
  • + *
  • Error logging: the created {@link UWSService} instance is using the same logger as the TAP service. It is also provided by the given {@link ServiceConnection} object at creation.
  • + *
+ * + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (04/2015) + * + * @see UWSService + */ public class ASync implements TAPResource { + /** Name of this TAP resource. */ public static final String RESOURCE_NAME = "async"; - @SuppressWarnings("unchecked") + /** Description of the TAP service owning this resource. */ protected final ServiceConnection service; + /** UWS service represented by this TAP resource. */ protected final UWSService uws; - - @SuppressWarnings("unchecked") - public ASync(ServiceConnection service) throws UWSException, TAPException{ + /** The only jobs' list managed by the inner UWS service. This resource represent the UWS but also this jobs' list. */ + protected final JobList jobList; + + /** + * Build an Asynchronous Resource of a TAP service. + * + * @param service Description of the TAP service which will own this resource. + * + * @throws TAPException If any error occurs while creating a UWS service or its backup manager. + * @throws UWSException If any error occurs while setting a new execution manager to the recent inner UWS service, + * or while restoring a UWS backup. + */ + public ASync(final ServiceConnection service) throws UWSException, TAPException{ this.service = service; uws = service.getFactory().createUWS(); @@ -49,8 +104,12 @@ public class ASync implements TAPResource { if (uws.getUserIdentifier() == null) uws.setUserIdentifier(service.getUserIdentifier()); - if (uws.getJobList(getName()) == null) - uws.addJobList(new JobList(getName())); + if (uws.getJobList(getName()) == null){ + jobList = new JobList(getName()); + uws.addJobList(jobList); + jobList.setExecutionManager(new AsyncExecutionManager(service.getLogger(), service.getNbMaxAsyncJobs())); + }else + jobList = uws.getJobList(getName()); if (uws.getBackupManager() == null) uws.setBackupManager(service.getFactory().createUWSBackupManager(uws)); @@ -61,49 +120,114 @@ public class ASync implements TAPResource { int[] report = uws.getBackupManager().restoreAll(); String errorMsg = null; if (report == null || report.length == 0) - errorMsg = "GRAVE error while the restoration of the asynchronous jobs !"; + errorMsg = "GRAVE error while the restoration of the asynchronous jobs!"; else if (report.length < 4) - errorMsg = "Incorrect restoration report format ! => Impossible to know the restoration status !"; + errorMsg = "Incorrect restoration report format! => Impossible to know the restoration status!"; else if (report[0] != report[1]) - errorMsg = "FAILED restoration of the asynchronous jobs: " + report[0] + " on " + report[1] + " restored !"; + errorMsg = "FAILED restoration of the asynchronous jobs: " + report[0] + " on " + report[1] + " restored!"; else backupManager.setEnabled(true); if (errorMsg != null){ errorMsg += " => Backup disabled."; - service.getLogger().error(errorMsg); + service.getLogger().logTAP(LogLevel.FATAL, null, "ASYNC_INIT", errorMsg, null); throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, errorMsg); } } } + /** + *

Notify this TAP resource that free DB connection(s) is(are) now available. + * It means that the execution manager should be refreshed in order to execute one or more queued jobs.

+ * + *

Note: + * This function has no effect if there is no execution manager. + *

+ */ + public void freeConnectionAvailable(){ + if (jobList.getExecutionManager() != null) + jobList.getExecutionManager().refresh(); + } + @Override public String getName(){ return RESOURCE_NAME; } @Override - public void setTAPBaseURL(String baseURL){ + public void setTAPBaseURL(final String baseURL){ ; } + /** + * Get the UWS behind this TAP resource. + * + * @return The inner UWS used by this TAP resource. + */ public final UWSService getUWS(){ return uws; } @Override - public void init(ServletConfig config) throws ServletException{ + public void init(final ServletConfig config) throws ServletException{ ; } @Override public void destroy(){ - ; + if (uws != null) + uws.destroy(); } @Override - public boolean executeResource(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException, TAPException, UWSException{ - return uws.executeRequest(request, response); + public boolean executeResource(final HttpServletRequest request, final HttpServletResponse response) throws IOException, TAPException{ + try{ + + // Ensure the service is currently available: + if (!service.isAvailable()) + throw new TAPException("Can not execute a query: this TAP service is not available! " + service.getAvailability(), UWSException.SERVICE_UNAVAILABLE); + + // Forward the request to the UWS service: + return uws.executeRequest(request, response); + + }catch(UWSException ue){ + service.getLogger().logTAP(LogLevel.FATAL, null, null, "Error while executing the /async resource.", ue); + throw new TAPException(ue); + } + } + + /** + * An execution manager which queues jobs when too many asynchronous jobs are running or + * when no more DB connection is available for the moment. + * + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (02/2015) + * @since 2.0 + */ + private class AsyncExecutionManager extends AbstractQueuedExecutionManager { + + /** The maximum number of running jobs. */ + protected int nbMaxRunningJobs = QueuedExecutionManager.NO_QUEUE; + + /** + * Build a queuing execution manager. + * + * @param logger Logger to use. + * @param maxRunningJobs Maximum number of asynchronous jobs that can run in the same time. + */ + public AsyncExecutionManager(UWSLog logger, int maxRunningJobs){ + super(logger); + nbMaxRunningJobs = (maxRunningJobs <= 0) ? QueuedExecutionManager.NO_QUEUE : maxRunningJobs; + } + + @Override + public boolean isReadyForExecution(final UWSJob jobToExecute){ + if (!hasQueue()) + return ((TAPJob)jobToExecute).isReadyForExecution(); + else + return (runningJobs.size() < nbMaxRunningJobs) && ((TAPJob)jobToExecute).isReadyForExecution(); + } + } } diff --git a/src/tap/resource/Availability.java b/src/tap/resource/Availability.java index 73832f8..aede393 100644 --- a/src/tap/resource/Availability.java +++ b/src/tap/resource/Availability.java @@ -16,7 +16,8 @@ package tap.resource; * You should have received a copy of the GNU Lesser General Public License * along with TAPLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.io.IOException; @@ -28,23 +29,42 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import tap.ServiceConnection; +import tap.TAPException; +import uk.ac.starlink.votable.VOSerializer; +import uws.UWSToolBox; +/** + *

TAP resource describing the availability of a TAP service.

+ * + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (04/2015) + */ public class Availability implements TAPResource, VOSIResource { + /** Name of this TAP resource. */ public static final String RESOURCE_NAME = "availability"; - private final ServiceConnection service; + /** Description of the TAP service owning this resource. */ + protected final ServiceConnection service; + + /**

URL toward this TAP resource. + * This URL is particularly important for its declaration in the capabilities of the TAP service.

+ * + *

Note: By default, it is just the name of this resource. It is updated after initialization of the service + * when the TAP service base URL is communicated to its resources. Then, it is: baseTAPURL + "/" + RESOURCE_NAME.

*/ protected String accessURL = getName(); - protected Availability(ServiceConnection service){ + /** + * Build a "availability" resource. + * + * @param service Description of the TAP service which will own this resource. + */ + protected Availability(final ServiceConnection service){ this.service = service; } - public ServiceConnection getService(){ - return service; - } - - public final void setTAPBaseURL(String baseURL){ + @Override + public final void setTAPBaseURL(final String baseURL){ accessURL = ((baseURL == null) ? "" : (baseURL + "/")) + getName(); } @@ -69,7 +89,7 @@ public class Availability implements TAPResource, VOSIResource { } @Override - public void init(ServletConfig config) throws ServletException{ + public void init(final ServletConfig config) throws ServletException{ ; } @@ -79,19 +99,40 @@ public class Availability implements TAPResource, VOSIResource { } @Override - public boolean executeResource(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{ - if (!request.getMethod().equalsIgnoreCase("GET")) // ERREUR 405 selon VOSI (cf p.4) - response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "The AVAILABILITY resource is only accessible in HTTP-GET !"); - + public boolean executeResource(final HttpServletRequest request, final HttpServletResponse response) throws IOException, TAPException{ + /* "In the REST binding, the support interfaces shall have distinct URLs in the HTTP scheme and shall be accessible by the GET operation in the HTTP protocol. + * The response to an HTTP POST, PUT or DELETE to these resources is not defined by this specification. However, if an implementation has no special action + * to perform for these requests, the normal response would be a 405 "Method not allowed" error." + * (Extract of the VOSI definition: http://www.ivoa.net/documents/VOSI/20100311/PR-VOSI-1.0-20100311.html#sec2) */ + if (!request.getMethod().equalsIgnoreCase("GET")) + throw new TAPException("The AVAILABILITY resource is only accessible in HTTP-GET! No special action can be perfomed with another HTTP method.", HttpServletResponse.SC_METHOD_NOT_ALLOWED); + + // Set the response MIME type (XML): response.setContentType("text/xml"); - String xml = "\n"; - xml += "\n"; - xml += "\t" + service.isAvailable() + "\n\t" + service.getAvailability() + "\n"; - xml += ""; + // Set the character encoding: + response.setCharacterEncoding(UWSToolBox.DEFAULT_CHAR_ENCODING); + // Get the output stream: PrintWriter pw = response.getWriter(); - pw.print(xml); + + // ...And write the XML document describing the availability of the TAP service: + pw.println(""); + pw.println(""); + + // available ? (true or false) + pw.print("\t"); + pw.print(service.isAvailable()); + pw.println(""); + + // reason/description of the (non-)availability: + pw.print("\t"); + if (service.getAvailability() != null) + pw.print(VOSerializer.formatText(service.getAvailability())); + pw.println(""); + + pw.println(""); + pw.flush(); return true; diff --git a/src/tap/resource/Capabilities.java b/src/tap/resource/Capabilities.java index be6ff92..e8b2aa7 100644 --- a/src/tap/resource/Capabilities.java +++ b/src/tap/resource/Capabilities.java @@ -16,7 +16,8 @@ package tap.resource; * You should have received a copy of the GNU Lesser General Public License * along with TAPLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.io.IOException; @@ -28,19 +29,44 @@ import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import tap.TAPException; +import uk.ac.starlink.votable.VOSerializer; +import uws.UWSToolBox; + +/** + *

TAP resource describing the capabilities of a TAP service.

+ * + *

This resource just return an XML document giving a description of the TAP service and list all its VOSI resources.

+ * + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (04/2015) + */ public class Capabilities implements TAPResource, VOSIResource { + /** Name of this TAP resource. */ public static final String RESOURCE_NAME = "capabilities"; - private final TAP tap; + /** Representation of the whole TAP service. This object list all available resources ; + * resources that correspond to the capabilities this resource must list. */ + private final TAP tap; + + /**

URL toward this TAP resource. + * This URL is particularly important for its declaration in the capabilities of the TAP service.

+ * + *

Note: By default, it is just the name of this resource. It is updated after initialization of the service + * when the TAP service base URL is communicated to its resources. Then, it is: baseTAPURL + "/" + RESOURCE_NAME.

*/ protected String accessURL = getName(); - public Capabilities(TAP tap){ + /** + * Build a "/capabilities" resource. + * + * @param tap Object representation of the whole TAP service. + */ + public Capabilities(final TAP tap){ this.tap = tap; } - /** - */ + @Override public final void setTAPBaseURL(String baseURL){ accessURL = ((baseURL == null) ? "" : (baseURL + "/")) + getName(); } @@ -67,52 +93,74 @@ public class Capabilities implements TAPResource, VOSIResource { @Override public void init(ServletConfig config) throws ServletException{ - + ; } @Override public void destroy(){ - // TODO Auto-generated method stub - + ; } @Override - public boolean executeResource(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{ + public boolean executeResource(HttpServletRequest request, HttpServletResponse response) throws IOException, TAPException{ + /* "In the REST binding, the support interfaces shall have distinct URLs in the HTTP scheme and shall be accessible by the GET operation in the HTTP protocol. + * The response to an HTTP POST, PUT or DELETE to these resources is not defined by this specification. However, if an implementation has no special action + * to perform for these requests, the normal response would be a 405 "Method not allowed" error." + * (Extract of the VOSI definition: http://www.ivoa.net/documents/VOSI/20100311/PR-VOSI-1.0-20100311.html#sec2) */ + if (!request.getMethod().equalsIgnoreCase("GET")) + throw new TAPException("The CAPABILITIES resource is only accessible in HTTP-GET! No special action can be perfomed with another HTTP method.", HttpServletResponse.SC_METHOD_NOT_ALLOWED); + + // Set the response MIME type (XML): response.setContentType("application/xml"); - StringBuffer xml = new StringBuffer("\n"); - xml.append("\n"); + // Set the character encoding: + response.setCharacterEncoding(UWSToolBox.DEFAULT_CHAR_ENCODING); + + // Get the response stream: + PrintWriter out = response.getWriter(); - xml.append(tap.getCapability()); + // Write the XML document header: + out.println(""); + out.print(""); - // Build the xml document: - Iterator it = tap.getTAPResources(); + // Write the full list of this TAP capabilities: + out.print(tap.getCapability()); + + // Write the capabilities of all VOSI resources: + Iterator it = tap.getResources(); while(it.hasNext()){ TAPResource res = it.next(); if (res instanceof VOSIResource){ String cap = ((VOSIResource)res).getCapability(); - if (cap != null) - xml.append('\n').append(cap); + if (cap != null){ + out.println(); + out.print(cap); + } } } - xml.append("\n"); + // Write the end of the XML document: + out.println("\n"); - // Write the Capabilities resource into the ServletResponse: - PrintWriter out = response.getWriter(); - out.print(xml.toString()); out.flush(); return true; } - public static final String getDefaultCapability(VOSIResource res){ - return "\t\n" + "\t\t\n" + "\t\t\t " + ((res.getAccessURL() == null) ? "" : res.getAccessURL()) + " \n" + "\t\t\n" + "\t"; + /** + * Write the XML description of the given VOSI resource. + * + * @param res Resource to describe in XML. + * + * @return XML description of the given VOSI resource. + */ + public static final String getDefaultCapability(final VOSIResource res){ + return "\t\n" + "\t\t\n" + "\t\t\t " + ((res.getAccessURL() == null) ? "" : VOSerializer.formatText(res.getAccessURL())) + " \n" + "\t\t\n" + "\t"; } } diff --git a/src/tap/resource/HomePage.java b/src/tap/resource/HomePage.java new file mode 100644 index 0000000..b3210bc --- /dev/null +++ b/src/tap/resource/HomePage.java @@ -0,0 +1,307 @@ +package tap.resource; + +/* + * This file is part of TAPLibrary. + * + * TAPLibrary is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * TAPLibrary is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with TAPLibrary. If not, see . + * + * Copyright 2015 - Astronomisches Rechen Institut (ARI) + */ + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.PrintWriter; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Iterator; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import tap.TAPException; +import tap.formatter.OutputFormat; +import uws.ClientAbortException; +import uws.UWSToolBox; +import uws.service.log.UWSLog.LogLevel; + +/** + *

Write the content of the TAP service's home page.

+ * + *

Note: + * This class is using the two following {@link TAP} attributes in order to display the home page: + * {@link TAP#homePageURI} and {@link TAP#homePageMimeType}. The MIME type is used only for the third case below (local file). + *

+ * + *

Four cases are taken into account in this class, in function of the {@link TAP#homePageURI} value:

+ *
    + *
  1. a default content if no custom home page (URI) has been specified using {@link TAP#setHomePageURI(String)}. + * This default home page is hard-coded in this class and displays just an HTML list of + * links. There is one link for each resources of this TAP service (excluding the home page).
  2. + *
  3. a file inside WebContent if the given URI has no scheme (e.g. "tapIndex.jsp" or "/myFiles/tapIndex.html"). + * The URI is then an absolute (if starting with "/") or a relative path to file inside the WebContent directory. + * In this case the request is forwarded to this file. It is neither a redirection nor a copy, + * but a kind of inclusion of the interpreted file into the response. + * This method MUST be used if your home page is a JSP.
  4. + *
  5. a local file if a URI starts with "file:". In this case, the content of the local file is copied in the HTTP response. There is no interpretation. So this method should not be used for JSP.
  6. + *
  7. a distance document in all other cases. Indeed, if there is a scheme different from "file:" the given URI will be considered as a URL. + * In this case, any request to the TAP home page is redirected to this URL.
  8. + *
+ * + * @author Grégory Mantelet (ARI) + * @version 2.0 (04/2015) + * @since 2.0 + */ +public class HomePage implements TAPResource { + + /** Name of this TAP resource. */ + public static final String RESOURCE_NAME = "HOME PAGE"; + + /** TAP service owning this resource. */ + protected final TAP tap; + + public HomePage(final TAP tap){ + if (tap == null) + throw new NullPointerException("Missing TAP object! The HOME PAGE resource can not be initialized without a TAP instance."); + this.tap = tap; + } + + @Override + public void init(final ServletConfig config) throws ServletException{} + + @Override + public void destroy(){} + + @Override + public void setTAPBaseURL(String baseURL){} + + @Override + public final String getName(){ + return RESOURCE_NAME; + } + + @Override + public boolean executeResource(final HttpServletRequest request, final HttpServletResponse response) throws IOException, TAPException{ + boolean written = false; + + // Display the specified home page, if any is specified: + if (tap.homePageURI != null){ + + URI uri = null; + try{ + uri = new URI(tap.homePageURI); + /* CASE: FILE IN WebContent */ + if (uri.getScheme() == null){ + try{ + if (request.getServletContext().getResource(tap.homePageURI) != null){ + request.getRequestDispatcher(tap.homePageURI).forward(request, response); + written = true; + }else + logError("Web application file not found", null); + }catch(MalformedURLException mue){ + logError("incorrect URL syntax", mue); + } + } + /* CASE: LOCAL FILE */ + else if (uri.getScheme().equalsIgnoreCase("file")){ + // Set the content type: + response.setContentType(tap.homePageMimeType); + + // Set the character encoding: + response.setCharacterEncoding(UWSToolBox.DEFAULT_CHAR_ENCODING); + + // Get the character writer: + PrintWriter writer = response.getWriter(); + + // Get an input toward the custom home page: + BufferedReader input = null; + try{ + File f = new File(uri.getPath()); + if (f.exists() && !f.isDirectory() && f.canRead()){ + // set the content length: + response.setContentLength((int)f.length()); + + // get the input stream: + input = new BufferedReader(new FileReader(f)); + + // Copy the content of the input into the given writer: + char[] buffer = new char[2048]; + int nbReads = 0, nbBufferWritten = 0; + while((nbReads = input.read(buffer)) > 0){ + writer.write(buffer, 0, nbReads); + if ((++nbBufferWritten) % 4 == 0){ // the minimum and default buffer size of an HttpServletResponse is 8kiB => 4*2048 + UWSToolBox.flush(writer); + nbBufferWritten = 0; + } + } + UWSToolBox.flush(writer); + + // copy successful: + written = true; + }else + logError("file not found or not readable (" + f.exists() + !f.isDirectory() + f.canRead() + ")", null); + + }catch(ClientAbortException cae){ + /* This exception is an extension of IOException thrown only by some functions of UWSToolBox. + * It aims to notify about an IO error while trying to write the content of an HttpServletResponse. + * Such exception just means that the connection with the HTTP client has been closed/aborted. + * Consequently, no error nor result can be written any more in the HTTP response. + * This error, is just propagated to the TAP instance, so that stopping any current process + * for this request and so that being logged without any attempt of writing the error in the HTTP response. + */ + throw cae; + + }catch(IOException ioe){ + /* This IOException can be thrown only by InputStream.read(...) (because PrintWriter.print(...) + * silently fallbacks in case of error). + * So this error must not be propagated but caught and logged right now. Thus the default home page + * can be displayed after the error has been logged. */ + logError("the following error occurred while reading the specified local file", ioe); + + }finally{ + if (input != null) + input.close(); + } + + // Stop trying to write the home page if the HTTP request has been aborted/closed: + /*if (requestAborted) + throw new IOException("HTTP request aborted or connection with the HTTP client closed for another reason!");*/ + } + /* CASE: HTTP/HTTPS/FTP/... */ + else{ + response.sendRedirect(tap.homePageURI); + written = true; + } + + }catch(IOException ioe){ + /* This IOException can be caught here only if caused by a HTTP client abortion or by a closing of the HTTPrequest. + * So, it must be propagated until the TAP instance, where it will be merely logged as INFO. No response/error can be + * returned in the HTTP response. */ + throw ioe; + + }catch(IllegalStateException ise){ + /* This exception is caused by an attempt to reset the HTTP response buffer while a part of its + * content has already been submitted to the HTTP client. + * It must be propagated to the TAP instance so that being logged as a FATAL error. */ + throw ise; + + }catch(Exception e){ + /* The other errors are just logged, but not reported to the HTTP client, + * and then the default home page is displayed. */ + if (e instanceof URISyntaxException) + logError("the given URI has a wrong and unexpected syntax", e); + else + logError(null, e); + } + } + + // DEFAULT: list all available resources: + if (!written){ + // Set the content type: HTML document + response.setContentType("text/html"); + + // Set the character encoding: + response.setCharacterEncoding(UWSToolBox.DEFAULT_CHAR_ENCODING); + + // Get the output stream: + PrintWriter writer = response.getWriter(); + + // HTML header + CSS + Javascript: + writer.print("\n\n\t\n\t\t\n\t\tTAP HOME PAGE\n\t\t\n\t\t\n\t\n\t"); + + // Page title: + writer.print("\n\t\t

TAP HOME PAGE
"); + if (tap.getServiceConnection().getProviderName() != null) + writer.print("- " + tap.getServiceConnection().getProviderName() + " -"); + writer.print("

"); + + // Service description: + if (tap.getServiceConnection().getProviderDescription() != null) + writer.print("\n\n\t\t

Service description

\n\t\t

" + tap.getServiceConnection().getProviderDescription() + "

"); + + // List of all available resources: + writer.print("\n\n\t\t

Available resources

\n\t\t
    "); + for(TAPResource res : tap.resources.values()) + writer.println("\n\t\t\t
  • " + res.getName() + "
  • "); + writer.print("\n\t\t
"); + + // ADQL query form: + writer.print("\n\t\t\n\t\t

ADQL query

\n\t\t"); + writer.print("\n\t\t
\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t
\n\t\t\t\tQuery:\n\t\t\t\t\n\t\t\t
"); + writer.print("\n\n\t\t\t
\n\t\t\t\tExecution mode: \n\t\t\t\t\n\t\t\t
"); + writer.print("\n\t\t\t
Format:\n\t\t\t\t\n\t\t\t
"); + + // Result limit: + writer.print("\n\t\t\t
\n\t\t\t\t rows (0 to get only metadata ; a value < 0 means 'default value')\n\t\t\t\t"); + if (tap.getServiceConnection().getOutputLimit() != null && tap.getServiceConnection().getOutputLimit().length >= 2){ + writer.print("\n\t\t\t\t\t"); + writer.print("\n\t\t\t\t\t"); + } + writer.print("\n\t\t\t\t\n\t\t\t
"); + + // Execution duration limit: + writer.print("\n\t\t\t
\n\t\t\t\t seconds (a value ≤ 0 means 'default value')\n\t\t\t\t"); + if (tap.getServiceConnection().getExecutionDuration() != null && tap.getServiceConnection().getExecutionDuration().length >= 2){ + writer.print("\n\t\t\t\t\t"); + writer.print("\n\t\t\t\t\t"); + } + writer.print("\n\t\t\t\t\n\t\t\t
"); + + // Upload feature: + if (tap.getServiceConnection().uploadEnabled()) + writer.print("\n\t\t\t
\n\t\t\t\t (the uploaded table must be referenced in the ADQL query with the following full name: TAP_UPLOAD.upload)\n\t\t\t
"); + + // Footer: + writer.print("\n\t\t\t\n\t\t
\n\t\t
\n\t\t
\n\t\t
\n\t\t\t

Page generated by TAPLibrary v2.0

\n\t\t
\n\t\n"); + + writer.flush(); + + written = true; + } + + return written; + } + + /** + *

Log the given error as a TAP log message with the {@link LogLevel} ERROR, and the event "HOME_PAGE".

+ * + *

+ * The logged message starts with: Can not write the specified home page content ({tap.homePageURI}). + * After the specified error message, the following is appended: ! => The default home page will be displayed.. + *

+ * + *

+ * If the message parameter is missing, the {@link Throwable} message will be taken instead. + * And if this latter is also missing, none will be written. + *

+ * + * @param message Error message to log. + * @param error The exception at the origin of the error. + */ + protected void logError(final String message, final Throwable error){ + tap.getLogger().logTAP(LogLevel.ERROR, null, "HOME_PAGE", "Can not write the specified home page content (" + tap.homePageURI + ") " + (message == null ? (error == null ? "" : ": " + error.getMessage()) : ": " + message) + "! => The default home page will be displayed.", error); + } + +} diff --git a/src/tap/resource/Sync.java b/src/tap/resource/Sync.java index 71be76f..9b93392 100644 --- a/src/tap/resource/Sync.java +++ b/src/tap/resource/Sync.java @@ -16,33 +16,55 @@ package tap.resource; * You should have received a copy of the GNU Lesser General Public License * along with TAPLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.io.IOException; + import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import tap.TAPJob; import tap.ServiceConnection; import tap.TAPException; +import tap.TAPJob; import tap.TAPSyncJob; import tap.parameters.TAPParameters; import uws.UWSException; +/** + *

Synchronous resource of a TAP service.

+ * + *

+ * Requests sent to this resource can be either to get the capabilities of the TAP service (which should actually be accessed with the resource /capabilities) + * or to execute synchronously an ADQL query. For the second case, "synchronously" means that result or error is returned immediately when the execution ends. + * Besides, generally, the execution time is much more limited than an asynchronous query. + *

+ * + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (09/2014) + */ public class Sync implements TAPResource { + /** Name of this TAP resource. */ public static final String RESOURCE_NAME = "sync"; - protected String accessURL = null; - - protected final ServiceConnection service; + /** Description of the TAP service owning this resource. */ + protected final ServiceConnection service; + /** List of all capabilities of the TAP service. */ protected final Capabilities capabilities; - public Sync(ServiceConnection service, Capabilities capabilities){ + /** + * Build a synchronous resource for the TAP service whose the description and + * the capabilities are provided in parameters. + * + * @param service Description of the TAP service which will own this resource. + * @param capabilities Capabilities of the TAP service. + */ + public Sync(final ServiceConnection service, final Capabilities capabilities){ this.service = service; this.capabilities = capabilities; } @@ -53,12 +75,12 @@ public class Sync implements TAPResource { } @Override - public void setTAPBaseURL(String baseURL){ - accessURL = ((baseURL != null) ? (baseURL + "/") : "") + getName(); + public void setTAPBaseURL(final String baseURL){ + ; } @Override - public void init(ServletConfig config) throws ServletException{ + public void init(final ServletConfig config) throws ServletException{ ; } @@ -68,22 +90,27 @@ public class Sync implements TAPResource { } @Override - public boolean executeResource(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException, TAPException, UWSException{ - TAPParameters params = (TAPParameters)service.getFactory().createUWSParameters(request); + public boolean executeResource(final HttpServletRequest request, final HttpServletResponse response) throws IOException, TAPException{ + // Retrieve the execution parameters: + TAPParameters params = service.getFactory().createTAPParameters(request); params.check(); + // CASE 1: GET CAPABILITIES + /* If the user asks for the capabilities through the TAP parameters, execute the corresponding resource. */ if (params.getRequest().equalsIgnoreCase(TAPJob.REQUEST_GET_CAPABILITIES)) return capabilities.executeResource(request, response); - if (!service.isAvailable()){ - response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, service.getAvailability()); - return false; - } + // CASE 2: EXECUTE SYNCHRONOUSLY AN ADQL QUERY + // Ensure the service is currently available: + if (!service.isAvailable()) + throw new TAPException("Can not execute a query: this TAP service is not available! " + service.getAvailability(), UWSException.SERVICE_UNAVAILABLE); + // Execute synchronously the given job: TAPSyncJob syncJob = new TAPSyncJob(service, params); syncJob.start(response); return true; + } } diff --git a/src/tap/resource/TAP.java b/src/tap/resource/TAP.java index 3ad3a55..2bc876f 100644 --- a/src/tap/resource/TAP.java +++ b/src/tap/resource/TAP.java @@ -16,20 +16,11 @@ package tap.resource; * You should have received a copy of the GNU Lesser General Public License * along with TAPLibrary. If not, see . * - * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomisches Rechen Institute (ARI) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ -import java.io.BufferedInputStream; -import java.io.File; -import java.io.FileInputStream; import java.io.IOException; -import java.io.PrintWriter; -import java.net.MalformedURLException; -import java.net.URL; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.Map; @@ -42,39 +33,113 @@ import javax.servlet.http.HttpServletResponse; import tap.ServiceConnection; import tap.ServiceConnection.LimitUnit; import tap.TAPException; -import tap.db.DBConnection; import tap.error.DefaultTAPErrorWriter; import tap.formatter.OutputFormat; import tap.log.TAPLog; import tap.metadata.TAPMetadata; +import uk.ac.starlink.votable.VOSerializer; import uws.UWSException; -import uws.job.ErrorType; -import uws.job.UWSJob; +import uws.UWSToolBox; import uws.job.user.JobOwner; +import uws.service.UWS; import uws.service.UWSService; -import uws.service.UWSUrl; import uws.service.error.ServiceErrorWriter; +import uws.service.log.UWSLog.LogLevel; +import adql.db.FunctionDef; -public class TAP< R > implements VOSIResource { - - private static final long serialVersionUID = 1L; - - protected final ServiceConnection service; - +/** + *

Root/Home of the TAP service. It is also the resource (HOME) which gathers all the others of the same TAP service.

+ * + *

At its creation it is creating and configuring the other resources in function of the given description of the TAP service.

+ * + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (04/2015) + */ +public class TAP implements VOSIResource { + + /**

Name of the TAP AVAILABILITY resource. + * This resource tells whether the TAP service is available (i.e. whether it accepts queries or not).

+ *

Note: this name is suffixing the root TAP URL in order to access one of its resources.

+ * @since 2.0 */ + public final static String RESOURCE_AVAILABILITY = "availability"; + /**

Name of the TAP CAPABILITIES resource. + * This resource list all capabilities (e.g. output limits and formats, uploads, ...) of this TAP resource.

+ *

Note: this name is suffixing the root TAP URL in order to access one of its resources.

+ * @since 2.0 */ + public final static String RESOURCE_CAPABILITIES = "capabilities"; + /**

Name of the TAP HOME PAGE resource. + * This resource lists and describes all published and query-able schemas, tables and columns.

+ *

Note: this name is suffixing the root TAP URL in order to access one of its resources.

+ * @since 2.0 */ + public final static String RESOURCE_METADATA = "tables"; + /**

Name of the TAP HOME PAGE resource. + * This resource is used to submit ADQL queries to run asynchronously.

+ *

Note: this name is suffixing the root TAP URL in order to access one of its resources.

+ * @since 2.0 */ + public final static String RESOURCE_ASYNC = "async"; + /**

Name of the TAP HOME PAGE resource. + * This resource is used to submit ADQL queries to run synchronously.

+ *

Note: this name is suffixing the root TAP URL in order to access one of its resources.

+ * @since 2.0 */ + public final static String RESOURCE_SYNC = "sync"; + + /** Description of the TAP service owning this resource. */ + protected final ServiceConnection service; + + /** List of all the other TAP resources of the service. */ protected final Map resources; + /** Base URL of the TAP service. It is also the URL of this resource (HOME). */ protected String tapBaseURL = null; + /** + *

HOME PAGE resource. + * This resource lets write the home page.

+ *

Note: + * at the URI {@link #homePageURI} or it is a very simple HTML page listing the link of all available + * TAP resources. + *

+ * @since 2.0 + */ + protected HomePage homePage = null; + + /** URI of the page or path of the file to display when this resource is requested. */ protected String homePageURI = null; + /** MIME type of the custom home page. By default, it is "text/html". */ + protected String homePageMimeType = "text/html"; + + /** Object to use when an error occurs or comes until this resource from the others. + * This object fills the HTTP response in the most appropriate way in function of the error. */ protected ServiceErrorWriter errorWriter; - public TAP(ServiceConnection serviceConnection) throws UWSException, TAPException{ + /** Last generated request ID. If the next generated request ID is equivalent to this one, + * a new one will generate in order to ensure the uniqueness. + * @since 2.0 */ + protected static String lastRequestID = null; + + /** + * Build a HOME resource of a TAP service whose the description is given in parameter. + * All the other TAP resources will be created and configured here thanks to the given {@link ServiceConnection}. + * + * @param serviceConnection Description of the TAP service. + * + * @throws UWSException If an error occurs while creating the /async resource. + * @throws TAPException If any other error occurs. + */ + public TAP(final ServiceConnection serviceConnection) throws UWSException, TAPException{ service = serviceConnection; resources = new HashMap(); - errorWriter = new DefaultTAPErrorWriter(service); + // Get the error writer to use, or create a default instance if none are provided by the factory: + errorWriter = serviceConnection.getFactory().getErrorWriter(); + if (errorWriter == null) + errorWriter = new DefaultTAPErrorWriter(service); + + // Set the default home page: + homePage = new HomePage(this); + // Set all the standard TAP resources: TAPResource res = new Availability(service); resources.put(res.getName(), res); @@ -86,68 +151,316 @@ public class TAP< R > implements VOSIResource { res = new ASync(service); resources.put(res.getName(), res); - getUWS().setErrorWriter(errorWriter); - if (service.uploadEnabled()){ - DBConnection dbConn = null; - try{ - dbConn = service.getFactory().createDBConnection("TAP(ServiceConnection)"); - dbConn.dropSchema("TAP_UPLOAD"); - dbConn.createSchema("TAP_UPLOAD"); - }catch(TAPException e){ - throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, e, "Error while creating the schema TAP_UPLOAD !"); - }finally{ - if (dbConn != null) - dbConn.close(); - } - } - - updateTAPMetadata(); + TAPMetadata metadata = service.getTAPMetadata(); + resources.put(metadata.getName(), metadata); } + /** + * Get the logger used by this resource and all the other resources managed by it. + * + * @return The used logger. + */ public final TAPLog getLogger(){ return service.getLogger(); } - public void setTAPBaseURL(String baseURL){ + /** + *

Let initialize this resource and all the other managed resources.

+ * + *

This function is called by the library just once: when the servlet is initialized.

+ * + * @param config Configuration of the servlet. + * + * @throws ServletException If any error occurs while reading the given configuration. + * + * @see TAPResource#init(ServletConfig) + */ + public void init(final ServletConfig config) throws ServletException{ + for(TAPResource res : resources.values()) + res.init(config); + } + + /** + *

Free all the resources used by this resource and the other managed resources.

+ * + *

This function is called by the library just once: when the servlet is destroyed.

+ * + * @see TAPResource#destroy() + */ + public void destroy(){ + // Set the availability to "false" and the reason to "The application server is stopping!": + service.setAvailable(false, "The application server is stopping!"); + + // Destroy all web resources: + for(TAPResource res : resources.values()) + res.destroy(); + + // Destroy also all resources allocated in the factory: + service.getFactory().destroy(); + + // Log the end: + getLogger().logTAP(LogLevel.INFO, this, "STOP", "TAP Service stopped!", null); + } + + /** + *

Set the base URL of this TAP service.

+ * + *

+ * This URL must be the same as the one of this resource ; it corresponds to the + * URL of the root (or home) of the TAP service. + *

+ * + *

The given URL will be propagated to the other TAP resources automatically.

+ * + * @param baseURL URL of this resource. + * + * @see TAPResource#setTAPBaseURL(String) + */ + public void setTAPBaseURL(final String baseURL){ tapBaseURL = baseURL; for(TAPResource res : resources.values()) res.setTAPBaseURL(tapBaseURL); } - public void setTAPBaseURL(HttpServletRequest request){ + /** + *

Build the base URL from the given HTTP request, and use it to set the base URL of this TAP service.

+ * + *

The given URL will be propagated to the other TAP resources automatically.

+ * + * @param request HTTP request from which a TAP service's base URL will be extracted. + * + * @see #setTAPBaseURL(String) + */ + public void setTAPBaseURL(final HttpServletRequest request){ setTAPBaseURL(request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + request.getContextPath() + request.getServletPath()); } + /* ******************** */ + /* RESOURCES MANAGEMENT */ + /* ******************** */ + + /** + * Get the description of this service. + * + * @return Description/Configuration of this TAP service. + * + * @since 2.0 + */ + public final ServiceConnection getServiceConnection(){ + return service; + } + + /** + * Get the /availability resource of this TAP service. + * + * @return The /availability resource. + */ public final Availability getAvailability(){ - return (Availability)resources.get(Availability.RESOURCE_NAME); + return (Availability)resources.get(RESOURCE_AVAILABILITY); } + /** + * Get the /capabilities resource of this TAP service. + * + * @return The /capabilities resource. + */ public final Capabilities getCapabilities(){ - return (Capabilities)resources.get(Capabilities.RESOURCE_NAME); + return (Capabilities)resources.get(RESOURCE_CAPABILITIES); } + /** + * Get the /sync resource of this TAP service. + * + * @return The /sync resource. + */ public final Sync getSync(){ - return (Sync)resources.get(Sync.RESOURCE_NAME); + return (Sync)resources.get(RESOURCE_SYNC); } + /** + * Get the /async resource of this TAP service. + * + * @return The /async resource. + */ public final ASync getASync(){ - return (ASync)resources.get(ASync.RESOURCE_NAME); + return (ASync)resources.get(RESOURCE_ASYNC); + } + + /** + * Get the UWS service used for the /async service. + * + * @return The used UWS service. + */ + public final UWSService getUWS(){ + TAPResource res = getASync(); + if (res != null) + return ((ASync)res).getUWS(); + else + return null; } + /** + *

Get the object managing all the metadata (information about the published columns and tables) + * of this TAP service.

+ * + *

This object is also to the /tables resource.

+ * + * @return List of all metadata of this TAP service. + */ public final TAPMetadata getTAPMetadata(){ return (TAPMetadata)resources.get(TAPMetadata.RESOURCE_NAME); } - public final Iterator getTAPResources(){ + /** + *

Add the given resource in this TAP service.

+ * + *

The ID of this resource (which is also its URI) will be its name (given by {@link TAPResource#getName()}).

+ * + *

WARNING: + * If another resource with an ID strictly identical (case sensitively) to the name of the given resource, it will be overwritten! + * You should check (thanks to {@link #hasResource(String)}) before calling this function that no resource is associated with the same URI. + * If it is the case, you should then use the function {@link #addResource(String, TAPResource)} with a different ID/URI. + *

+ * + *

Note: + * This function is equivalent to {@link #addResource(String, TAPResource)} with {@link TAPResource#getName()} in first parameter. + *

+ * + * @param newResource Resource to add in the service. + * + * @return true if the given resource has been successfully added, + * false otherwise (and particularly if the given resource is NULL). + * + * @see #addResource(String, TAPResource) + */ + public final boolean addResource(final TAPResource newResource){ + return addResource(newResource.getName(), newResource); + } + + /** + *

Add the given resource in this TAP service with the given ID (which will be also the URI to access this resource).

+ * + *

WARNING: + * If another resource with an ID strictly identical (case sensitively) to the name of the given resource, it will be overwritten! + * You should check (thanks to {@link #hasResource(String)}) before calling this function that no resource is associated with the same URI. + * If it is the case, you should then use the function {@link #addResource(String, TAPResource)} with a different ID/URI. + *

+ * + *

Note: + * If the given ID is NULL, the name of the resource will be used. + *

+ * + * @param resourceId ID/URI of the resource to add. + * @param newResource Resource to add. + * + * @return true if the given resource has been successfully added to this service with the given ID/URI, + * false otherwise (and particularly if the given resource is NULL). + */ + public final boolean addResource(final String resourceId, final TAPResource newResource){ + if (newResource == null) + return false; + resources.put((resourceId == null) ? newResource.getName() : resourceId, newResource); + return true; + } + + /** + * Get the number of all resources managed by this TAP service (this resource - HOME - excluded). + * + * @return Number of managed resources. + */ + public final int getNbResources(){ + return resources.size(); + } + + /** + *

Get the specified resource.

+ * + *

Note: + * The research is case sensitive. + *

+ * + * @param resourceId Exact ID/URI of the resource to get. + * + * @return The corresponding resource, + * or NULL if no match can be found. + */ + public final TAPResource getResource(final String resourceId){ + return resources.get(resourceId); + } + + /** + * Let iterate over the full list of the TAP resources managed by this TAP service. + * + * @return Iterator over the available TAP resources. + */ + public final Iterator getResources(){ return resources.values().iterator(); } + /** + * Let iterate over the full list of the TAP resources managed by this TAP service. + * + * @return Iterator over the available TAP resources. + * @deprecated The name of this function has been normalized. So now, you should use {@link #getResources()} + * which is doing exactly the same thing. + */ + @Deprecated + public final Iterator getTAPResources(){ + return getResources(); + } + + /** + *

Tell whether a resource is already associated with the given ID/URI.

+ * + *

Note: + * The research is case sensitive. + *

+ * + * @param resourceId Exact ID/URI of the resource to find. + * + * @return true if a resource is already associated with the given ID/URI, + * false otherwise. + */ + public final boolean hasResource(final String resourceId){ + return resources.containsKey(resourceId); + } + + /** + *

Remove the resource associated with the given ID/URI.

+ * + *

Note: + * The research is case sensitive. + *

+ * + * @param resourceId Exact ID/URI of the resource to remove. + * + * @return The removed resource, if associated with the given ID/URI, + * otherwise, NULL is returned. + */ + public final TAPResource removeResource(final String resourceId){ + return resources.remove(resourceId); + } + + /* **************** */ + /* ERROR MANAGEMENT */ + /* **************** */ + + /** + * Get the object to use in order to report errors to the user in replacement of the expected result. + * + * @return Used error writer. + */ public final ServiceErrorWriter getErrorWriter(){ return errorWriter; } - public final void setErrorWriter(ServiceErrorWriter errorWriter){ + /** + * Set the object to use in order to report errors to the user in replacement of the expected result. + * + * @param errorWriter Error writer to use. (if NULL, nothing will be done) + */ + public final void setErrorWriter(final ServiceErrorWriter errorWriter){ if (errorWriter != null){ this.errorWriter = errorWriter; getUWS().setErrorWriter(errorWriter); @@ -168,29 +481,72 @@ public class TAP< R > implements VOSIResource { public String getCapability(){ StringBuffer xml = new StringBuffer(); - xml.append("\n"); + // Header: + xml.append("\n"); + + // TAP access: xml.append("\t\n"); - xml.append("\t\t").append(getAccessURL()).append("\n"); + xml.append("\t\t").append((getAccessURL() == null) ? "" : VOSerializer.formatText(getAccessURL())).append("\n"); xml.append("\t\n"); + + // Language description: xml.append("\t\n"); xml.append("\t\tADQL\n"); - xml.append("\t\t2.0\n"); + xml.append("\t\t2.0\n"); xml.append("\t\tADQL 2.0\n"); + + // Geometrical functions: + if (service.getGeometries() != null && service.getGeometries().size() > 0){ + xml.append("\t\t"); + for(String geom : service.getGeometries()){ + if (geom != null){ + xml.append("\t\t\t"); + xml.append("\t\t\t\t
").append(VOSerializer.formatText(geom.toUpperCase())).append("
"); + xml.append("\t\t\t
"); + } + } + xml.append("\t\t
"); + } + + // User Defined Functions (UDFs): + if (service.getUDFs() != null && service.getUDFs().size() > 0){ + xml.append("\t\t"); + for(FunctionDef udf : service.getUDFs()){ + if (udf != null){ + xml.append("\t\t\t"); + xml.append("\t\t\t\t
").append(VOSerializer.formatText(udf.toString())).append("
"); + if (udf.description != null && udf.description.length() > 0) + xml.append("\t\t\t\t").append(VOSerializer.formatText(udf.description)).append(""); + xml.append("\t\t\t
"); + } + } + xml.append("\t\t
"); + } + xml.append("\t
\n"); - Iterator> itFormats = service.getOutputFormats(); - OutputFormat formatter; + // Available output formats: + Iterator itFormats = service.getOutputFormats(); + OutputFormat formatter; while(itFormats.hasNext()){ formatter = itFormats.next(); xml.append("\t\n"); - xml.append("\t\t").append(formatter.getMimeType()).append("\n"); + xml.append("\t\t").append(VOSerializer.formatText(formatter.getMimeType())).append("\n"); if (formatter.getShortMimeType() != null) - xml.append("\t\t").append(formatter.getShortMimeType()).append("\n"); + xml.append("\t\t").append(VOSerializer.formatText(formatter.getShortMimeType())).append("\n"); if (formatter.getDescription() != null) - xml.append("\t\t").append(formatter.getDescription()).append("\n"); + xml.append("\t\t").append(VOSerializer.formatText(formatter.getDescription())).append("\n"); xml.append("\t\n"); } + // Write upload methods: INLINE, HTTP, FTP: + if (service.uploadEnabled()){ + xml.append("\t\n"); + xml.append("\t\n"); + xml.append("\t\n"); + } + + // Retention period (for asynchronous jobs): int[] retentionPeriod = service.getRetentionPeriod(); if (retentionPeriod != null && retentionPeriod.length >= 2){ if (retentionPeriod[0] > -1 || retentionPeriod[1] > -1){ @@ -203,6 +559,7 @@ public class TAP< R > implements VOSIResource { } } + // Execution duration (still for asynchronous jobs): int[] executionDuration = service.getExecutionDuration(); if (executionDuration != null && executionDuration.length >= 2){ if (executionDuration[0] > -1 || executionDuration[1] > -1){ @@ -215,238 +572,334 @@ public class TAP< R > implements VOSIResource { } } + // Output/Result limit: int[] outputLimit = service.getOutputLimit(); LimitUnit[] outputLimitType = service.getOutputLimitType(); if (outputLimit != null && outputLimit.length >= 2 && outputLimitType != null && outputLimitType.length >= 2){ if (outputLimit[0] > -1 || outputLimit[1] > -1){ xml.append("\t\n"); - if (outputLimit[0] > -1) - xml.append("\t\t").append(outputLimit[0]).append("\n"); - if (outputLimit[1] > -1) - xml.append("\t\t").append(outputLimit[1]).append("\n"); + String limitType; + if (outputLimit[0] > -1){ + long limit = outputLimit[0] * outputLimitType[0].bytesFactor(); + limitType = (outputLimitType[0] == null || outputLimitType[0] == LimitUnit.rows) ? LimitUnit.rows.toString() : LimitUnit.bytes.toString(); + xml.append("\t\t").append(limit).append("\n"); + } + if (outputLimit[1] > -1){ + long limit = outputLimit[1] * outputLimitType[1].bytesFactor(); + limitType = (outputLimitType[1] == null || outputLimitType[1] == LimitUnit.rows) ? LimitUnit.rows.toString() : LimitUnit.bytes.toString(); + xml.append("\t\t").append(limit).append("\n"); + } xml.append("\t\n"); } } + // Upload limits if (service.uploadEnabled()){ - // Write upload methods: INLINE, HTTP, FTP: - xml.append(""); - xml.append(""); - xml.append(""); - xml.append(""); - xml.append(""); - xml.append(""); - // Write upload limits: int[] uploadLimit = service.getUploadLimit(); LimitUnit[] uploadLimitType = service.getUploadLimitType(); if (uploadLimit != null && uploadLimit.length >= 2 && uploadLimitType != null && uploadLimitType.length >= 2){ if (uploadLimit[0] > -1 || uploadLimit[1] > -1){ xml.append("\t\n"); - if (uploadLimit[0] > -1) - xml.append("\t\t").append(uploadLimit[0]).append("\n"); - if (uploadLimit[1] > -1) - xml.append("\t\t").append(uploadLimit[1]).append("\n"); + String limitType; + if (uploadLimit[0] > -1){ + long limit = uploadLimit[0] * uploadLimitType[0].bytesFactor(); + limitType = (uploadLimitType[0] == null || uploadLimitType[0] == LimitUnit.rows) ? LimitUnit.rows.toString() : LimitUnit.bytes.toString(); + xml.append("\t\t").append(limit).append("\n"); + } + if (uploadLimit[1] > -1){ + long limit = uploadLimit[1] * uploadLimitType[1].bytesFactor(); + limitType = (uploadLimitType[1] == null || uploadLimitType[1] == LimitUnit.rows) ? LimitUnit.rows.toString() : LimitUnit.bytes.toString(); + xml.append("\t\t").append(limit).append("\n"); + } xml.append("\t\n"); } } } + // Footer: xml.append("\t
"); return xml.toString(); } - public final UWSService getUWS(){ - TAPResource res = resources.get("async"); - if (res != null) - return ((ASync)res).getUWS(); - else - return null; + /* ************************************* */ + /* MANAGEMENT OF THIS RESOURCE'S CONTENT */ + /* ************************************* */ + + /** + * Get the HOME PAGE resource of this TAP service. + * + * @return The HOME PAGE resource. + * + * @since 2.0 + */ + public final HomePage getHomePage(){ + return homePage; } /** - * @return The homePageURI. + *

Change the whole behavior of the TAP home page.

+ * + *

Note: + * If the given resource is NULL, the default home page (i.e. {@link HomePage}) is set. + *

+ * + * @param newHomePageResource The new HOME PAGE resource for this TAP service. + * + * @since 2.0 + */ + public final void setHomePage(final HomePage newHomePageResource){ + if (newHomePageResource == null){ + if (homePage == null || !(homePage instanceof HomePage)) + homePage = new HomePage(this); + }else + homePage = newHomePageResource; + } + + /** + *

Get the URL or the file path of a custom home page.

+ * + *

The home page will be displayed when this resource is directly requested.

+ * + *

Note: + * This function has a sense only if the HOME PAGE resource of this TAP service + * is still the default home page (i.e. {@link HomePage}). + *

+ * + * @return URL or file path of the file to display as home page, + * or NULL if no custom home page has been specified. */ public final String getHomePageURI(){ return homePageURI; } - public final void setHomePageURI(String uri){ + /** + *

Set the URL or the file path of a custom home page.

+ * + *

The home page will be displayed when this resource is directly requested.

+ * + *

Note: + * This function has a sense only if the HOME PAGE resource of this TAP service + * is still the default home page (i.e. {@link HomePage}). + *

+ * + * @param uri URL or file path of the file to display as home page, or NULL to display the default home page. + */ + public final void setHomePageURI(final String uri){ homePageURI = (uri != null) ? uri.trim() : uri; if (homePageURI != null && homePageURI.length() == 0) homePageURI = null; } - public void init(ServletConfig config) throws ServletException{ - for(TAPResource res : resources.values()) - res.init(config); + /** + *

Get the MIME type of the custom home page.

+ * + *

By default, it is the same as the default home page: "text/html".

+ * + *

Note: + * This function has a sense only if the HOME PAGE resource of this TAP service + * is still the default home page (i.e. {@link HomePage}). + *

+ * + * @return MIME type of the custom home page. + */ + public final String getHomePageMimeType(){ + return homePageMimeType; } - public void destroy(){ - for(TAPResource res : resources.values()) - res.destroy(); + /** + *

Set the MIME type of the custom home page.

+ * + *

A NULL value will be considered as "text/html".

+ * + *

Note: + * This function has a sense only if the HOME PAGE resource of this TAP service + * is still the default home page (i.e. {@link HomePage}). + *

+ * + * @param mime MIME type of the custom home page. + */ + public final void setHomePageMimeType(final String mime){ + homePageMimeType = (mime == null || mime.trim().length() == 0) ? "text/html" : mime.trim(); } - public void executeRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{ - response.setContentType("text/plain"); + /** + *

Generate a unique ID for the given request.

+ * + *

By default, a timestamp is returned.

+ * + * @param request Request whose an ID is asked. + * + * @return The ID of the given request. + * + * @since 2.0 + */ + protected synchronized String generateRequestID(final HttpServletRequest request){ + String id; + do{ + id = System.currentTimeMillis() + ""; + }while(lastRequestID != null && lastRequestID.startsWith(id)); + lastRequestID = id; + return id; + } - if (tapBaseURL == null) - setTAPBaseURL(request); + /** + *

Execute the given request in the TAP service by forwarding it to the appropriate resource.

+ * + *

Home page

+ *

+ * If the appropriate resource is the home page, the request is propagated to a {@link TAPResource} + * (by default {@link HomePage}) whose the resource name is "HOME PAGE". Once called, this resource + * displays directly the home page in the given response by calling. + * The default implementation of the default implementation ({@link HomePage}) takes several cases into account. + * Those are well documented in the Javadoc of {@link HomePage}. What you should know, is that sometimes it is + * using the following attributes of this class: {@link #getHomePage()}, {@link #getHomePageURI()}, {@link #getHomePageMimeType()}. + *

+ * + *

Error/Exception management

+ *

+ * Only this resource (the root) should write any errors in the response. For that, it catches any {@link Throwable} and + * write an appropriate message in the HTTP response. The format and the content of this message is designed by the {@link ServiceErrorWriter} + * set in this class. By changing it, it is then possible to change, for instance, the format of the error responses. + *

+ * + *

Request ID & Log

+ *

+ * Each request is identified by a unique identifier (see {@link #generateRequestID(HttpServletRequest)}). + * This ID is used only for logging purpose. Request and jobs/threads can then be associated more easily in the logs. + * Besides, every requests and their response are logged as INFO with this ID. + *

+ * + * @param request Request of the user to execute in this TAP service. + * @param response Object in which the result of the request must be written. + * + * @throws ServletException If any grave/fatal error occurs. + * @throws IOException If any error occurs while reading or writing from or into a stream (and particularly the given request or response). + */ + public void executeRequest(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException{ + if (request == null || response == null) + return; + + // Generate a unique ID for this request execution (for log purpose only): + final String reqID = generateRequestID(request); + if (request.getAttribute(UWS.REQ_ATTRIBUTE_ID) == null) + request.setAttribute(UWS.REQ_ATTRIBUTE_ID, reqID); - JobOwner owner = null; - String resourceName = null; + // Extract all parameters: + if (request.getAttribute(UWS.REQ_ATTRIBUTE_PARAMETERS) == null){ + try{ + request.setAttribute(UWS.REQ_ATTRIBUTE_PARAMETERS, getUWS().getRequestParser().parse(request)); + }catch(UWSException ue){ + getLogger().log(LogLevel.WARNING, "REQUEST_PARSER", "Can not extract the HTTP request parameters!", ue); + } + } + // Retrieve the resource path parts: + String[] resourcePath = (request.getPathInfo() == null) ? null : request.getPathInfo().split("/"); + String resourceName = (resourcePath == null || resourcePath.length < 1) ? "" : resourcePath[1].trim(); + + // Log the reception of the request, only if the asked resource is not UWS (because UWS is already logging the received request): + if (!resourceName.equalsIgnoreCase(ASync.RESOURCE_NAME)) + getLogger().logHttp(LogLevel.INFO, request, reqID, null, null); + + // Initialize the base URL of this TAP service by guessing it from the received request: + if (tapBaseURL == null){ + // initialize the base URL: + setTAPBaseURL(request); + // log the successful initialization: + getLogger().logUWS(LogLevel.INFO, this, "INIT", "TAP successfully initialized (" + tapBaseURL + ").", null); + } + + JobOwner user = null; try{ // Identify the user: - if (service.getUserIdentifier() != null) - owner = service.getUserIdentifier().extractUserId(new UWSUrl(request), request); - - String[] resourcePath = (request.getPathInfo() == null) ? null : request.getPathInfo().split("/"); - // Display the TAP Main Page: - if (resourcePath == null || resourcePath.length < 1){ - resourceName = "homePage"; - response.setContentType("text/html"); - writeHomePage(response.getWriter(), owner); + try{ + user = UWSToolBox.getUser(request, service.getUserIdentifier()); + }catch(UWSException ue){ + getLogger().logTAP(LogLevel.ERROR, null, "IDENT_USER", "Can not identify the HTTP request user!", ue); + throw new TAPException(ue); + } + + // Set the character encoding: + response.setCharacterEncoding(UWSToolBox.DEFAULT_CHAR_ENCODING); + + // Display the TAP Home Page: + if (resourceName.length() == 0){ + resourceName = homePage.getName(); + homePage.executeResource(request, response); } // or Display/Execute the selected TAP Resource: else{ - resourceName = resourcePath[1].trim().toLowerCase(); + // search for the corresponding resource: TAPResource res = resources.get(resourceName); + // if one is found, execute it: if (res != null) res.executeResource(request, response); + // otherwise, throw an error: else - errorWriter.writeError("This TAP service does not have a resource named \"" + resourceName + "\" !", ErrorType.TRANSIENT, HttpServletResponse.SC_NOT_FOUND, response, request, null, "Get a TAP resource"); + throw new TAPException("Unknown TAP resource: \"" + resourceName + "\"!", UWSException.NOT_IMPLEMENTED); } - service.getLogger().httpRequest(request, owner, resourceName, HttpServletResponse.SC_OK, "[OK]", null); - response.flushBuffer(); - }catch(IOException ioe){ - errorWriter.writeError(ioe, response, request, owner, (resourceName == null) ? "Writing the TAP home page" : ("Executing the TAP resource " + resourceName)); - }catch(UWSException ue){ - errorWriter.writeError(ue, response, request, owner, (resourceName == null) ? "Writing the TAP home page" : ("Executing the TAP resource " + resourceName)); - }catch(TAPException te){ - writeError(te, response); - }catch(Throwable t){ - errorWriter.writeError(t, response, request, owner, (resourceName == null) ? "Writing the TAP home page" : ("Executing the TAP resource " + resourceName)); - } - } - - public void writeHomePage(final PrintWriter writer, final JobOwner owner) throws IOException{ - // By default, list all available resources: - if (homePageURI == null){ - writer.println("TAP HOME PAGE

TAP HOME PAGE

Available resources:

    "); - for(TAPResource res : resources.values()) - writer.println("
  • " + res.getName() + "
  • "); - writer.println("
"); - } - // or Display the specified home page: - else{ - BufferedInputStream input = null; - try{ - input = new BufferedInputStream((new URL(homePageURI)).openStream()); - }catch(MalformedURLException mue){ - input = new BufferedInputStream(new FileInputStream(new File(homePageURI))); - } - if (input == null) - throw new IOException("Incorrect TAP home page URI !"); - byte[] buffer = new byte[255]; - int nbReads = 0; - while((nbReads = input.read(buffer)) > 0) - writer.print(new String(buffer, 0, nbReads)); - } - } - public void writeError(TAPException ex, HttpServletResponse response) throws ServletException, IOException{ - service.getLogger().error(ex); - response.reset(); - response.setStatus(ex.getHttpErrorCode()); - response.setContentType("text/xml"); - writeError(ex, response.getWriter()); - } - - protected void writeError(TAPException ex, PrintWriter output) throws ServletException, IOException{ - output.println(""); - output.println(""); - output.println("\t"); - - // Print the error: - output.println("\t\t"); - output.print("\t\t\t\t\t"); - - // Print the current date: - DateFormat dateFormat = new SimpleDateFormat(UWSJob.DEFAULT_DATE_FORMAT); - output.print("\t\t"); - - // Print the provider (if any): - if (service.getProviderName() != null){ - output.print("\t\t\n\t\t\t\n\t\t"); - }else - output.println("\" />"); - } - - // Print the query (if any): - if (ex.getQuery() != null){ - output.print("\t\t\n\t\t\t\t\t"); - } - - output.println("\t"); - output.println(""); - - output.flush(); - } + // Log the successful execution of the action, only if the asked resource is not UWS (because UWS is already logging the received request): + if (!resourceName.equalsIgnoreCase(ASync.RESOURCE_NAME)) + getLogger().logHttp(LogLevel.INFO, response, reqID, user, "Action \"" + resourceName + "\" successfully executed.", null); - public final boolean addResource(TAPResource newResource){ - if (newResource == null) - return false; - resources.put(newResource.getName(), newResource); - return true; - } - - public final boolean addResource(String resourceId, TAPResource newResource){ - if (newResource == null) - return false; - resources.put((resourceId == null) ? newResource.getName() : resourceId, newResource); - return true; - } - - public final int getNbResources(){ - return resources.size(); - } - - public final TAPResource getResource(String resourceId){ - return resources.get(resourceId); - } - - public final boolean hasResource(String resourceId){ - return resources.containsKey(resourceId); - } + }catch(IOException ioe){ + /* + * Any IOException thrown while writing the HTTP response is generally caused by a client abortion (intentional or timeout) + * or by a connection closed with the client for another reason. + * Consequently, a such error should not be considered as a real error from the server or the library: the request is + * canceled, and so the response is not expected. It is anyway not possible any more to send it (header and/or body) totally + * or partially. + * Nothing can solve this error. So the "error" is just reported as a simple information and theoretically the action + * executed when this error has been thrown is already stopped. + */ + getLogger().logHttp(LogLevel.INFO, response, reqID, user, "HTTP request aborted or connection with the client closed => the TAP resource \"" + resourceName + "\" has stopped and the body of the HTTP response can not have been partially or completely written!", null); - public final TAPResource removeResource(String resourceId){ - return resources.remove(resourceId); - } + }catch(TAPException te){ + /* + * Any known/"expected" TAP exception is logged but also returned to the HTTP client in an XML error document. + * Since the error is known, it is supposed to have already been logged with a full stack trace. Thus, there + * is no need to log again its stack trace...just its message is logged. + */ + // Write the error in the response and return the appropriate HTTP status code: + errorWriter.writeError(te, response, request, reqID, user, resourceName); + // Log the error: + getLogger().logHttp(LogLevel.ERROR, response, reqID, user, "TAP resource \"" + resourceName + "\" execution FAILED with the error: \"" + te.getMessage() + "\"!", null); + + }catch(IllegalStateException ise){ + /* + * Any IllegalStateException that reaches this point, is supposed coming from a HttpServletResponse operation which + * has to reset the response buffer (e.g. resetBuffer(), sendRedirect(), sendError()). + * If this exception happens, the library tried to rewrite the HTTP response body with a message or a result, + * while this body has already been partially sent to the client. It is then no longer possible to change its content. + * Consequently, the error is logged as FATAL and a message will be appended at the end of the already submitted response + * to alert the HTTP client that an error occurs and the response should not be considered as complete and reliable. + */ + // Write the error in the response and return the appropriate HTTP status code: + errorWriter.writeError(ise, response, request, reqID, user, resourceName); + // Log the error: + getLogger().logHttp(LogLevel.FATAL, response, reqID, user, "HTTP response already partially committed => the TAP resource \"" + resourceName + "\" has stopped and the body of the HTTP response can not have been partially or completely written!", (ise.getCause() != null) ? ise.getCause() : ise); - public boolean updateTAPMetadata(){ - TAPMetadata metadata = service.getTAPMetadata(); - if (metadata != null){ - resources.put(metadata.getName(), metadata); - return true; + }catch(Throwable t){ + /* + * Any other error is considered as unexpected if it reaches this point. Consequently, it has not yet been logged. + * So its stack trace will be fully logged, and an appropriate message will be returned to the HTTP client. The + * returned XML document should contain not too technical information which would be useless for the user. + */ + // Write the error in the response and return the appropriate HTTP status code: + errorWriter.writeError(t, response, request, reqID, user, resourceName); + // Log the error: + getLogger().logHttp(LogLevel.FATAL, response, reqID, user, "TAP resource \"" + resourceName + "\" execution FAILED with a GRAVE error!", t); + + }finally{ + // Notify the queue of the asynchronous jobs that a new connection may be available: + if (resourceName.equalsIgnoreCase(Sync.RESOURCE_NAME)) + getASync().freeConnectionAvailable(); } - return false; } } diff --git a/src/tap/resource/TAPResource.java b/src/tap/resource/TAPResource.java index 4a830c9..37938da 100644 --- a/src/tap/resource/TAPResource.java +++ b/src/tap/resource/TAPResource.java @@ -16,7 +16,8 @@ package tap.resource; * You should have received a copy of the GNU Lesser General Public License * along with TAPLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.io.IOException; @@ -27,18 +28,77 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import tap.TAPException; -import uws.UWSException; +/** + *

List the common functions that a TAP resource must have. + * Basically, any TAP resource may be initialized, may be destroyed, must have a name and must execute a request provided by its TAP service.

+ * + *

Important note: + * It is strongly recommended that the name of the TAP resource is also provided through a public static attribute named "RESOURCE_NAME". + * If this attribute exists, its value must be the same as the one returned by {@link #getName()}. + *

+ * + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (09/2014) + */ public interface TAPResource { + /** + * Let initialize this TAP resource. + * + * @param config Servlet configuration. (may be useful for the configuration of this resource) + * + * @throws ServletException If any error prevent the initialization of this TAP resource. In case a such exception is thrown, the service should stop immediately. + */ public void init(ServletConfig config) throws ServletException; + /** + * Let free properly all system/file/DB resources kept opened by this TAP resource. + */ public void destroy(); + /** + *

Let diffuse the base URL of the TAP service to all its TAP resources.

+ * + *

Important note: + * This function should be called just once: either at the creation of the service or when the first request is sent to the TAP service + * (in this case, the request is also used to finish the initialization of the TAP service, and of all its resources). + *

+ * + * @param baseURL Common URL/URI used in all requests sent by any user to the TAP service. + */ public void setTAPBaseURL(String baseURL); + /** + *

Get the name of this TAP resource.

+ * + *

Important note: + * This name MUST NOT be NULL and SHOULD NEVER change. + *

+ * + * @return Name of this TAP resource. + */ public String getName(); - public boolean executeResource(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException, TAPException, UWSException; + /** + *

Interpret the given request, execute the appropriate action and finally return a result or display information to the user.

+ * + *

IMPORTANT: + * "TAP resources can not take the law in their own hands!" :-) + * Errors that could occur inside this function should not be written directly in the given {@link HttpServletResponse}. + * They should be thrown to the resources executor: an instance of {@link TAP}, which + * will fill the {@link HttpServletResponse} with the error in the format described by the IVOA standard - VOTable. Besides, {@link TAP} may also + * add more information and may log the error (in function of this type). + *

+ * + * @param request Request sent by the user and which should be interpreted/executed here. + * @param response Response in which the result of the request must be written. + * + * @return true if the request has been successfully executed, false otherwise (but generally an exception will be sent if the request can't be executed). + * + * @throws IOException If any error occurs while writing the result of the given request. + * @throws TAPException If any other error occurs while interpreting and executing the request or by formating and writing its result. + */ + public boolean executeResource(HttpServletRequest request, HttpServletResponse response) throws IOException, TAPException; } diff --git a/src/tap/resource/VOSIResource.java b/src/tap/resource/VOSIResource.java index 758c8ad..216b55b 100644 --- a/src/tap/resource/VOSIResource.java +++ b/src/tap/resource/VOSIResource.java @@ -16,15 +16,70 @@ package tap.resource; * You should have received a copy of the GNU Lesser General Public License * along with TAPLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ +/** + *

VOSI - VO Support Interfaces - lets describe a minimal interface that VO web services should provide.

+ * + *

+ * This interface aims to give information about the capabilities, the availability and the reliability of the service. + * To reach this goal the 3 following endpoints (resources) should be provided: + *

+ *
    + *
  1. Capability metadata: list all available resources, give their access URL and a standard ID (helping to identify the type of resource). + * More information related to the service itself (or about the VO standard it is implementing) may be provided.
  2. + * + *
  3. Availability metadata: indicate whether the service is available or not. It may also provide a note and some other information about + * its reliability, such as the date at which it is up, or since when it is down and when it will be back.
  4. + * + *
  5. Tables metadata: since some VO services deal with tabular data (in output, in input or queriable by a language like ADQL), + * a VOSI-compliant service shall provide a list and a description of them.
  6. + *
+ * + *

+ * Implementing the VOSI interface means that each service endpoint/resource must be described in the capability endpoint with an access URL and a standard VO ID. + *

+ * + *

The standard IDs of the VOSI endpoints are the following:

+ *
    + *
  • Capabilities: ivo://ivoa.net/std/VOSI#capabilities
  • + *
  • Availability: ivo://ivoa.net/std/VOSI#availability
  • + *
  • Tables: ivo://ivoa.net/std/VOSI#tables
  • + *
+ * + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (09/2014) + */ public interface VOSIResource { + /** + * Get the capabilities of this resource. + * + * @return Resource capabilities. + */ public String getCapability(); + /** + * Get the URL which lets access this resource. + * + * @return Access URL. + */ public String getAccessURL(); + /** + *

Get the standardID of this endpoint of the VOSI interface.

+ * + *

The standard IDs of the VOSI endpoints are the following:

+ *
    + *
  • Capabilities: ivo://ivoa.net/std/VOSI#capabilities
  • + *
  • Availability: ivo://ivoa.net/std/VOSI#availability
  • + *
  • Tables: ivo://ivoa.net/std/VOSI#tables
  • + *
+ * + * @return Standard ID of this VOSI endpoint. + */ public String getStandardID(); } \ No newline at end of file diff --git a/src/tap/upload/LimitedSizeInputStream.java b/src/tap/upload/LimitedSizeInputStream.java index dd95527..60acbe8 100644 --- a/src/tap/upload/LimitedSizeInputStream.java +++ b/src/tap/upload/LimitedSizeInputStream.java @@ -16,7 +16,8 @@ package tap.upload; * You should have received a copy of the GNU Lesser General Public License * along with TAPLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.io.IOException; @@ -25,15 +26,34 @@ import java.security.InvalidParameterException; import com.oreilly.servlet.multipart.ExceededSizeException; +/** + * Let limit the number of bytes that can be read from a given input stream. + * + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (08/2014) + */ public final class LimitedSizeInputStream extends InputStream { + /** Input stream whose the number of bytes that can be read must be limited. */ private final InputStream input; + /** Maximum number of bytes that can be read. */ private final long sizeLimit; + /** Number of bytes currently read. */ private long counter = 0; + /** Indicate whether the byte limit has already been reached. If true no more byte can be read ; + * all read(...) function will throw an {@link ExceededSizeException}. */ private boolean exceed = false; + /** + * Wrap the given input stream so that limiting the number of bytes that can be read. + * + * @param stream Stream to limit. + * @param sizeLimit Maximum number of bytes that can be read. If <=0 an {@link InvalidParameterException} will be thrown. + * + * @throws NullPointerException If the input stream is missing. + */ public LimitedSizeInputStream(final InputStream stream, final long sizeLimit) throws NullPointerException{ if (stream == null) throw new NullPointerException("The given input stream is NULL !"); @@ -44,16 +64,45 @@ public final class LimitedSizeInputStream extends InputStream { this.sizeLimit = sizeLimit; } + /** + * Get the input stream wrapped by this instance of {@link LimitedSizeInputStream}. + * + * @return The wrapped input stream. + * @since 2.0 + */ + public final InputStream getInnerStream(){ + return input; + } + + /** + *

Update the number of bytes currently read and them check whether the limit has been exceeded. + * If the limit has been exceeded, an {@link ExceededSizeException} is thrown.

+ * + *

Besides, the flag {@link #exceed} is set to true in order to forbid the further reading of bytes.

+ * + * @param nbReads Number of bytes read. + * + * @throws ExceededSizeException If, after update, the limit of bytes has been exceeded. + */ private void updateCounter(final long nbReads) throws ExceededSizeException{ if (nbReads > 0){ counter += nbReads; if (counter > sizeLimit){ exceed = true; - throw new ExceededSizeException(); + throw new ExceededSizeException("Data read overflow: the limit of " + sizeLimit + " bytes has been reached!"); } } } + /** + *

Tell whether the limit has already been exceeded or not.

+ * + *

Note: + * If true is returned, no more read will be allowed, and any attempt to read a byte will throw an {@link ExceededSizeException}. + *

+ * + * @return true if the byte limit has been exceeded, false otherwise. + */ public final boolean sizeExceeded(){ return exceed; } @@ -98,17 +147,17 @@ public final class LimitedSizeInputStream extends InputStream { @Override public synchronized void mark(int readlimit) throws UnsupportedOperationException{ - throw new UnsupportedOperationException("mark() not supported in a LimitedSizeInputStream !"); + input.mark(readlimit); } @Override public boolean markSupported(){ - return false; + return input.markSupported(); } @Override public synchronized void reset() throws IOException, UnsupportedOperationException{ - throw new UnsupportedOperationException("mark() not supported in a LimitedSizeInputStream !"); + input.reset(); } } diff --git a/src/tap/upload/TableLoader.java b/src/tap/upload/TableLoader.java deleted file mode 100644 index e8b0502..0000000 --- a/src/tap/upload/TableLoader.java +++ /dev/null @@ -1,104 +0,0 @@ -package tap.upload; - -/* - * This file is part of TAPLibrary. - * - * TAPLibrary is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * TAPLibrary is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with TAPLibrary. If not, see . - * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) - */ - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; - -import java.net.MalformedURLException; -import java.net.URL; - -import java.security.InvalidParameterException; -import java.util.Enumeration; - -import com.oreilly.servlet.MultipartRequest; - -public class TableLoader { - private static final String URL_REGEXP = "^(https?|ftp)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]"; - private static final String PARAM_PREFIX = "param:"; - - public final String tableName; - private final URL url; - private final String param; - - private final File file; - - public TableLoader(final String name, final String value){ - this(name, value, (MultipartRequest)null); - } - - @SuppressWarnings("unchecked") - public TableLoader(final String name, final String uri, final MultipartRequest multipart){ - if (name == null || name.trim().isEmpty()) - throw new NullPointerException("A table name can not be NULL !"); - tableName = name.trim(); - - if (uri == null || uri.trim().isEmpty()) - throw new NullPointerException("The table URI can not be NULL !"); - String URI = uri.trim(); - if (URI.startsWith(PARAM_PREFIX)){ - if (multipart == null) - throw new InvalidParameterException("The URI scheme \"param\" can be used ONLY IF the VOTable is provided inside the HTTP request (multipart/form-data) !"); - else if (URI.length() <= PARAM_PREFIX.length()) - throw new InvalidParameterException("Incomplete URI (" + URI + "): empty parameter name !"); - url = null; - param = URI.substring(PARAM_PREFIX.length()).trim(); - - Enumeration enumeration = multipart.getFileNames(); - File foundFile = null; - while(foundFile == null && enumeration.hasMoreElements()){ - String fileName = enumeration.nextElement(); - if (fileName.equals(param)) - foundFile = multipart.getFile(fileName); - } - - if (foundFile == null) - throw new InvalidParameterException("Incorrect file reference (" + URI + "): the parameter \"" + param + "\" does not exist !"); - else - file = foundFile; - }else if (URI.matches(URL_REGEXP)){ - try{ - url = new URL(URI); - param = null; - file = null; - }catch(MalformedURLException mue){ - throw new InvalidParameterException(mue.getMessage()); - } - }else - throw new InvalidParameterException("Invalid table URI: \"" + URI + "\" !"); - } - - public InputStream openStream() throws IOException{ - if (url != null) - return url.openStream(); - else - return new FileInputStream(file); - } - - public boolean deleteFile(){ - if (file != null && file.exists()) - return file.delete(); - else - return false; - } - -} diff --git a/src/tap/upload/Uploader.java b/src/tap/upload/Uploader.java index 651a198..27f7d81 100644 --- a/src/tap/upload/Uploader.java +++ b/src/tap/upload/Uploader.java @@ -16,8 +16,8 @@ package tap.upload; * You should have received a copy of the GNU Lesser General Public License * along with TAPLibrary. If not, see . * - * Copyright 2012-2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomisches Rechen Institute (ARI) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.io.IOException; @@ -26,119 +26,173 @@ import java.io.InputStream; import tap.ServiceConnection; import tap.ServiceConnection.LimitUnit; import tap.TAPException; +import tap.data.DataReadException; +import tap.data.LimitedTableIterator; +import tap.data.TableIterator; +import tap.data.VOTableIterator; import tap.db.DBConnection; -import tap.db.DBException; +import tap.metadata.TAPColumn; +import tap.metadata.TAPMetadata; +import tap.metadata.TAPMetadata.STDSchema; import tap.metadata.TAPSchema; import tap.metadata.TAPTable; -import tap.metadata.TAPTypes; -import tap.metadata.VotType; -import cds.savot.model.DataBinaryReader; -import cds.savot.model.FieldSet; -import cds.savot.model.SavotBinary; -import cds.savot.model.SavotField; -import cds.savot.model.SavotResource; -import cds.savot.model.SavotTR; -import cds.savot.model.SavotTableData; -import cds.savot.model.TRSet; -import cds.savot.pull.SavotPullEngine; -import cds.savot.pull.SavotPullParser; +import tap.parameters.DALIUpload; +import uws.UWSException; +import uws.service.file.UnsupportedURIProtocolException; import com.oreilly.servlet.multipart.ExceededSizeException; /** + *

Let create properly given VOTable inputs in the "database".

* - * @author Grégory Mantelet (CDS;ARI) - gmantele@ari.uni-heidelberg.de - * @version 1.1 (03/2014) + *

+ * This class manages particularly the upload limit in rows and in bytes by creating a {@link LimitedTableIterator} + * with a {@link VOTableIterator}. + *

+ * + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (04/2015) + * + * @see LimitedTableIterator + * @see VOTableIterator */ public class Uploader { - - protected final ServiceConnection service; - protected final DBConnection dbConn; - protected final int nbRowsLimit; - protected final int nbBytesLimit; - + /** Specification of the TAP service. */ + protected final ServiceConnection service; + /** Connection to the "database" (which lets upload the content of any given VOTable). */ + protected final DBConnection dbConn; + /** Description of the TAP_UPLOAD schema to use. + * @since 2.0 */ + protected final TAPSchema uploadSchema; + /** Type of limit to set: ROWS or BYTES. MAY be NULL ; if NULL, no limit will be set. */ + protected final LimitUnit limitUnit; + /** Limit on the number of rows or bytes (depending of {@link #limitUnit}) allowed to be uploaded in once (whatever is the number of tables). */ + protected final int limit; + + /** Number of rows already loaded. */ protected int nbRows = 0; - public Uploader(final ServiceConnection service, final DBConnection dbConn) throws TAPException{ + /** + * Build an {@link Uploader} object. + * + * @param service Specification of the TAP service using this uploader. + * @param dbConn A valid (open) connection to the "database". + * + * @throws TAPException If any error occurs while building this {@link Uploader}. + */ + public Uploader(final ServiceConnection service, final DBConnection dbConn) throws TAPException{ + this(service, dbConn, null); + } + + /** + * Build an {@link Uploader} object. + * + * @param service Specification of the TAP service using this uploader. + * @param dbConn A valid (open) connection to the "database". + * + * @throws TAPException If any error occurs while building this {@link Uploader}. + * + * @since 2.0 + */ + public Uploader(final ServiceConnection service, final DBConnection dbConn, final TAPSchema uplSchema) throws TAPException{ + // NULL tests: if (service == null) throw new NullPointerException("The given ServiceConnection is NULL !"); if (dbConn == null) throw new NullPointerException("The given DBConnection is NULL !"); + // Set the service and database connections: this.service = service; - this.dbConn = dbConn; - if (service.uploadEnabled()){ - if (service.getUploadLimitType()[1] == LimitUnit.rows){ - nbRowsLimit = ((service.getUploadLimit()[1] > 0) ? service.getUploadLimit()[1] : -1); - nbBytesLimit = -1; + // Set the given upload schema: + if (uplSchema != null){ + if (!uplSchema.getADQLName().equalsIgnoreCase(TAPMetadata.STDSchema.UPLOADSCHEMA.label)) + throw new TAPException("Incorrect upload schema! Its ADQL name MUST be \"" + TAPMetadata.STDSchema.UPLOADSCHEMA.label + "\" ; here is is \"" + uplSchema.getADQLName() + "\".", UWSException.INTERNAL_SERVER_ERROR); + else + this.uploadSchema = uplSchema; + } + // ...or the default one: + else + this.uploadSchema = new TAPSchema(TAPMetadata.STDSchema.UPLOADSCHEMA.label, "Schema for tables uploaded by users."); + + // Ensure UPLOAD is allowed by the TAP service specification... + if (this.service.uploadEnabled()){ + // ...and set the rows or bytes limit: + if (this.service.getUploadLimitType()[1] != null && this.service.getUploadLimit()[1] >= 0){ + limit = (int)(this.service.getUploadLimitType()[1].bytesFactor() * this.service.getUploadLimit()[1]); + limitUnit = (this.service.getUploadLimitType()[1] == LimitUnit.rows) ? LimitUnit.rows : LimitUnit.bytes; }else{ - nbBytesLimit = ((service.getUploadLimit()[1] > 0) ? service.getUploadLimit()[1] : -1); - nbRowsLimit = -1; + limit = -1; + limitUnit = null; } }else throw new TAPException("Upload aborted: this functionality is disabled in this TAP service!"); } - public TAPSchema upload(final TableLoader[] loaders) throws TAPException{ - // Begin a DB transaction: - dbConn.startTransaction(); - - TAPSchema uploadSchema = new TAPSchema("TAP_UPLOAD"); + /** + *

Upload all the given VOTable inputs.

+ * + *

Note: + * The {@link TAPTable} objects representing the uploaded tables will be associated with the TAP_UPLOAD schema specified at the creation of this {@link Uploader}. + * If no such schema was specified, a default one (whose DB name will be equals to the ADQL name, that's to say {@link STDSchema#UPLOADSCHEMA}) + * is created, will be associated with the uploaded tables and will be returned by this function. + *

+ * + * @param uploads Array of tables to upload. + * + * @return A {@link TAPSchema} containing the list and the description of all uploaded tables. + * + * @throws TAPException If any error occurs while reading the VOTable inputs or while uploading the table into the "database". + * + * @see DBConnection#addUploadedTable(TAPTable, tap.data.TableIterator) + */ + public TAPSchema upload(final DALIUpload[] uploads) throws TAPException{ + TableIterator dataIt = null; InputStream votable = null; String tableName = null; - nbRows = 0; try{ - for(TableLoader loader : loaders){ - tableName = loader.tableName; - votable = loader.openStream(); + // Iterate over the full list of uploaded tables: + for(DALIUpload upl : uploads){ + tableName = upl.label; - if (nbBytesLimit > 0) - votable = new LimitedSizeInputStream(votable, nbBytesLimit); + // Open a stream toward the VOTable: + votable = upl.open(); - // start parsing the VOTable: - SavotPullParser parser = new SavotPullParser(votable, SavotPullEngine.SEQUENTIAL, null); + // Start reading the VOTable (with the identified limit, if any): + dataIt = new LimitedTableIterator(VOTableIterator.class, votable, limitUnit, limit); - SavotResource resource = parser.getNextResource(); - if (resource == null) - throw new TAPException("Incorrect VOTable format !"); + // Define the table to upload: + TAPColumn[] columns = dataIt.getMetadata(); + TAPTable table = new TAPTable(tableName); + table.setDBName(tableName + "_" + System.currentTimeMillis()); + for(TAPColumn col : columns) + table.addColumn(col); - FieldSet fields = resource.getFieldSet(0); + // Add the table to the TAP_UPLOAD schema: + uploadSchema.addTable(table); - // 1st STEP: Convert the VOTable metadata into DBTable: - TAPTable tapTable = fetchTableMeta(tableName, System.currentTimeMillis() + "", fields); - uploadSchema.addTable(tapTable); - - // 2nd STEP: Create the corresponding table in the database: - dbConn.createTable(tapTable); - - // 3rd STEP: Load rows into this table: - SavotBinary binary = resource.getData(0).getBinary(); - if (binary != null) - loadTable(tapTable, fields, binary); - else - loadTable(tapTable, fields, resource.getData(0).getTableData()); + // Create and fill the corresponding table in the database: + dbConn.addUploadedTable(table, dataIt); + // Close the VOTable stream: + dataIt.close(); votable.close(); + votable = null; } - }catch(DBException dbe){ - dbConn.cancelTransaction(); // ROLLBACK - throw dbe; - }catch(ExceededSizeException ese){ - dbConn.cancelTransaction(); // ROLLBACK - throw new TAPException("Upload limit exceeded ! You can upload at most " + ((nbBytesLimit > 0) ? (nbBytesLimit + " bytes.") : (nbRowsLimit + " rows."))); - }catch(IOException ioe){ - dbConn.cancelTransaction(); // ROLLBACK - throw new TAPException("Error while reading the VOTable of \"" + tableName + "\" !", ioe); - }catch(NullPointerException npe){ - dbConn.cancelTransaction(); // ROLLBACK - if (votable != null && votable instanceof LimitedSizeInputStream) - throw new TAPException("Upload limit exceeded ! You can upload at most " + ((nbBytesLimit > 0) ? (nbBytesLimit + " bytes.") : (nbRowsLimit + " rows."))); + }catch(DataReadException dre){ + if (dre.getCause() instanceof ExceededSizeException) + throw dre; else - throw new TAPException(npe); + throw new TAPException("Error while reading the VOTable \"" + tableName + "\": " + dre.getMessage(), dre, UWSException.BAD_REQUEST); + }catch(IOException ioe){ + throw new TAPException("IO error while reading the VOTable of \"" + tableName + "\"!", ioe); + }catch(UnsupportedURIProtocolException e){ + throw new TAPException("URI error while trying to open the VOTable of \"" + tableName + "\"!", e); }finally{ try{ + if (dataIt != null) + dataIt.close(); if (votable != null) votable.close(); }catch(IOException ioe){ @@ -146,77 +200,8 @@ public class Uploader { } } - // Commit modifications: - try{ - dbConn.endTransaction(); - }finally{ - dbConn.close(); - } - + // Return the TAP_UPLOAD schema (containing just the description of the uploaded tables): return uploadSchema; } - private TAPTable fetchTableMeta(final String tableName, final String userId, final FieldSet fields){ - TAPTable tapTable = new TAPTable(tableName); - tapTable.setDBName(tableName + "_" + userId); - - for(int j = 0; j < fields.getItemCount(); j++){ - SavotField field = (SavotField)fields.getItemAt(j); - int arraysize = TAPTypes.NO_SIZE; - if (field.getArraySize() == null || field.getArraySize().trim().isEmpty()) - arraysize = 1; - else if (field.getArraySize().equalsIgnoreCase("*")) - arraysize = TAPTypes.STAR_SIZE; - else{ - try{ - arraysize = Integer.parseInt(field.getArraySize()); - }catch(NumberFormatException nfe){ - service.getLogger().warning("Invalid array-size in the uploaded table \"" + tableName + "\" for the field \"" + field.getName() + "\": \"" + field.getArraySize() + "\" ! It will be considered as \"*\" !"); - } - } - tapTable.addColumn(field.getName(), field.getDescription(), field.getUnit(), field.getUcd(), field.getUtype(), new VotType(field.getDataType(), arraysize, field.getXtype()), false, false, false); - } - - return tapTable; - } - - private int loadTable(final TAPTable tapTable, final FieldSet fields, final SavotBinary binary) throws TAPException, ExceededSizeException{ - // Read the raw binary data: - DataBinaryReader reader = null; - try{ - reader = new DataBinaryReader(binary.getStream(), fields, false); - while(reader.next()){ - if (nbRowsLimit > 0 && nbRows >= nbRowsLimit) - throw new ExceededSizeException(); - dbConn.insertRow(reader.getTR(), tapTable); - nbRows++; - } - }catch(ExceededSizeException ese){ - throw ese; - }catch(IOException se){ - throw new TAPException("Error while reading the binary data of the VOTable of \"" + tapTable.getADQLName() + "\" !", se); - }finally{ - try{ - if (reader != null) - reader.close(); - }catch(IOException ioe){ - ; - } - } - - return nbRows; - } - - private int loadTable(final TAPTable tapTable, final FieldSet fields, final SavotTableData data) throws TAPException, ExceededSizeException{ - TRSet rows = data.getTRs(); - for(int i = 0; i < rows.getItemCount(); i++){ - if (nbRowsLimit > 0 && nbRows >= nbRowsLimit) - throw new ExceededSizeException(); - dbConn.insertRow((SavotTR)rows.getItemAt(i), tapTable); - nbRows++; - } - - return nbRows; - } - } diff --git a/src/uws/ClientAbortException.java b/src/uws/ClientAbortException.java new file mode 100644 index 0000000..df8a9cd --- /dev/null +++ b/src/uws/ClientAbortException.java @@ -0,0 +1,56 @@ +package uws; + +/* + * This file is part of UWSLibrary. + * + * UWSLibrary is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * UWSLibrary is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with UWSLibrary. If not, see . + * + * Copyright 2015 - Astronomisches Rechen Institut (ARI) + */ + +import java.io.IOException; + +/** + *

Exception which occurs when the connection between the HTTP client and a servlet has been unexpectedly closed.

+ * + *

+ * In such situation Tomcat and JBoss throw a class extending {@link IOException} and also named ClientAbortException. + * Jetty just throw a simple {@link IOException} with an appropriate message. And so, other servlet + * containers may throw a similar exception when a client-server connection is closed. This implementation + * of ClientAbortException provided by the library aims to signal this error in a unified way, with a single + * {@link IOException}, whatever is the underlying servlet container. + *

+ * + *

Note: + * Instead of this exception any IOException thrown by an {@link java.io.OutputStream} or a {@link java.io.PrintWriter} + * which has been provided by an {@link javax.servlet.http.HttpServletResponse} should be considered as an abortion of + * the HTTP client. + *

+ * + * @author Grégory Mantelet (ARI) + * @version 4.1 (04/2015) + * @since 4.1 + */ +public class ClientAbortException extends IOException { + private static final long serialVersionUID = 1L; + + public ClientAbortException(){ + super(); + } + + public ClientAbortException(final IOException ioe){ + super(ioe); + } + +} diff --git a/src/uws/ISO8601Format.java b/src/uws/ISO8601Format.java new file mode 100644 index 0000000..fab1cc0 --- /dev/null +++ b/src/uws/ISO8601Format.java @@ -0,0 +1,342 @@ +package uws; + +import java.text.DecimalFormat; +import java.text.ParseException; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.TimeZone; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + *

Let formatting and parsing date expressed in ISO8601 format.

+ * + *

Date formatting

+ * + *

+ * Dates are formatted using the following format: "yyyy-MM-dd'T'hh:mm:ss'Z'" if in UTC or "yyyy-MM-dd'T'hh:mm:ss[+|-]hh:mm" otherwise. + * On the contrary to the time zone, by default the number of milliseconds is not displayed. However, when displayed, the format is: + * "yyyy-MM-dd'T'hh:mm:ss.sss'Z'" if in UTC or "yyyy-MM-dd'T'hh:mm:ss.sss[+|-]hh:mm" otherwise. + * + * + *

+ * As said previously, it is possible to display or to hide the time zone and the milliseconds. This can be easily done by changing + * the value of the static attributes {@link #displayTimeZone} and {@link #displayMilliseconds}. By default {@link #displayTimeZone} is true + * and {@link #displayMilliseconds} is false. + * + * + *

+ * By default the date will be formatted in the local time zone. But this could be specified either in the format function {@link #format(long, String, boolean, boolean)} + * or by changing the static attribute {@link #targetTimeZone}. The time zone must be specified with its ID. The list of all available time zone IDs is given by + * {@link TimeZone#getAvailableIDs()}. + *

+ * + *

Date parsing

+ * + *

+ * This class is able to parse dates - with the function {@link #parse(String)} - formatted strictly in ISO8601 + * but is also more permissive. Particularly, separators (like '-' and ':') are optional. The date and time separator + * ('T') can be replaced by a space. + *

+ * + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (10/2014) + * @since 4.1 + */ +public class ISO8601Format { + + /** Indicate whether any date formatted with this class displays the time zone. */ + public static boolean displayTimeZone = false; + /** Indicate whether any date formatted with this class displays the milliseconds. */ + public static boolean displayMilliseconds = false; + /** Indicate the time zone in which the date and time should be formatted (whatever is the time zone of the given date). */ + public static String targetTimeZone = "UTC"; // for the local time zone: TimeZone.getDefault().getID(); + + /** Object to use to format numbers with two digits (ie. 12, 02, 00). */ + protected final static DecimalFormat twoDigitsFmt = new DecimalFormat("00"); + /** Object to use to format numbers with three digits (ie. 001, 000, 123). */ + protected final static DecimalFormat threeDigitsFmt = new DecimalFormat("000"); + + /** + *

Format the given date-time in ISO8601 format.

+ * + *

Note: + * This function is equivalent to {@link #format(long, String, boolean, boolean)} with the following parameters: + * d, ISO8601Format.targetTimeZone, ISO8601Format.displayTimeZone, ISO8601Format.displayMilliseconds. + *

+ * + * @param date Date-time. + * + * @return Date formatted in ISO8601. + */ + public static String format(final Date date){ + return format(date.getTime(), targetTimeZone, displayTimeZone, displayMilliseconds); + } + + /** + *

Format the given date-time in ISO8601 format.

+ * + *

Note: + * This function is equivalent to {@link #format(long, String, boolean, boolean)} with the following parameters: + * d, ISO8601Format.targetTimeZone, ISO8601Format.displayTimeZone, ISO8601Format.displayMilliseconds. + *

+ * + * @param date Date-time in milliseconds (from the 1st January 1970 ; this value is returned by java.util.Date#getTime()). + * + * @return Date formatted in ISO8601. + */ + public static String format(final long date){ + return format(date, targetTimeZone, displayTimeZone, displayMilliseconds); + } + + /** + *

Convert the given date-time in the given time zone and format it in ISO8601 format.

+ * + *

Note: + * This function is equivalent to {@link #format(long, String, boolean, boolean)} with the following parameters: + * d, ISO8601Format.targetTimeZone, withTimeZone, ISO8601Format.displayMilliseconds. + *

+ * + * @param date Date-time in milliseconds (from the 1st January 1970 ; this value is returned by java.util.Date#getTime()). + * @param withTimeZone Target time zone. + * + * @return Date formatted in ISO8601. + */ + public static String format(final long date, final boolean withTimeZone){ + return format(date, targetTimeZone, withTimeZone, displayMilliseconds); + } + + /** + *

Convert the given date-time in UTC and format it in ISO8601 format.

+ * + *

Note: + * This function is equivalent to {@link #format(long, String, boolean, boolean)} with the following parameters: + * d, "UTC", ISO8601Format.displayTimeZone, ISO8601Format.displayMilliseconds. + *

+ * + * @param date Date-time in milliseconds (from the 1st January 1970 ; this value is returned by java.util.Date#getTime()). + * + * @return Date formatted in ISO8601. + */ + public static String formatInUTC(final long date){ + return format(date, "UTC", displayTimeZone, displayMilliseconds); + } + + /** + *

Convert the given date-time in UTC and format it in ISO8601 format.

+ * + *

Note: + * This function is equivalent to {@link #format(long, String, boolean, boolean)} with the following parameters: + * d, "UTC", withTimeZone, ISO8601Format.displayMilliseconds. + *

+ * + * @param date Date-time in milliseconds (from the 1st January 1970 ; this value is returned by java.util.Date#getTime()). + * @param withTimeZone Target time zone. + * + * @return Date formatted in ISO8601. + */ + public static String formatInUTC(final long date, final boolean withTimeZone){ + return format(date, "UTC", withTimeZone, displayMilliseconds); + } + + /** + * Convert the given date in the given time zone and format it in ISO8601 format, with or without displaying the time zone + * and/or the milliseconds field. + * + * @param date Date-time in milliseconds (from the 1st January 1970 ; this value is returned by java.util.Date#getTime()). + * @param targetTimeZone Target time zone. + * @param withTimeZone true to display the time zone, false otherwise. + * @param withMillisec true to display the milliseconds, false otherwise. + * + * @return Date formatted in ISO8601. + */ + protected static String format(final long date, final String targetTimeZone, final boolean withTimeZone, final boolean withMillisec){ + GregorianCalendar cal = new GregorianCalendar(); + cal.setTimeInMillis(date); + + // Convert the given date in the target Time Zone: + if (targetTimeZone != null && targetTimeZone.length() > 0) + cal.setTimeZone(TimeZone.getTimeZone(targetTimeZone)); + else + cal.setTimeZone(TimeZone.getTimeZone(ISO8601Format.targetTimeZone)); + + StringBuffer buf = new StringBuffer(); + + // Date with format yyyy-MM-dd : + buf.append(cal.get(Calendar.YEAR)).append('-'); + buf.append(twoDigitsFmt.format(cal.get(Calendar.MONTH) + 1)).append('-'); + buf.append(twoDigitsFmt.format(cal.get(Calendar.DAY_OF_MONTH))); + + // Time with format 'T'HH:mm:ss : + buf.append('T').append(twoDigitsFmt.format(cal.get(Calendar.HOUR_OF_DAY))).append(':'); + buf.append(twoDigitsFmt.format(cal.get(Calendar.MINUTE))).append(':'); + buf.append(twoDigitsFmt.format(cal.get(Calendar.SECOND))); + if (withMillisec){ + buf.append('.').append(threeDigitsFmt.format(cal.get(Calendar.MILLISECOND))); + } + + // Time zone with format (+|-)HH:mm : + if (withTimeZone){ + int tzOffset = (cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET)) / (60 * 1000); // offset in minutes + boolean negative = (tzOffset < 0); + if (negative) + tzOffset *= -1; + int hours = tzOffset / 60, minutes = tzOffset - (hours * 60); + if (hours == 0 && minutes == 0) + buf.append('Z'); + else{ + buf.append(negative ? '-' : '+'); + buf.append(twoDigitsFmt.format(hours)).append(':'); + buf.append(twoDigitsFmt.format(minutes)); + } + } + + return buf.toString(); + } + + /** + *

Parse the given date expressed using the ISO8601 format ("yyyy-MM-dd'T'hh:mm:ss.sssZ" + * or "yyyy-MM-dd'T'hh:mm:ss.sssZ[+|-]hh:mm:ss").

+ * + *

+ * The syntax of the given date may be more or less strict. Particularly, separators like '-' and ':' are optional. + * Besides the date and time separator ('T') may be replaced by a space. + *

+ * + *

+ * The minimum allowed string is the date: "yyyy-MM-dd". All other date-time fields are optional, + * BUT, the time zone can be given without the time. + *

+ * + *

+ * If no time zone is specified (by a 'Z' or a time offset), the time zone in which the date is expressed + * is supposed to be the local one. + *

+ * + *

Note: + * This function is equivalent to {@link #parse(String)}, but whose the returned value is used to create a Date object, like this: + * return new Date(parse(strDate)). + *

+ * + * @param strDate Date expressed as a string in ISO8601 format. + * + * @return Parsed date (expressed in milliseconds from the 1st January 1970 ; + * a date can be easily built with this number using {@link java.util.Date#Date(long)}). + * + * @throws ParseException If the given date is not expressed in ISO8601 format or is not merely parseable with this implementation. + */ + public final static Date parseToDate(final String strDate) throws ParseException{ + return new Date(parse(strDate)); + } + + /** + *

Parse the given date expressed using the ISO8601 format ("yyyy-MM-dd'T'hh:mm:ss.sssZ" + * or "yyyy-MM-dd'T'hh:mm:ss.sssZ[+|-]hh:mm:ss").

+ * + *

+ * The syntax of the given date may be more or less strict. Particularly, separators like '-' and ':' are optional. + * Besides the date and time separator ('T') may be replaced by a space. + *

+ * + *

+ * The minimum allowed string is the date: "yyyy-MM-dd". All other date-time fields are optional, + * BUT, the time zone can be given without the time. + *

+ * + *

+ * If no time zone is specified (by a 'Z' or a time offset), the time zone in which the date is expressed + * is supposed to be the local one. + *

+ * + * @param strDate Date expressed as a string in ISO8601 format. + * + * @return Parsed date (expressed in milliseconds from the 1st January 1970 ; + * a date can be easily built with this number using {@link java.util.Date#Date(long)}). + * + * @throws ParseException If the given date is not expressed in ISO8601 format or is not merely parseable with this implementation. + */ + public static long parse(final String strDate) throws ParseException{ + Pattern p = Pattern.compile("(\\d{4})-?(\\d{2})-?(\\d{2})([T| ](\\d{2}):?(\\d{2}):?(\\d{2})(\\.(\\d+))?(Z|([\\+|\\-])(\\d{2}):?(\\d{2})(:?(\\d{2}))?)?)?"); + /* + * With this regular expression, we will get the following groups: + * + * ( 0: everything) + * 1: year (yyyy) + * 2: month (MM) + * 3: day (dd) + * ( 4: the full time part) + * 5: hours (hh) + * 6: minutes (mm) + * 7: seconds (ss) + * ( 8: the full ms part) + * 9: milliseconds (sss) + * (10: the full time zone part: 'Z' or the applied time offset) + * 11: sign of the offset ('+' if an addition was applied, '-' if it was a subtraction) + * 12: applied hours offset (hh) + * 13: applied minutes offset (mm) + * (14: the full seconds offset) + * 15: applied seconds offset (ss) + * + * Groups in parenthesis should be ignored ; but an exception must be done for the 10th which may contain 'Z' meaning a UTC time zone. + * + * All groups from the 4th (included) are optional. If not filled, an optional group is set to NULL. + * + * This regular expression is more permissive than the strict definition of the ISO8601 format. Particularly, separator characters + * ('-', 'T' and ':') are optional and it is possible to specify seconds in the time zone offset. + */ + + Matcher m = p.matcher(strDate); + if (m.matches()){ + Calendar cal = new GregorianCalendar(); + + // Set the time zone: + /* + * Note: In this library, we suppose that any date provided without specified time zone, is in UTC. + * + * It is more a TAP specification than a UWS one ; see the REC-TAP 1.0 at section 2.3.4 (page 15): + * "Within the ADQL query, the service must support the use of timestamp values in + * ISO8601 format, specifically yyyy-MM-dd['T'HH:mm:ss[.SSS]], where square + * brackets denote optional parts and the 'T' denotes a single character separator + * (T) between the date and time parts." + * + * ...and 2.5 (page 20): + * "TIMESTAMP values are specified using ISO8601 format without a timezone (as in 2.3.4 ) and are assumed to be in UTC." + */ + cal.setTimeZone(TimeZone.getTimeZone("UTC")); + + // Set the date: + cal.set(Calendar.DAY_OF_MONTH, twoDigitsFmt.parse(m.group(3)).intValue()); + cal.set(Calendar.MONTH, twoDigitsFmt.parse(m.group(2)).intValue() - 1); + cal.set(Calendar.YEAR, Integer.parseInt(m.group(1))); + + // Set the time: + if (m.group(4) != null){ + cal.set(Calendar.HOUR_OF_DAY, twoDigitsFmt.parse(m.group(5)).intValue()); + cal.set(Calendar.MINUTE, twoDigitsFmt.parse(m.group(6)).intValue()); + cal.set(Calendar.SECOND, twoDigitsFmt.parse(m.group(7)).intValue()); + if (m.group(9) != null) + cal.set(Calendar.MILLISECOND, twoDigitsFmt.parse(m.group(9)).intValue()); + else + cal.set(Calendar.MILLISECOND, 0); + }else{ + cal.set(Calendar.HOUR_OF_DAY, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MILLISECOND, 0); + } + + // Compute and apply the offset: + if (m.group(10) != null && !m.group(10).equals("Z")){ + int sign = (m.group(11).equals("-") ? 1 : -1); + cal.add(Calendar.HOUR_OF_DAY, sign * twoDigitsFmt.parse(m.group(12)).intValue()); + cal.add(Calendar.MINUTE, sign * twoDigitsFmt.parse(m.group(13)).intValue()); + if (m.group(15) != null) + cal.add(Calendar.SECOND, sign * twoDigitsFmt.parse(m.group(15)).intValue()); + } + + return cal.getTimeInMillis(); + }else + throw new ParseException("Invalid date format: \"" + strDate + "\"! An ISO8601 date was expected.", 0); + } +} diff --git a/src/uws/UWSException.java b/src/uws/UWSException.java index 32aed8c..1128170 100644 --- a/src/uws/UWSException.java +++ b/src/uws/UWSException.java @@ -16,23 +16,30 @@ package uws; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import uws.job.ErrorType; /** - * Any exception returned by a class of the UWS pattern may be associated with - * an HTTP error code (like: 404, 303, 500) and a UWS error type. + *

Any exception returned by a class of the UWS pattern may be associated with + * an HTTP error code (like: 404, 303, 500) and a UWS error type.

* - * @author Grégory Mantelet (CDS) - * @version 12/2010 + *

+ * Any error reported with this kind of exception will (in the most of cases) interrupt a UWS action, + * by reporting an error related with the UWS usage. + *

+ * + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (09/2014) */ public class UWSException extends Exception { private static final long serialVersionUID = 1L; // SUCCESS codes: public final static int OK = 200; + public final static int ACCEPTED_BUT_NOT_COMPLETE = 202; public final static int NO_CONTENT = 204; // REDIRECTION codes: @@ -61,69 +68,136 @@ public class UWSException extends Exception { /* ************ */ /* CONSTRUCTORS */ /* ************ */ + /** + * Exception in the general context of UWS. + * + * @param msg Error message to display. + */ public UWSException(String msg){ - this(msg, ErrorType.FATAL); + this(msg, null); } + /** + * Exception that occurs in the general context of UWS, and with the specified error type (FATAL or TRANSIENT). + * + * @param msg Error message to display. + * @param type Type of the error (FATAL or TRANSIENT). Note: If NULL, it will be considered as FATAL. + */ public UWSException(String msg, ErrorType type){ super(msg); - if (type != null) - errorType = type; + this.errorType = (type == null) ? ErrorType.FATAL : type; } + /** + * Exception that occurs in the general context of UWS because the given exception has been thrown. + * + * @param t The thrown (and so caught) exception. + */ public UWSException(Throwable t){ - this(t, ErrorType.FATAL); + this(t, null); } + /** + * Exception with the given type that occurs in the general context of UWS + * because the given exception has been thrown. + * + * @param t The thrown (and so caught) exception. + * @param type Type of the error (FATAL or TRANSIENT). Note: If NULL, it will be considered as FATAL. + */ public UWSException(Throwable t, ErrorType type){ super(t); - if (type != null) - errorType = type; + this.errorType = (type == null) ? ErrorType.FATAL : type; } + /** + * Exception that occurs in the general context of UWS and which should return the given HTTP error code. + * + * @param httpError HTTP error code to return. + * @param msg Error message to display. + */ public UWSException(int httpError, String msg){ - this(msg); - if (httpError >= 0) - httpErrorCode = httpError; + this(httpError, msg, null); } + /** + * Exception that occurs in the general context of UWS, with the given type and which should return the given HTTP error code. + * + * @param httpError HTTP error code to return. + * @param msg Error message to display. + * @param type Type of the error (FATAL or TRANSIENT). Note: If NULL, it will be considered as FATAL. + */ public UWSException(int httpError, String msg, ErrorType type){ this(msg, type); - if (httpError >= 0) - httpErrorCode = httpError; + this.httpErrorCode = (httpError < 0) ? NOT_FOUND : httpError; } + /** + * Exception that occurs in the general context of UWS, + * because the given exception has been thrown and that which should return the given HTTP error status. + * + * @param httpError HTTP error code to return. + * @param t The thrown (and so caught) exception. + */ public UWSException(int httpError, Throwable t){ - this(t); - if (httpError >= 0) - httpErrorCode = httpError; + this(httpError, t, (t != null) ? t.getMessage() : null, null); } + /** + * Exception that occurs in the general context of UWS with the given error type, + * because the given exception has been thrown and that which should return the given HTTP error status. + * + * @param httpError HTTP error code to return. + * @param t The thrown (and so caught) exception. + * @param type Type of the error (FATAL or TRANSIENT). Note: If NULL, it will be considered as FATAL. + */ public UWSException(int httpError, Throwable t, ErrorType type){ - this(t, type); - if (httpError >= 0) - httpErrorCode = httpError; + this(httpError, t, (t != null) ? t.getMessage() : null, type); } + /** + * Exception that occurs in the general context of UWS, + * because the given exception has been thrown and that which should return the given HTTP error status. + * + * @param httpError HTTP error code to return. + * @param t The thrown (and so caught) exception. + * @param msg Error message to display. + */ public UWSException(int httpError, Throwable t, String msg){ - this(httpError, t, msg, ErrorType.FATAL); + this(httpError, t, msg, null); } + /** + * Exception that occurs in the general context of UWS, + * because the given exception has been thrown and that which should return the given HTTP error status. + * + * @param httpError HTTP error code to return. + * @param t The thrown (and so caught) exception. + * @param msg Error message to display. + * @param type Type of the error (FATAL or TRANSIENT). Note: If NULL, it will be considered as FATAL. + */ public UWSException(int httpError, Throwable t, String msg, ErrorType type){ super(msg, t); - if (httpError >= 0) - httpErrorCode = httpError; - if (type != null) - errorType = type; + this.httpErrorCode = (httpError < 0) ? NOT_FOUND : httpError; + this.errorType = (type == null) ? ErrorType.FATAL : type; } /* ******* */ /* GETTERS */ /* ******* */ + /** + * Get the HTTP error code that should be returned. + * + * @return The corresponding HTTP error code. + */ public int getHttpErrorCode(){ return httpErrorCode; } + /** + * Get the type of this error (from the UWS point of view ; FATAL or TRANSIENT). + * + * @return Type of this error. + */ public ErrorType getUWSErrorType(){ return errorType; } diff --git a/src/uws/UWSExceptionFactory.java b/src/uws/UWSExceptionFactory.java index c281273..c88fff2 100644 --- a/src/uws/UWSExceptionFactory.java +++ b/src/uws/UWSExceptionFactory.java @@ -16,18 +16,18 @@ package uws; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import uws.job.ExecutionPhase; - import uws.job.user.JobOwner; /** * Let's creating the common exceptions of a UWS service. * - * @author Grégory Mantelet (CDS) - * @version 05/2012 + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (09/2014) * * @see UWSException */ @@ -49,164 +49,47 @@ public final class UWSExceptionFactory { return ((consequence == null || consequence.trim().length() > 0) ? "" : " => " + consequence); } - public final static UWSException missingJobListName(){ - return missingJobListName(null); - } - - public final static UWSException missingJobListName(final String consequence){ - return new UWSException(UWSException.BAD_REQUEST, "Missing job list name !" + appendMessage(consequence)); - } - - public final static UWSException incorrectJobListName(final String jlName){ - return incorrectJobListName(jlName, null); - } - - public final static UWSException incorrectJobListName(final String jlName, final String consequence){ - return new UWSException(UWSException.NOT_FOUND, "Incorrect job list name ! The jobs list " + jlName + " does not exist." + appendMessage(consequence)); - } - - public final static UWSException missingJobID(){ - return missingJobID(null); - } - - public final static UWSException missingJobID(final String consequence){ - return new UWSException(UWSException.BAD_REQUEST, "Missing job ID !" + appendMessage(consequence)); - } - - public final static UWSException incorrectJobID(String jobListName, String jobID){ - return incorrectJobID(jobListName, jobID, null); - } - - public final static UWSException incorrectJobID(final String jobListName, final String jobID, final String consequence){ - return new UWSException(UWSException.NOT_FOUND, "Incorrect job ID ! The job " + jobID + " does not exist in the jobs list " + jobListName + appendMessage(consequence)); - } - - public final static UWSException missingSerializer(final String mimeTypes){ - return missingSerializer(null); - } - - public final static UWSException missingSerializer(final String mimeTypes, final String consequence){ - return new UWSException(UWSException.INTERNAL_SERVER_ERROR, "Missing UWS serializer for the MIME types: " + mimeTypes + " !" + appendMessage(consequence)); - } - - public final static UWSException incorrectJobParameter(final String jobID, final String paramName){ - return incorrectJobParameter(jobID, paramName, null); - } - - public final static UWSException incorrectJobParameter(final String jobID, final String paramName, final String consequence){ - return new UWSException(UWSException.NOT_FOUND, "Incorrect job parameter ! The parameter " + paramName + " does not exist in the job " + jobID + "." + appendMessage(consequence)); - } - - public final static UWSException incorrectJobResult(final String jobID, final String resultID){ - return incorrectJobResult(jobID, resultID, null); - } - - public final static UWSException incorrectJobResult(final String jobID, final String resultID, final String consequence){ - return new UWSException(UWSException.NOT_FOUND, "Incorrect result ID ! There is no result " + resultID + " in the job " + jobID + "." + appendMessage(consequence)); - } - - public final static UWSException noErrorSummary(final String jobID){ - return noErrorSummary(jobID, null); - } - - public final static UWSException noErrorSummary(final String jobID, final String consequence){ - return new UWSException(UWSException.NOT_FOUND, "There is no error summary in the job " + jobID + " !" + appendMessage(consequence)); - } - - public final static UWSException incorrectPhaseTransition(final String jobID, final ExecutionPhase fromPhase, final ExecutionPhase toPhase){ + public final static String incorrectPhaseTransition(final String jobID, final ExecutionPhase fromPhase, final ExecutionPhase toPhase){ return incorrectPhaseTransition(jobID, fromPhase, toPhase, null); } - public final static UWSException incorrectPhaseTransition(final String jobID, final ExecutionPhase fromPhase, final ExecutionPhase toPhase, final String consequence){ - return new UWSException(UWSException.BAD_REQUEST, "Incorrect phase transition ! => the job " + jobID + " is in the phase " + fromPhase + ". It can not go to " + toPhase + "." + appendMessage(consequence)); - } - - public final static UWSException missingOutputStream(){ - return missingOutputStream(null); + public final static String incorrectPhaseTransition(final String jobID, final ExecutionPhase fromPhase, final ExecutionPhase toPhase, final String consequence){ + return "Incorrect phase transition ! => the job " + jobID + " is in the phase " + fromPhase + ". It can not go to " + toPhase + "." + appendMessage(consequence); } - public final static UWSException missingOutputStream(final String consequence){ - return new UWSException(UWSException.INTERNAL_SERVER_ERROR, "Missing output stream !" + appendMessage(consequence)); - } - - public final static UWSException incorrectSerialization(final String serializationValue, final String serializationTarget){ - return incorrectSerialization(serializationValue, serializationTarget, null); - } - - public final static UWSException incorrectSerialization(final String serializationValue, final String serializationTarget, final String consequence){ - return new UWSException(UWSException.INTERNAL_SERVER_ERROR, "Incorrect serialization value (=" + serializationValue + ") ! => impossible to serialize " + serializationTarget + "." + appendMessage(consequence)); - } - - public final static UWSException readPermissionDenied(final JobOwner user, final boolean jobList, final String containerName){ + public final static String readPermissionDenied(final JobOwner user, final boolean jobList, final String containerName){ return readPermissionDenied(user, jobList, containerName, null); } - public final static UWSException readPermissionDenied(final JobOwner user, final boolean jobList, final String containerName, final String consequence){ - return new UWSException(UWSException.PERMISSION_DENIED, user.getID() + ((user.getPseudo() == null) ? "" : (" (alias " + user.getPseudo() + ")")) + " is not allowed to read the content of the " + (jobList ? "jobs list" : "job") + " \"" + containerName + "\" !" + appendMessage(consequence)); + public final static String readPermissionDenied(final JobOwner user, final boolean jobList, final String containerName, final String consequence){ + return user.getID() + ((user.getPseudo() == null) ? "" : (" (alias " + user.getPseudo() + ")")) + " is not allowed to read the content of the " + (jobList ? "jobs list" : "job") + " \"" + containerName + "\" !" + appendMessage(consequence); } - public final static UWSException writePermissionDenied(final JobOwner user, final boolean jobList, final String containerName){ + public final static String writePermissionDenied(final JobOwner user, final boolean jobList, final String containerName){ return writePermissionDenied(user, jobList, containerName, null); } - public final static UWSException writePermissionDenied(final JobOwner user, final boolean jobList, final String containerName, final String consequence){ - return new UWSException(UWSException.PERMISSION_DENIED, user.getID() + ((user.getPseudo() == null) ? "" : (" (alias " + user.getPseudo() + ")")) + " is not allowed to update the content of the " + (jobList ? "jobs list" : "job") + " \"" + containerName + "\" !" + appendMessage(consequence)); + public final static String writePermissionDenied(final JobOwner user, final boolean jobList, final String containerName, final String consequence){ + return user.getID() + ((user.getPseudo() == null) ? "" : (" (alias " + user.getPseudo() + ")")) + " is not allowed to update the content of the " + (jobList ? "jobs list" : "job") + " \"" + containerName + "\" !" + appendMessage(consequence); } - public final static UWSException executePermissionDenied(final JobOwner user, final String jobID){ + public final static String executePermissionDenied(final JobOwner user, final String jobID){ return executePermissionDenied(user, jobID, null); } - public final static UWSException executePermissionDenied(final JobOwner user, final String jobID, final String consequence){ - return new UWSException(UWSException.PERMISSION_DENIED, user.getID() + ((user.getPseudo() == null) ? "" : (" (alias " + user.getPseudo() + ")")) + " is not allowed to execute/abort the job \"" + jobID + "\" !" + appendMessage(consequence)); + public final static String executePermissionDenied(final JobOwner user, final String jobID, final String consequence){ + return user.getID() + ((user.getPseudo() == null) ? "" : (" (alias " + user.getPseudo() + ")")) + " is not allowed to execute/abort the job \"" + jobID + "\" !" + appendMessage(consequence); } - public final static UWSException restoreJobImpossible(final Throwable t, final String cause){ - return restoreJobImpossible(t, cause, null); - } - - public final static UWSException restoreJobImpossible(final Throwable t, final String cause, final String consequence){ - return new UWSException(UWSException.INTERNAL_SERVER_ERROR, t, ((cause == null) ? "" : cause) + " Impossible to restore a job from the backup file(s)." + appendMessage(consequence)); - } - - public final static UWSException restoreUserImpossible(final String cause){ - return restoreUserImpossible(null, cause, null); - } - - public final static UWSException restoreUserImpossible(final Throwable t, final String cause){ - return restoreUserImpossible(t, cause, null); - } - - public final static UWSException restoreUserImpossible(final Throwable t, final String cause, final String consequence){ - return new UWSException(UWSException.INTERNAL_SERVER_ERROR, t, ((cause == null) ? "" : cause) + " Impossible to restore a user from the backup file(s)." + appendMessage(consequence)); - } - - public final static UWSException jobModificationForbidden(final String jobId, final ExecutionPhase phase, final String parameter){ + public final static String jobModificationForbidden(final String jobId, final ExecutionPhase phase, final String parameter){ return jobModificationForbidden(jobId, phase, parameter, null); } - public final static UWSException jobModificationForbidden(final String jobId, final ExecutionPhase phase, final String parameter, final String consequence){ + public final static String jobModificationForbidden(final String jobId, final ExecutionPhase phase, final String parameter, final String consequence){ if (parameter != null && !parameter.trim().isEmpty()) - return new UWSException(UWSException.NOT_ALLOWED, "Impossible to change the parameter \"" + parameter + "\" of the job " + jobId + ((phase != null) ? (" (phase: " + phase + ")") : "") + " !" + appendMessage(consequence)); + return "Impossible to change the parameter \"" + parameter + "\" of the job " + jobId + ((phase != null) ? (" (phase: " + phase + ")") : "") + " !" + appendMessage(consequence); else - return new UWSException(UWSException.NOT_ALLOWED, "Impossible to change the parameters of the job " + jobId + ((phase != null) ? (" (phase: " + phase + ")") : "") + " !" + appendMessage(consequence)); - } - - public final static UWSException badFormat(final String jobId, final String paramName, final String paramValue, final String valueClass, final String expectedFormat){ - return badFormat(jobId, paramName, paramValue, valueClass, expectedFormat, null); - } - - public final static UWSException badFormat(final String jobId, final String paramName, final String paramValue, final String valueClass, final String expectedFormat, final String consequence){ - String strExpected = ((expectedFormat != null && !expectedFormat.trim().isEmpty()) ? (" Expected: " + expectedFormat) : ""); - String strClass = ((valueClass != null && !valueClass.trim().isEmpty()) ? (" {an instance of " + valueClass + "}") : ""); - - if (paramName != null && !paramName.trim().isEmpty()){ - if (jobId != null && !jobId.trim().isEmpty()) - return new UWSException(UWSException.BAD_REQUEST, "Bad format for the parameter " + paramName.toUpperCase() + " of the job " + jobId + ": \"" + paramValue + "\"" + strClass + "." + strExpected + appendMessage(consequence)); - else - return new UWSException(UWSException.BAD_REQUEST, "Bad format for " + paramName + ": \"" + paramValue + "\"" + strClass + "." + strExpected + appendMessage(consequence)); - }else - return new UWSException(UWSException.BAD_REQUEST, "Bad format: \"" + paramValue + "\"" + strClass + "." + strExpected + appendMessage(consequence)); + return "Impossible to change the parameters of the job " + jobId + ((phase != null) ? (" (phase: " + phase + ")") : "") + " !" + appendMessage(consequence); } } diff --git a/src/uws/UWSToolBox.java b/src/uws/UWSToolBox.java index f6dc4f5..34bb155 100644 --- a/src/uws/UWSToolBox.java +++ b/src/uws/UWSToolBox.java @@ -16,7 +16,8 @@ package uws; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.io.File; @@ -24,36 +25,44 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; - +import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URL; - +import java.net.URLDecoder; +import java.net.URLEncoder; import java.util.Date; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; import javax.servlet.ServletOutputStream; - import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import uws.job.ErrorSummary; import uws.job.UWSJob; - +import uws.job.user.JobOwner; +import uws.service.UWS; import uws.service.UWSUrl; - +import uws.service.UserIdentifier; import uws.service.log.DefaultUWSLog; import uws.service.log.UWSLog; +import uws.service.request.RequestParser; +import uws.service.request.UploadFile; /** * Some useful functions for the managing of a UWS service. * - * @author Grégory Mantelet (CDS) - * @version 05/2012 + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (04/2015) */ public class UWSToolBox { + /** + * Default character encoding for all HTTP response sent by this library. + * @since 4.1 */ + public final static String DEFAULT_CHAR_ENCODING = "UTF-8"; + private static UWSLog defaultLogger = null; /** THIS CLASS CAN'T BE INSTANTIATED ! */ @@ -111,9 +120,15 @@ public class UWSToolBox { /** *

Builds a map of strings with all parameters of the given HTTP request.

* - *

NOTE: - * it converts the Map<String, String[]> returned by {@link HttpServletRequest#getParameterMap()} - * into a Map<String, String> (the key is put in lower case). + *

Note: + * If the request attribute {@link UWS#REQ_ATTRIBUTE_PARAMETERS} has been already set by the UWS library, + * this map (after conversion into a Map) is returned. + * Otherwise, the parameters identified automatically by the Servlet are returned (just the last occurrence of each parameter is kept). + *

+ * + *

WARNING: + * This function does not extract directly the parameters from the request content. It is just returning those already extracted + * either by the Servlet or by a {@link RequestParser}. *

* * @param req The HTTP request which contains the parameters to extract. @@ -121,25 +136,58 @@ public class UWSToolBox { * @return The corresponding map of string. */ @SuppressWarnings("unchecked") - public static final HashMap getParamsMap(HttpServletRequest req){ - HashMap params = new HashMap(req.getParameterMap().size()); + public static final HashMap getParamsMap(final HttpServletRequest req){ + HashMap map = new HashMap(); + + /* If the attribute "PARAMETERS" has been already set by the UWS library, + * return it by casting it from Map into Map: */ + if (req.getAttribute(UWS.REQ_ATTRIBUTE_PARAMETERS) != null){ + try{ + // Get the extracted parameters: + Map params = (Map)req.getAttribute(UWS.REQ_ATTRIBUTE_PARAMETERS); + + // Transform the map of Objects into a map of Strings: + for(Map.Entry e : params.entrySet()){ + if (e.getValue() != null) + map.put(e.getKey(), e.getValue().toString()); + } - Enumeration e = req.getParameterNames(); - while(e.hasMoreElements()){ - String name = e.nextElement(); - params.put(name.toLowerCase(), req.getParameter(name)); + // Return the fetched map: + return map; + + }catch(Exception ex){ + map.clear(); + } } - return params; + /* If there is no "PARAMETERS" attribute or if an error occurs while reading it, + * return all the parameters fetched by the Servlet: */ + Enumeration names = req.getParameterNames(); + int i; + String n; + String[] values; + while(names.hasMoreElements()){ + n = names.nextElement(); + values = req.getParameterValues(n); + // search for the last non-null occurrence: + i = values.length - 1; + while(i >= 0 && values[i] == null) + i--; + // if there is one, keep it: + if (i >= 0) + map.put(n.toLowerCase(), values[i]); + } + return map; } /** * Converts map of UWS parameters into a string corresponding to the query part of a HTTP-GET URL (i.e. ?EXECUTIONDURATION=60&DESTRUCTION=2010-09-01T13:58:00:000-0200). * * @param parameters A Map of parameters. + * * @return The corresponding query part of an HTTP-GET URL (all keys have been set in upper case). */ - public final static String getQueryPart(Map parameters){ + public final static String getQueryPart(final Map parameters){ if (parameters == null || parameters.isEmpty()) return ""; @@ -154,8 +202,10 @@ public class UWSToolBox { val = val.trim(); if (key != null && !key.isEmpty() && val != null && !val.isEmpty()){ - queryPart.append(e.getKey() + "=" + val); - queryPart.append("&"); + try{ + queryPart.append(URLEncoder.encode(e.getKey(), "UTF-8") + "=" + URLEncoder.encode(val, "UTF-8")); + queryPart.append("&"); + }catch(UnsupportedEncodingException uee){} } } @@ -166,6 +216,7 @@ public class UWSToolBox { * Converts the given query part of a HTTP-GET URL to a map of parameters. * * @param queryPart A query part of a HTTP-GET URL. + * * @return The corresponding map of parameters (all keys have been set in lower case). */ public final static Map getParameters(String queryPart){ @@ -180,8 +231,11 @@ public class UWSToolBox { if (keyValue.length == 2){ keyValue[0] = keyValue[0].trim().toLowerCase(); keyValue[1] = keyValue[1].trim(); - if (!keyValue[0].isEmpty() && !keyValue[1].isEmpty()) - parameters.put(keyValue[0].trim(), keyValue[1].trim()); + if (!keyValue[0].isEmpty() && !keyValue[1].isEmpty()){ + try{ + parameters.put(URLDecoder.decode(keyValue[0], "UTF-8"), URLDecoder.decode(keyValue[1], "UTF-8")); + }catch(UnsupportedEncodingException uee){} + } } } } @@ -190,6 +244,200 @@ public class UWSToolBox { return parameters; } + /** + *

Extract only the GET parameters from the given HTTP request and add them inside the given map.

+ * + *

Warning: + * If entries with the same key already exist in the map, they will overwritten. + *

+ * + * @param req The HTTP request whose the GET parameters must be extracted. + * @param parameters List of parameters to update. + * + * @return The same given parameters map (but updated with all found GET parameters). + * + * @since 4.1 + */ + public static final Map addGETParameters(final HttpServletRequest req, final Map parameters){ + String queryString = req.getQueryString(); + if (queryString != null){ + String[] params = queryString.split("&"); + int indSep; + for(String p : params){ + indSep = p.indexOf('='); + if (indSep >= 0){ + try{ + parameters.put(URLDecoder.decode(p.substring(0, indSep), "UTF-8"), URLDecoder.decode(p.substring(indSep + 1), "UTF-8")); + }catch(UnsupportedEncodingException uee){} + } + } + } + return parameters; + } + + /** + * Get the number of parameters submitted in the given HTTP request. + * + * @param request An HTTP request; + * + * @return The number of submitted parameters. + * + * @since 4.1 + */ + @SuppressWarnings("unchecked") + public static final int getNbParameters(final HttpServletRequest request){ + if (request == null) + return 0; + try{ + return ((Map)request.getAttribute(UWS.REQ_ATTRIBUTE_PARAMETERS)).size(); + }catch(Exception ex){ + return request.getParameterMap().size(); + } + } + + /** + * Check whether a parameter has been submitted with the given name. + * + * @param name Name of the parameter to search. The case is important! + * @param request HTTP request in which the specified parameter must be searched. + * @param caseSensitive true to perform the research case-sensitively, + * false for a case INsensitive research. + * + * @return true if the specified parameter has been found, false otherwise. + * + * @since 4.1 + */ + public static final boolean hasParameter(final String name, final HttpServletRequest request, final boolean caseSensitive){ + return getParameter(name, request, caseSensitive) != null; + } + + /** + * Check whether the parameter specified with the given pair (name,value) exists in the given HTTP request. + * + * @param name Name of the parameter to search. + * @param value Expected value of the parameter. + * @param request HTTP request in which the given pair must be searched. + * @param caseSensitive true to perform the research (on name AND value) case-sensitively, + * false for a case INsensitive research. + * + * @return true if the specified parameter has been found with the given value in the given HTTP request, + * false otherwise. + * + * @since 4.1 + */ + public static final boolean hasParameter(final String name, final String value, final HttpServletRequest request, final boolean caseSensitive){ + Object found = getParameter(name, request, caseSensitive); + if (value == null) + return found != null; + else{ + if (found == null || !(found instanceof String)) + return false; + else + return (caseSensitive && ((String)found).equals(value)) || (!caseSensitive && ((String)found).equalsIgnoreCase(value)); + } + } + + /** + * Get the parameter specified by the given name from the given HTTP request. + * + * @param name Name of the parameter to search. + * @param request HTTP request in which the given pair must be searched. + * @param caseSensitive true to perform the research case-sensitively, + * false for a case INsensitive research. + * + * @return Value of the parameter. + * + * @since 4.1 + */ + @SuppressWarnings("unchecked") + public static final Object getParameter(final String name, final HttpServletRequest request, final boolean caseSensitive){ + try{ + // Get the extracted parameters: + Map params = (Map)request.getAttribute(UWS.REQ_ATTRIBUTE_PARAMETERS); + + // Search case IN-sensitively the given pair (name, value): + for(Map.Entry e : params.entrySet()){ + if ((!caseSensitive && e.getKey().equalsIgnoreCase(name)) || (caseSensitive && e.getKey().equals(name))) + return (e.getValue() != null) ? e.getValue() : null; + } + }catch(Exception ex){} + return null; + } + + /** + *

Delete all unused uploaded files of the given request.

+ * + *

+ * These files have been stored on the file system + * if there is a request attribute named {@link UWS#REQ_ATTRIBUTE_PARAMETERS}. + *

+ * + * @param req Request in which files have been uploaded. + * + * @return The number of deleted files. + * + * @see UploadFile#isUsed() + * + * @since 4.1 + */ + @SuppressWarnings("unchecked") + public static final int deleteUploads(final HttpServletRequest req){ + int cnt = 0; + Object attribute = req.getAttribute(UWS.REQ_ATTRIBUTE_PARAMETERS); + // If there is the request attribute "UWS_PARAMETERS": + if (attribute != null && attribute instanceof Map){ + Map params = (Map)attribute; + // For each parameter... + for(Map.Entry e : params.entrySet()){ + // ...delete physically the uploaded file ONLY IF not used AND IF it is an uploaded file: + if (e.getValue() != null && e.getValue() instanceof UploadFile && !((UploadFile)e.getValue()).isUsed()){ + try{ + ((UploadFile)e.getValue()).deleteFile(); + cnt++; + }catch(IOException ioe){} + } + } + } + return cnt; + } + + /* *************** */ + /* USER EXTRACTION */ + /* *************** */ + /** + *

Extract the user/job owner from the given HTTP request.

+ * + * Two cases are supported: + *
    + *
  1. The user has already been identified and is stored in the HTTP attribute {@link UWS#REQ_ATTRIBUTE_USER} => the stored value is returned.
  2. + *
  3. No HTTP attribute and a {@link UserIdentifier} is provided => the user is identified with the given {@link UserIdentifier} and stored in the HTTP attribute {@link UWS#REQ_ATTRIBUTE_USER} before being returned.
  4. + *
+ * + *

In any other case, NULL is returned.

+ * + * @param request The HTTP request from which the user must be extracted. note: if NULL, NULL will be returned. + * @param userIdentifier The method to use in order to extract a user from the given request. note: if NULL, NULL is returned IF no HTTP attribute {@link UWS#REQ_ATTRIBUTE_USER} can be found. + * + * @return The identified user. MAY be NULL + * + * @throws NullPointerException If an error occurs while extracting a {@link UWSUrl} from the given {@link HttpServletRequest}. + * @throws UWSException If any error occurs while extracting a user from the given {@link HttpServletRequest}. + * + * @since 4.1 + */ + public static final JobOwner getUser(final HttpServletRequest request, final UserIdentifier userIdentifier) throws NullPointerException, UWSException{ + if (request == null) + return null; + else if (request.getAttribute(UWS.REQ_ATTRIBUTE_USER) != null) + return (JobOwner)request.getAttribute(UWS.REQ_ATTRIBUTE_USER); + else if (userIdentifier != null){ + JobOwner user = userIdentifier.extractUserId(new UWSUrl(request), request); + request.setAttribute(UWS.REQ_ATTRIBUTE_USER, user); + return user; + }else + return null; + } + /* **************************** */ /* DIRECTORY MANAGEMENT METHODS */ /* **************************** */ @@ -220,6 +468,30 @@ public class UWSToolBox { /* *************************** */ /* RESPONSE MANAGEMENT METHODS */ /* *************************** */ + + /** + *

Flush the buffer of the given {@link PrintWriter}.

+ * + *

+ * This function aims to be used if the given {@link PrintWriter} has been provided by an {@link HttpServletResponse}. + * In such case, a call to its flush() function may generate a silent error which could only mean that + * the connection with the HTTP client has been closed. + *

+ * + * @param writer The writer to flush. + * + * @throws ClientAbortException If the connection with the HTTP client is closed. + * + * @see PrintWriter#flush() + * + * @since 4.1 + */ + public static final void flush(final PrintWriter writer) throws ClientAbortException{ + writer.flush(); + if (writer.checkError()) + throw new ClientAbortException(); + } + /** * Copies the content of the given input stream in the given HTTP response. * @@ -237,6 +509,9 @@ public class UWSToolBox { if (mimeType != null) response.setContentType(mimeType); + // Set the character encoding: + response.setCharacterEncoding(UWSToolBox.DEFAULT_CHAR_ENCODING); + // Set the HTTP content length: if (contentSize > 0) response.setContentLength((int)contentSize); @@ -246,7 +521,7 @@ public class UWSToolBox { byte[] buffer = new byte[1024]; int length; while((length = input.read(buffer)) > 0) - output.print(new String(buffer, 0, length)); + output.write(buffer, 0, length); }finally{ if (output != null) output.flush(); @@ -256,7 +531,7 @@ public class UWSToolBox { /** * Writes the stack trace of the given exception in the file whose the name and the parent directory are given in parameters. * If the specified file already exists, it will be overwritten if the parameter overwrite is equal to true, otherwise - * no file will not be changed (default behavior of {@link UWSToolBox#writeErrorFile(Exception, String, String)}). + * no file will not be changed (default behavior of {@link UWSToolBox#writeErrorFile(Exception, ErrorSummary, UWSJob, OutputStream)}). * * @param ex The exception which has to be used to generate the error file. * @param error The error description. @@ -385,7 +660,7 @@ public class UWSToolBox { /** * Gets the file extension corresponding to the given MIME type. * - * @param MIME type A MIME type (i.e. text/plain, application/json, application/xml, text/xml, application/x-votable+xml, ....) + * @param mimeType A MIME type (i.e. text/plain, application/json, application/xml, text/xml, application/x-votable+xml, ....) * * @return The corresponding file extension or null if not known. */ diff --git a/src/uws/job/ErrorSummary.java b/src/uws/job/ErrorSummary.java index cc2c983..607dbff 100644 --- a/src/uws/job/ErrorSummary.java +++ b/src/uws/job/ErrorSummary.java @@ -16,21 +16,20 @@ package uws.job; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import uws.UWSException; - import uws.job.serializer.UWSSerializer; - import uws.job.user.JobOwner; /** * This class gives a short description of the occurred error (if any) during a job execution. * A fuller representation of the error may be retrieved from {jobs}/(job-id)/error. * - * @author Grégory Mantelet (CDS) - * @version 02/2011 + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (08/2014) */ public class ErrorSummary extends SerializableUWSObject { private static final long serialVersionUID = 1L; @@ -132,7 +131,7 @@ public class ErrorSummary extends SerializableUWSObject { /* INHERITED METHODS */ /* ***************** */ @Override - public String serialize(UWSSerializer serializer, JobOwner owner) throws UWSException{ + public String serialize(UWSSerializer serializer, JobOwner owner) throws UWSException, Exception{ return serializer.getErrorSummary(this, true); } diff --git a/src/uws/job/ErrorType.java b/src/uws/job/ErrorType.java index 7ee038c..7e80d9d 100644 --- a/src/uws/job/ErrorType.java +++ b/src/uws/job/ErrorType.java @@ -16,7 +16,8 @@ package uws.job; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ /** @@ -24,9 +25,14 @@ package uws.job; * * @see ErrorSummary * - * @author Grégory Mantelet (CDS) - * @version 09/2010 + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (09/2014) */ public enum ErrorType{ - FATAL, TRANSIENT + FATAL, TRANSIENT; + + @Override + public String toString(){ + return super.toString().toLowerCase(); + } } diff --git a/src/uws/job/JobList.java b/src/uws/job/JobList.java index 9c10ee5..90e43f4 100644 --- a/src/uws/job/JobList.java +++ b/src/uws/job/JobList.java @@ -16,7 +16,8 @@ package uws.job; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.util.ArrayList; @@ -24,25 +25,22 @@ import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; -import java.lang.IllegalStateException; - import uws.UWSException; import uws.UWSExceptionFactory; import uws.UWSToolBox; - import uws.job.manager.DefaultDestructionManager; import uws.job.manager.DefaultExecutionManager; import uws.job.manager.DestructionManager; import uws.job.manager.ExecutionManager; import uws.job.serializer.UWSSerializer; import uws.job.user.JobOwner; - -import uws.service.UWSService; import uws.service.UWS; +import uws.service.UWSService; import uws.service.UWSUrl; import uws.service.UserIdentifier; import uws.service.backup.UWSBackupManager; import uws.service.log.UWSLog; +import uws.service.log.UWSLog.LogLevel; /** *

General description

@@ -94,12 +92,11 @@ import uws.service.log.UWSLog; *

* *

- * To use a custom destruction manager, you can use the method {@link #setDestructionManager(DestructionManager)} - * if the jobs list is not managed by a UWS or {@link UWSService#setDestructionManager(DestructionManager)} otherwise. + * To use a custom destruction manager, you can use the method {@link #setDestructionManager(DestructionManager)}. *

* - * @author Grégory Mantelet (CDS) - * @version 06/2012 + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (11/2014) * * @see UWSJob */ @@ -132,12 +129,12 @@ public class JobList extends SerializableUWSObject implements Iterable { * * @param jobListName The jobs list name. * - * @throws UWSException If the given name is null or empty. + * @throws NullPointerException If the given job list name is NULL. * * @see #JobList(String, ExecutionManager) */ - public JobList(String jobListName) throws UWSException{ - this(jobListName, new DefaultExecutionManager(), new DefaultDestructionManager()); + public JobList(String jobListName) throws NullPointerException{ + this(jobListName, null, new DefaultDestructionManager()); } /** @@ -145,10 +142,10 @@ public class JobList extends SerializableUWSObject implements Iterable { * * @param jobListName The jobs list name. * @param executionManager The object which will manage the execution of all jobs of this list. - * - * @throws UWSException If the given name is null or empty, or if the given execution manager is null. + * + * @throws NullPointerException If the given job list name is NULL or empty or if no execution manager is provided. */ - public JobList(String jobListName, ExecutionManager executionManager) throws UWSException{ + public JobList(String jobListName, ExecutionManager executionManager) throws NullPointerException{ this(jobListName, executionManager, new DefaultDestructionManager()); } @@ -157,10 +154,10 @@ public class JobList extends SerializableUWSObject implements Iterable { * * @param jobListName The jobs list name. * @param destructionManager The object which manages the automatic destruction of jobs when they have reached their destruction date. - * - * @throws UWSException If the given name is null or empty, or if the given destruction manager is null. + * + * @throws NullPointerException If the given job list name is NULL or empty or if no destruction manager is provided. */ - public JobList(String jobListName, DestructionManager destructionManager) throws UWSException{ + public JobList(String jobListName, DestructionManager destructionManager) throws NullPointerException{ this(jobListName, new DefaultExecutionManager(), destructionManager); } @@ -170,29 +167,26 @@ public class JobList extends SerializableUWSObject implements Iterable { * @param jobListName The jobs list name. * @param executionManager The object which will manage the execution of all jobs of this list. * @param destructionManager The object which manages the automatic destruction of jobs when they have reached their destruction date. - * - * @throws UWSException If the given name is null or empty, or if the given execution or destruction manager is null. + * + * @throws NullPointerException If the given job list name is NULL or empty or if no execution manager and destruction manager are provided. */ - public JobList(String jobListName, ExecutionManager executionManager, DestructionManager destructionManager) throws UWSException{ + public JobList(String jobListName, ExecutionManager executionManager, DestructionManager destructionManager) throws NullPointerException{ if (jobListName == null) - throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, "Missing job list name ! => Impossible to build the job list."); + throw new NullPointerException("Missing job list name ! => Impossible to build the job list."); else{ jobListName = jobListName.trim(); if (jobListName.length() == 0) - throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, "Missing job list name ! => Impossible to build the job list."); + throw new NullPointerException("Missing job list name ! => Impossible to build the job list."); } name = jobListName; jobsList = new LinkedHashMap(); ownerJobs = new LinkedHashMap>(); - if (executionManager == null) - throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, "Missing execution manager ! => Impossible to build the job list."); - else - this.executionManager = executionManager; + this.executionManager = executionManager; if (destructionManager == null) - throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, "Missing destruction manager ! => Impossible to build the job list."); + throw new NullPointerException("Missing destruction manager ! => Impossible to build the job list."); else this.destructionManager = destructionManager; } @@ -236,7 +230,7 @@ public class JobList extends SerializableUWSObject implements Iterable { * @return A logger. * * @see #getUWS() - * @see UWS#getLogger(); + * @see UWS#getLogger() * @see UWSToolBox#getDefaultLogger() */ public UWSLog getLogger(){ @@ -289,6 +283,12 @@ public class JobList extends SerializableUWSObject implements Iterable { * @return The used execution manager. */ public final ExecutionManager getExecutionManager(){ + if (executionManager == null){ + if (uws == null) + executionManager = new DefaultExecutionManager(); + else + executionManager = new DefaultExecutionManager(uws.getLogger()); + } return executionManager; } @@ -302,17 +302,19 @@ public class JobList extends SerializableUWSObject implements Iterable { * @see ExecutionManager#remove(UWSJob) * @see ExecutionManager#execute(UWSJob) */ - public synchronized final void setExecutionManager(final ExecutionManager manager) throws UWSException{ + public synchronized final void setExecutionManager(final ExecutionManager manager){ if (manager == null) return; ExecutionManager oldManager = executionManager; executionManager = manager; - for(UWSJob job : this){ - if (job.getPhase() != ExecutionPhase.PENDING && !job.isFinished()){ - oldManager.remove(job); - executionManager.execute(job); + if (oldManager != null){ + for(UWSJob job : this){ + if (job.getPhase() != ExecutionPhase.PENDING && !job.isFinished()){ + oldManager.remove(job); + executionManager.execute(job); + } } } } @@ -356,7 +358,7 @@ public class JobList extends SerializableUWSObject implements Iterable { * Gets the job whose the ID is given in parameter ONLY IF it is the one of the specified user OR IF the specified job is owned by an anonymous user. * * @param jobID ID of the job to get. - * @param userID ID of the user who asks this job (null means no particular owner => cf {@link #getJob(String)}). + * @param user The user who asks this job (null means no particular owner => cf {@link #getJob(String)}). * * @return The requested job or null if there is no job with the given ID or if the user is not allowed to get the given job. * @@ -364,7 +366,7 @@ public class JobList extends SerializableUWSObject implements Iterable { */ public UWSJob getJob(String jobID, JobOwner user) throws UWSException{ if (user != null && !user.hasReadPermission(this)) - throw UWSExceptionFactory.readPermissionDenied(user, true, getName()); + throw new UWSException(UWSException.PERMISSION_DENIED, UWSExceptionFactory.readPermissionDenied(user, true, getName())); // Get the specified job: UWSJob job = jobsList.get(jobID); @@ -373,7 +375,7 @@ public class JobList extends SerializableUWSObject implements Iterable { if (user != null && job != null && job.getOwner() != null){ JobOwner owner = job.getOwner(); if (!owner.equals(user) && !user.hasReadPermission(job)) - throw UWSExceptionFactory.readPermissionDenied(user, false, job.getJobId()); + throw new UWSException(UWSException.PERMISSION_DENIED, UWSExceptionFactory.readPermissionDenied(user, false, job.getJobId())); } return job; @@ -393,7 +395,7 @@ public class JobList extends SerializableUWSObject implements Iterable { /** * Gets an iterator on the jobs list of the specified user. * - * @param ownerId The ID of the owner/user (may be null). + * @param user The owner/user who asks for this operation (may be null). * * @return An iterator on all jobs which have been created by the specified owner/user * or a NullIterator if the specified owner/user has no job @@ -407,14 +409,17 @@ public class JobList extends SerializableUWSObject implements Iterable { return ownerJobs.get(user).values().iterator(); else return new Iterator(){ + @Override public boolean hasNext(){ return false; } + @Override public UWSJob next(){ return null; } + @Override public void remove(){ ; } @@ -427,6 +432,7 @@ public class JobList extends SerializableUWSObject implements Iterable { * * @see java.lang.Iterable#iterator() */ + @Override public final Iterator iterator(){ return jobsList.values().iterator(); } @@ -510,7 +516,6 @@ public class JobList extends SerializableUWSObject implements Iterable { * * @throws UWSException If the owner of the given job is not allowed to add any job into this jobs list. * - * @see UWSJob#loadAdditionalParams() * @see UWSJob#setJobList(JobList) * @see UWSService#getBackupManager() * @see UWSBackupManager#saveOwner(JobOwner) @@ -518,46 +523,44 @@ public class JobList extends SerializableUWSObject implements Iterable { * @see UWSJob#applyPhaseParam(JobOwner) */ public synchronized String addNewJob(final UWSJob j) throws UWSException{ - if (j == null || jobsList.containsKey(j.getJobId())){ + if (uws == null) + throw new IllegalStateException("Jobs can not be added to this job list until this job list is linked to a UWS!"); + else if (j == null || jobsList.containsKey(j.getJobId())){ return null; }else{ JobOwner owner = j.getOwner(); // Check the WRITE permission of the owner of this job: if (owner != null && !owner.hasWritePermission(this)) - throw UWSExceptionFactory.writePermissionDenied(owner, true, getName()); - - try{ - // Set its job list: - j.setJobList(this); - - // Add the job to the jobs list: - jobsList.put(j.getJobId(), j); - if (owner != null){ - // Index also this job in function of its owner: - if (!ownerJobs.containsKey(owner)) - ownerJobs.put(owner, new LinkedHashMap()); - ownerJobs.get(owner).put(j.getJobId(), j); - } + throw new UWSException(UWSException.PERMISSION_DENIED, UWSExceptionFactory.writePermissionDenied(owner, true, getName())); + + // Set its job list: + j.setJobList(this); + + // Add the job to the jobs list: + jobsList.put(j.getJobId(), j); + if (owner != null){ + // Index also this job in function of its owner: + if (!ownerJobs.containsKey(owner)) + ownerJobs.put(owner, new LinkedHashMap()); + ownerJobs.get(owner).put(j.getJobId(), j); + } - // Save the owner jobs list: - if (owner != null && uws.getBackupManager() != null && j.getRestorationDate() == null) - uws.getBackupManager().saveOwner(j.getOwner()); + // Save the owner jobs list: + if (owner != null && uws.getBackupManager() != null && j.getRestorationDate() == null) + uws.getBackupManager().saveOwner(j.getOwner()); - // Add it to the destruction manager: - destructionManager.update(j); + // Add it to the destruction manager: + destructionManager.update(j); - // Execute the job if asked in the additional parameters: - j.applyPhaseParam(null); + // Execute the job if asked in the additional parameters: + j.applyPhaseParam(null); // Note: can not throw an exception since no user is specified (so, no permission check is done). - // Log the "creation" of the job: - if (j.getRestorationDate() == null) - getLogger().jobCreated(j); + // Log the "creation" of the job: + if (j.getRestorationDate() == null) + getLogger().logJob(LogLevel.INFO, j, "CREATED", "Job \"" + j.getJobId() + "\" successfully created and added in the job list \"" + getName() + "\".", null); - return j.getJobId(); - }catch(IllegalStateException e){ - throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, e, "Impossible to add the job " + j.getJobId() + " into the jobs list " + getName() + " !"); - } + return j.getJobId(); } } @@ -589,13 +592,6 @@ public class JobList extends SerializableUWSObject implements Iterable { UWSJob removedJob = (jobId == null) ? null : jobsList.remove(jobId); if (removedJob != null){ - // Delete completely their association: - /*try{ - removedJob.setJobList(null); - }catch(IllegalStateException ue){ - getLogger().error("Impossible to set the job list of the removed job to NULL !", ue); - }*/ - // Clear its owner index: JobOwner owner = removedJob.getOwner(); if (owner != null && ownerJobs.containsKey(owner)){ @@ -638,7 +634,7 @@ public class JobList extends SerializableUWSObject implements Iterable { uws.getBackupManager().saveOwner(destroyedJob.getOwner()); // Log this job destruction: - getLogger().jobDestroyed(destroyedJob, this); + getLogger().logJob(LogLevel.INFO, destroyedJob, "DESTROY", "The job \"" + destroyedJob.getJobId() + "\" has been removed from the job list \"" + name + "\".", null); return true; } @@ -660,10 +656,10 @@ public class JobList extends SerializableUWSObject implements Iterable { public boolean destroyJob(final String jobId, final JobOwner user) throws UWSException{ if (user != null){ if (!user.hasWritePermission(this)) - throw UWSExceptionFactory.writePermissionDenied(user, true, getName()); + throw new UWSException(UWSException.PERMISSION_DENIED, UWSExceptionFactory.writePermissionDenied(user, true, getName())); UWSJob job = getJob(jobId); if (job != null && job.getOwner() != null && !user.equals(job.getOwner()) && !user.hasWritePermission(job)) - throw UWSExceptionFactory.writePermissionDenied(user, false, job.getJobId()); + throw new UWSException(UWSException.PERMISSION_DENIED, UWSExceptionFactory.writePermissionDenied(user, false, jobId)); } return destroyJob(jobId); } @@ -693,7 +689,7 @@ public class JobList extends SerializableUWSObject implements Iterable { /** * Destroys all jobs owned by the specified user. * - * @param ownerId The ID of the owner/user. + * @param owner The owner/user who asks for this operation. * * @throws UWSException If the given user is not allowed to update of the content of this jobs list. * @@ -704,7 +700,7 @@ public class JobList extends SerializableUWSObject implements Iterable { if (owner == null) clear(); else if (!owner.hasWritePermission(this)) - throw UWSExceptionFactory.writePermissionDenied(owner, true, getName()); + throw new UWSException(UWSException.PERMISSION_DENIED, UWSExceptionFactory.writePermissionDenied(owner, true, getName())); else{ if (ownerJobs.containsKey(owner)){ ArrayList jobIDs = new ArrayList(ownerJobs.get(owner).keySet()); @@ -719,9 +715,9 @@ public class JobList extends SerializableUWSObject implements Iterable { /* INHERITED METHODS */ /* ***************** */ @Override - public String serialize(UWSSerializer serializer, JobOwner user) throws UWSException{ + public String serialize(UWSSerializer serializer, JobOwner user) throws UWSException, Exception{ if (user != null && !user.hasReadPermission(this)) - throw UWSExceptionFactory.readPermissionDenied(user, true, getName()); + throw new UWSException(UWSException.PERMISSION_DENIED, UWSExceptionFactory.writePermissionDenied(user, true, getName())); return serializer.getJobList(this, user, true); } diff --git a/src/uws/job/JobPhase.java b/src/uws/job/JobPhase.java index cf0a270..1de05bb 100644 --- a/src/uws/job/JobPhase.java +++ b/src/uws/job/JobPhase.java @@ -16,7 +16,8 @@ package uws.job; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.io.Serializable; @@ -28,8 +29,8 @@ import uws.UWSExceptionFactory; * An instance of this class represents the current execution phase of a given job, * and it describes the transitions between the different phases. * - * @author Grégory Mantelet (CDS) - * @version 05/2012 + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (08/2014) * * @see ExecutionPhase * @see UWSJob @@ -48,11 +49,11 @@ public class JobPhase implements Serializable { * * @param j The job whose the execution phase must be represented by the built JobPhase instance. * - * @throws UWSException If the given job is null. + * @throws NullPointerException If the given job is null. */ - public JobPhase(UWSJob j) throws UWSException{ + public JobPhase(UWSJob j) throws NullPointerException{ if (j == null) - throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, "Missing job instance ! => impossible to build a JobPhase instance."); + throw new NullPointerException("Missing job instance ! => impossible to build a JobPhase instance."); job = j; } @@ -88,12 +89,16 @@ public class JobPhase implements Serializable { } /** - * Lets changing the current phase of the associated job considering or not the order of execution phases. + *

Lets changing the current phase of the associated job considering or not the order of execution phases.

+ * + *

Note: + * If the given phase is null, nothing is done. + *

* * @param p The new phase. * @param force true to ignore the phases order, false otherwise. * - * @throws UWSException If the given phase is null or if the phase transition is forbidden. + * @throws UWSException If the phase transition is forbidden. * * @see #setPendingPhase(boolean) * @see #setQueuedPhase(boolean) @@ -107,7 +112,7 @@ public class JobPhase implements Serializable { */ public void setPhase(ExecutionPhase p, boolean force) throws UWSException{ if (p == null) - throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, "Incorrect phase ! => The phase of a job can not be set to NULL !"); + return; // Check that the given phase follows the imposed phases order: switch(p){ @@ -151,7 +156,7 @@ public class JobPhase implements Serializable { */ protected void setPendingPhase(boolean force) throws UWSException{ if (!force && phase != ExecutionPhase.PENDING && phase != ExecutionPhase.UNKNOWN) - throw UWSExceptionFactory.incorrectPhaseTransition(job.getJobId(), phase, ExecutionPhase.PENDING); + throw new UWSException(UWSException.BAD_REQUEST, UWSExceptionFactory.incorrectPhaseTransition(job.getJobId(), phase, ExecutionPhase.PENDING)); phase = ExecutionPhase.PENDING; } @@ -168,7 +173,7 @@ public class JobPhase implements Serializable { phase = ExecutionPhase.QUEUED; else{ if (phase != ExecutionPhase.QUEUED && phase != ExecutionPhase.HELD && phase != ExecutionPhase.PENDING && phase != ExecutionPhase.UNKNOWN) - throw UWSExceptionFactory.incorrectPhaseTransition(job.getJobId(), phase, ExecutionPhase.QUEUED); + throw new UWSException(UWSException.BAD_REQUEST, UWSExceptionFactory.incorrectPhaseTransition(job.getJobId(), phase, ExecutionPhase.QUEUED)); phase = ExecutionPhase.QUEUED; } @@ -186,7 +191,7 @@ public class JobPhase implements Serializable { phase = ExecutionPhase.EXECUTING; else{ if (phase != ExecutionPhase.EXECUTING && phase != ExecutionPhase.SUSPENDED && phase != ExecutionPhase.PENDING && phase != ExecutionPhase.QUEUED && phase != ExecutionPhase.UNKNOWN) - throw UWSExceptionFactory.incorrectPhaseTransition(job.getJobId(), phase, ExecutionPhase.EXECUTING); + throw new UWSException(UWSException.BAD_REQUEST, UWSExceptionFactory.incorrectPhaseTransition(job.getJobId(), phase, ExecutionPhase.EXECUTING)); phase = ExecutionPhase.EXECUTING; } @@ -204,7 +209,7 @@ public class JobPhase implements Serializable { phase = ExecutionPhase.COMPLETED; else{ if (phase != ExecutionPhase.COMPLETED && phase != ExecutionPhase.EXECUTING && phase != ExecutionPhase.UNKNOWN) - throw UWSExceptionFactory.incorrectPhaseTransition(job.getJobId(), phase, ExecutionPhase.COMPLETED); + throw new UWSException(UWSException.BAD_REQUEST, UWSExceptionFactory.incorrectPhaseTransition(job.getJobId(), phase, ExecutionPhase.COMPLETED)); phase = ExecutionPhase.COMPLETED; } @@ -222,7 +227,7 @@ public class JobPhase implements Serializable { phase = ExecutionPhase.ABORTED; else{ if (phase == ExecutionPhase.COMPLETED || phase == ExecutionPhase.ERROR) - throw UWSExceptionFactory.incorrectPhaseTransition(job.getJobId(), phase, ExecutionPhase.ABORTED); + throw new UWSException(UWSException.BAD_REQUEST, UWSExceptionFactory.incorrectPhaseTransition(job.getJobId(), phase, ExecutionPhase.ABORTED)); phase = ExecutionPhase.ABORTED; } @@ -240,7 +245,7 @@ public class JobPhase implements Serializable { phase = ExecutionPhase.ERROR; else{ if (phase == ExecutionPhase.COMPLETED || phase == ExecutionPhase.ABORTED) - throw UWSExceptionFactory.incorrectPhaseTransition(job.getJobId(), phase, ExecutionPhase.ERROR); + throw new UWSException(UWSException.BAD_REQUEST, UWSExceptionFactory.incorrectPhaseTransition(job.getJobId(), phase, ExecutionPhase.ERROR)); phase = ExecutionPhase.ERROR; } @@ -255,7 +260,7 @@ public class JobPhase implements Serializable { */ protected void setHeldPhase(boolean force) throws UWSException{ if (!force && phase != ExecutionPhase.HELD && phase != ExecutionPhase.PENDING && phase != ExecutionPhase.UNKNOWN) - throw UWSExceptionFactory.incorrectPhaseTransition(job.getJobId(), phase, ExecutionPhase.HELD); + throw new UWSException(UWSException.BAD_REQUEST, UWSExceptionFactory.incorrectPhaseTransition(job.getJobId(), phase, ExecutionPhase.HELD)); phase = ExecutionPhase.HELD; } diff --git a/src/uws/job/JobThread.java b/src/uws/job/JobThread.java index 0b80bd6..28e9dcf 100644 --- a/src/uws/job/JobThread.java +++ b/src/uws/job/JobThread.java @@ -16,7 +16,8 @@ package uws.job; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.io.IOException; @@ -25,7 +26,10 @@ import java.util.Date; import uws.UWSException; import uws.UWSToolBox; +import uws.service.error.ServiceErrorWriter; import uws.service.file.UWSFileManager; +import uws.service.log.UWSLog; +import uws.service.log.UWSLog.LogLevel; /** *

An instance of this class is a thread dedicated to a job execution.

@@ -55,8 +59,8 @@ import uws.service.file.UWSFileManager; *
  • an {@link InterruptedException}: the method {@link UWSJob#abort()} is called.
  • * * - * @author Grégory Mantelet (CDS) - * @version 05/2012 + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (04/2015) * * @see UWSJob#start() * @see UWSJob#abort() @@ -71,44 +75,91 @@ public abstract class JobThread extends Thread { /** The last error which has occurred during the execution of this thread. */ protected UWSException lastError = null; - /** Indicates whether the {@link UWSJob#jobWork()} has been called and finished, or not. */ + /** Indicate whether the exception stored in the attribute {@link #lastError} should be considered as a grave error or not. + * By default, {@link #lastError} is a "normal" error. + * @since 4.1 */ + protected boolean fatalError = false; + + /** Indicates whether the {@link #jobWork()} has been called and finished, or not. */ protected boolean finished = false; /** Description of what is done by this thread. */ protected final String taskDescription; + /** + * Object to use in order to write the content of an error/exception in any output stream. + * If NULL, the content will be written by {@link UWSToolBox#writeErrorFile(Exception, ErrorSummary, UWSJob, OutputStream)} + * (in text/plain with stack-trace). + * Otherwise the content and the MIME type are determined by the error writer. + * @since 4.1 + */ + protected final ServiceErrorWriter errorWriter; + + /** Group of threads in which this job thread will run. */ public final static ThreadGroup tg = new ThreadGroup("UWS_GROUP"); /** * Builds the JobThread instance which will be used by the given job to execute its task. * * @param j The associated job. - * @param fileManager An object to get access to UWS files (particularly: error and results file). * - * @throws UWSException If the given job or the given file manager is null. + * @throws NullPointerException If the given job or the given file manager is null. * * @see #getDefaultTaskDescription(UWSJob) */ - public JobThread(UWSJob j) throws UWSException{ - this(j, getDefaultTaskDescription(j)); + public JobThread(final UWSJob j) throws NullPointerException{ + this(j, getDefaultTaskDescription(j), null); + } + + /** + * Builds the JobThread instance which will be used by the given job to execute its task. + * + * @param j The associated job. + * @param errorWriter Object to use in case of error in order to format the details of the error for the .../error/details parameter. + * + * @throws NullPointerException If the given job is null. + * + * @see #getDefaultTaskDescription(UWSJob) + * + * @since 4.1 + */ + public JobThread(final UWSJob j, final ServiceErrorWriter errorWriter) throws NullPointerException{ + this(j, getDefaultTaskDescription(j), errorWriter); + } + + /** + * Builds the JobThread instance which will be used by the given job to execute its task. + * + * @param j The associated job. + * @param task Description of the task executed by this thread. + * + * @throws NullPointerException If the given job is null. + */ + public JobThread(final UWSJob j, final String task) throws NullPointerException{ + super(tg, j.getJobId()); + + job = j; + taskDescription = task; + errorWriter = null; } /** * Builds the JobThread instance which will be used by the given job to execute its task. * * @param j The associated job. - * @param fileManager An object to get access to UWS files (particularly: error and results file). * @param task Description of the task executed by this thread. + * @param errorWriter Object to use in case of error in order to format the details of the error for the .../error/details parameter. + * + * @throws NullPointerException If the given job is null. * - * @throws UWSException If the given job or the given file manager is null. + * @since 4.1 */ - public JobThread(UWSJob j, String task) throws UWSException{ + public JobThread(final UWSJob j, final String task, final ServiceErrorWriter errorWriter) throws NullPointerException{ super(tg, j.getJobId()); - if (j == null) - throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, "Missing job instance ! => impossible to build a JobThread instance."); job = j; taskDescription = task; + this.errorWriter = errorWriter; } /** @@ -158,7 +209,7 @@ public abstract class JobThread extends Thread { } /** - * Indicates whether the {@link UWSJob#jobWork()} method has been called or not. + * Indicates whether the {@link #jobWork()} method has been called or not. * * @return true if the job work is done, false otherwise. */ @@ -189,7 +240,7 @@ public abstract class JobThread extends Thread { * * @throws UWSException If there is an error while publishing the error. * - * @see {@link UWSJob#error(ErrorSummary)} + * @see UWSJob#error(ErrorSummary) */ public void setError(final ErrorSummary error) throws UWSException{ job.error(error); @@ -210,22 +261,33 @@ public abstract class JobThread extends Thread { * * @throws UWSException If there is an error while publishing the given exception. * - * {@link UWSToolBox#writeErrorFile(Exception, ErrorSummary, UWSJob, OutputStream)} + * @see #setError(ErrorSummary) + * @see UWSToolBox#writeErrorFile(Exception, ErrorSummary, UWSJob, OutputStream) */ public void setError(final UWSException ue) throws UWSException{ if (ue == null) return; try{ + // Set the error summary: ErrorSummary error = new ErrorSummary(ue, ue.getUWSErrorType(), job.getUrl() + "/" + UWSJob.PARAM_ERROR_SUMMARY + "/details"); + + // Prepare the output stream: OutputStream output = getFileManager().getErrorOutput(error, job); - UWSToolBox.writeErrorFile(ue, error, job, output); + // Format and write the error... + // ...using the error writer, if any: + if (errorWriter != null) + errorWriter.writeError(ue, error, job, output); + // ...or write a default output: + else + UWSToolBox.writeErrorFile(ue, error, job, output); + // Set the error summary inside the job: setError(error); }catch(IOException ioe){ - job.getLogger().error("The stack trace of a UWSException (job ID: " + job.getJobId() + " ; error message: \"" + ue.getMessage() + "\") had not been written !", ioe); + job.getLogger().logThread(LogLevel.ERROR, this, "SET_ERROR", "The stack trace of a UWSException had not been written!", ioe); setError(new ErrorSummary(ue.getMessage(), ue.getUWSErrorType())); } } @@ -233,8 +295,6 @@ public abstract class JobThread extends Thread { /** * Creates a default result description. * - * @param job The job which will contains this result. - * * @return The created result. * * @see #createResult(String) @@ -256,7 +316,6 @@ public abstract class JobThread extends Thread { /** * Creates a default result description but by precising its name/ID. * - * @param job The job which will contains this result. * @param name The name/ID of the result to create. * * @return The created result. @@ -305,7 +364,7 @@ public abstract class JobThread extends Thread { * * @throws IOException If there is an error while getting the result file size. * - * @see {@link UWSFileManager#getResultSize(Result, UWSJob)} + * @see UWSFileManager#getResultSize(Result, UWSJob) */ public long getResultSize(final Result result) throws IOException{ return getFileManager().getResultSize(result, job); @@ -318,14 +377,14 @@ public abstract class JobThread extends Thread { *
      *
    • This method does the job work but it MUST also fill the associated job with the execution results and/or errors.
    • *
    • Do not forget to check the interrupted flag of the thread ({@link Thread#isInterrupted()}) and then to send an {@link InterruptedException}. - * Otherwise the {@link UWSJob#stop()} method will have no effect, as for {@link #abort()} and {@link #error(ErrorSummary)}.
    • + * Otherwise the {@link UWSJob#stop()} method will have no effect, as for {@link UWSJob#abort()} and {@link #setError(ErrorSummary)}. *

    * *

    Notes: *

      *
    • The "setPhase(COMPLETED)" and the "endTime=new Date()" are automatically applied just after the call to jobWork.
    • - *
    • If an {@link UWSException} is thrown the {@link JobThread} will automatically publish the exception in this job - * thanks to the {@link UWSJob#error(UWSException)} method or the {@link #setErrorSummary(ErrorSummary)} method, + *
    • If a {@link UWSException} is thrown the {@link JobThread} will automatically publish the exception in this job + * thanks to the {@link UWSJob#error(ErrorSummary)} method or the {@link UWSJob#setErrorSummary(ErrorSummary)} method, * and so it will set its phase to {@link ExecutionPhase#ERROR}.
    • *
    • If an {@link InterruptedException} is thrown the {@link JobThread} will automatically set the phase to {@link ExecutionPhase#ABORTED}
    • *

    @@ -338,19 +397,19 @@ public abstract class JobThread extends Thread { /** *
      *
    1. Tests the execution phase of the job: if not {@link ExecutionPhase#EXECUTING EXECUTING}, nothing is done...the thread ends immediately.
    2. - *
    3. Calls the {@link UWSJob#jobWork()} method.
    4. + *
    5. Calls the {@link #jobWork()} method.
    6. *
    7. Sets the finished flag to true.
    8. *
    9. Changes the job phase to {@link ExecutionPhase#COMPLETED COMPLETED} if not interrupted, else {@link ExecutionPhase#ABORTED ABORTED}. *
    *

    If any {@link InterruptedException} occurs the job phase is only set to {@link ExecutionPhase#ABORTED ABORTED}.

    *

    If any {@link UWSException} occurs while the phase is {@link ExecutionPhase#EXECUTING EXECUTING} the job phase * is set to {@link ExecutionPhase#ERROR ERROR} and an error summary is created.

    - *

    Whatever is the exception, it will always be available thanks to the {@link JobThread#getError()} after execution.

    + *

    Whatever is the exception, it will always be available thanks to the {@link #getError()} after execution.

    * - * @see UWSJob#jobWork() + * @see #jobWork() * @see UWSJob#setPhase(ExecutionPhase) - * @see UWSJob#publishExecutionError(UWSException) - * @see UWSToolBox#publishErrorSummary(UWSJob, String, ErrorType) + * @see #setError(UWSException) + * @see #setError(ErrorSummary) */ @Override public final void run(){ @@ -361,63 +420,79 @@ public abstract class JobThread extends Thread { finished = false; } + UWSLog logger = job.getLogger(); + // Log the start of this thread: - job.getLogger().threadStarted(this, taskDescription); + logger.logThread(LogLevel.INFO, this, "START", "Thread \"" + getName() + "\" started.", null); try{ - try{ - // Execute the task: - jobWork(); - - // Change the phase to COMPLETED: - finished = true; - complete(); - }catch(InterruptedException ex){ - // Abort: - finished = true; - if (!job.stopping) + // Execute the task: + jobWork(); + + // Change the phase to COMPLETED: + finished = true; + complete(); + logger.logThread(LogLevel.INFO, this, "END", "Thread \"" + getName() + "\" successfully ended.", null); + + }catch(InterruptedException ex){ + /* CASE: ABORTION + * In case of abortion, the thread just stops normally, just logging an INFO saying that the thread has been cancelled. + * Since it is not an abnormal behavior there is no reason to keep a trace of the interrupted exception. */ + finished = true; + // Abort: + if (!job.stopping){ + try{ job.abort(); - // Log the abortion: - job.getLogger().threadInterrupted(this, taskDescription, ex); + }catch(UWSException ue){ + /* Should not happen since the reason of a such exception would be that the thread can not be stopped... + * ...but we are already in the thread and it is stopping. */ + logger.logJob(LogLevel.WARNING, job, "ABORT", "Can not put the job in its ABORTED phase!", ue); + } } - return; + // Log the abortion: + logger.logThread(LogLevel.INFO, this, "END", "Thread \"" + getName() + "\" cancelled.", null); }catch(UWSException ue){ - // Save the error: + /* CASE: ERROR for a known reason + * A such error is just a "normal" error, in the sense its cause is known and in a way supported or expected in + * a special configuration or parameters. Thus, the error is kept and will logged with a stack trace afterwards.*/ lastError = ue; }catch(Throwable t){ - // Build the error: - if (t.getMessage() == null || t.getMessage().trim().isEmpty()) - lastError = new UWSException(UWSException.INTERNAL_SERVER_ERROR, t.getClass().getName(), ErrorType.FATAL); + /* DEFAULT: FATAL error + * Any other error is considered as FATAL because it was not expected or supported at a given point. + * It is generally a bug or a forgiven thing in the code of the library. As for "normal" errors, this error + * is kept and will logged with stack trace afterwards. */ + fatalError = true; + if (t instanceof Error) + lastError = new UWSException(UWSException.INTERNAL_SERVER_ERROR, t, "A FATAL DEEP ERROR OCCURED WHILE EXECUTING THIS QUERY! This error is reported in the service logs.", ErrorType.FATAL); + else if (t.getMessage() == null || t.getMessage().trim().isEmpty()) + lastError = new UWSException(UWSException.INTERNAL_SERVER_ERROR, t, t.getClass().getName(), ErrorType.FATAL); else lastError = new UWSException(UWSException.INTERNAL_SERVER_ERROR, t, ErrorType.FATAL); }finally{ finished = true; - // Publish the error if any has occurred: + /* PUBLISH THE ERROR if any has occurred */ if (lastError != null){ // Log the error: - job.getLogger().threadInterrupted(this, taskDescription, lastError); + LogLevel logLevel = fatalError ? LogLevel.FATAL : LogLevel.ERROR; + logger.logJob(logLevel, job, "END", "The following " + (fatalError ? "GRAVE" : "") + " error interrupted the execution of the job " + job.getJobId() + ".", lastError); + logger.logThread(logLevel, this, "END", "Thread \"" + getName() + "\" ended with an error.", null); // Set the error into the job: try{ setError(lastError); }catch(UWSException ue){ try{ - job.getLogger().error("[JobThread] LEVEL 1 -> Problem in JobThread.setError(UWSException), while setting the execution error of the job " + job.getJobId(), ue); + logger.logThread(logLevel, this, "SET_ERROR", "[1st Attempt] Problem in JobThread.setError(UWSException), while setting the execution error of the job " + job.getJobId() + ". A last attempt will be done.", ue); setError(new ErrorSummary((lastError.getCause() != null) ? lastError.getCause().getMessage() : lastError.getMessage(), lastError.getUWSErrorType())); }catch(UWSException ue2){ - job.getLogger().error("[JobThread] LEVEL 2 -> Problem in JobThread.setError(ErrorSummary), while setting the execution error of the job " + job.getJobId(), ue2); - try{ - setError(new ErrorSummary(lastError.getMessage(), ErrorType.FATAL)); - }catch(UWSException ue3){ - job.getLogger().error("[JobThread] LEVEL 3 -> Problem in JobThread.setError(ErrorSummary), while setting the execution error of the job " + job.getJobId(), ue3); - } + logger.logThread(logLevel, this, "SET_ERROR", "[2nd and last Attempt] Problem in JobThread.setError(ErrorSummary), while setting the execution error of the job " + job.getJobId() + ". This error can not be reported to the user, but it will be reported in the log in the JOB context.", ue2); + // Note: no need of a level 3: if the second attempt fails, it means the job is in a wrong phase and no error summary can never be set ; further attempt won't change anything! } } - }else - job.getLogger().threadFinished(this, taskDescription); + } } } } diff --git a/src/uws/job/Result.java b/src/uws/job/Result.java index 1361bac..fa45a75 100644 --- a/src/uws/job/Result.java +++ b/src/uws/job/Result.java @@ -16,21 +16,20 @@ package uws.job; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import uws.UWSException; - import uws.job.serializer.UWSSerializer; import uws.job.user.JobOwner; - import uws.service.UWSUrl; /** * This class gives a short description (mainly an ID and a URL) of a job result. * - * @author Grégory Mantelet (CDS) - * @version 06/2012 + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (08/2014) */ public class Result extends SerializableUWSObject { private static final long serialVersionUID = 1L; @@ -245,9 +244,7 @@ public class Result extends SerializableUWSObject { } /** - * Sets the size of the corresponding result file. - * - * @return size Result file size (in bytes). + * Sets the size (in bytes) of the corresponding result file. */ public final void setSize(long size){ this.size = size; @@ -257,7 +254,7 @@ public class Result extends SerializableUWSObject { /* INHERITED METHODS */ /* ***************** */ @Override - public String serialize(UWSSerializer serializer, JobOwner owner) throws UWSException{ + public String serialize(UWSSerializer serializer, JobOwner owner) throws UWSException, Exception{ return serializer.getResult(this, true); } diff --git a/src/uws/job/SerializableUWSObject.java b/src/uws/job/SerializableUWSObject.java index 3fc7c49..878d9ce 100644 --- a/src/uws/job/SerializableUWSObject.java +++ b/src/uws/job/SerializableUWSObject.java @@ -16,7 +16,8 @@ package uws.job; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.io.IOException; @@ -25,7 +26,6 @@ import java.io.Serializable; import javax.servlet.ServletOutputStream; import uws.UWSException; -import uws.UWSExceptionFactory; import uws.job.serializer.UWSSerializer; import uws.job.serializer.XMLSerializer; import uws.job.user.JobOwner; @@ -36,8 +36,8 @@ import uws.job.user.JobOwner; *

    The {@link SerializableUWSObject#serialize(UWSSerializer, JobOwner)} method must be implemented. It is the most important method of this class * because it returns a serialized representation of this UWS object.

    * - * @author Grégory Mantelet (CDS) - * @version 01/2012 + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (08/2014) */ public abstract class SerializableUWSObject implements Serializable { private static final long serialVersionUID = 1L; @@ -49,11 +49,11 @@ public abstract class SerializableUWSObject implements Serializable { * * @return The serialized representation of this object. * - * @throws UWSException If there is an error during the serialization. + * @throws Exception If there is an unexpected error during the serialization. * - * @see #serialize(UWSSerializer, String) + * @see #serialize(UWSSerializer, JobOwner) */ - public String serialize(UWSSerializer serializer) throws UWSException{ + public String serialize(UWSSerializer serializer) throws Exception{ return serialize(serializer, null); } @@ -66,9 +66,10 @@ public abstract class SerializableUWSObject implements Serializable { * * @return The serialized representation of this object. * - * @throws UWSException If there is an error during the serialization. + * @throws UWSException If the owner is not allowed to see the content of the serializable object. + * @throws Exception If there is any other error during the serialization. */ - public abstract String serialize(UWSSerializer serializer, JobOwner owner) throws UWSException; + public abstract String serialize(UWSSerializer serializer, JobOwner owner) throws UWSException, Exception; /** * Serializes the whole object in the given output stream and thanks to the given serializer. @@ -78,9 +79,9 @@ public abstract class SerializableUWSObject implements Serializable { * * @throws UWSException If there is an error during the serialization. * - * @see #serialize(ServletOutputStream, UWSSerializer, String) + * @see #serialize(ServletOutputStream, UWSSerializer, JobOwner) */ - public void serialize(ServletOutputStream output, UWSSerializer serializer) throws UWSException{ + public void serialize(ServletOutputStream output, UWSSerializer serializer) throws Exception{ serialize(output, serializer, null); } @@ -90,27 +91,23 @@ public abstract class SerializableUWSObject implements Serializable { * * @param output The ouput stream in which this object must be serialized. * @param serializer The serializer to use. - * @param ownerId The ID of the current ID. + * @param owner The user who asks for the serialization. * - * @throws UWSException If the given ouput stream is null, - * or if there is an error during the serialization, - * or if there is an error while writing in the given stream. + * @throws UWSException If the owner is not allowed to see the content of the serializable object. + * @throws IOException If there is an error while writing in the given stream. + * @throws Exception If there is any other error during the serialization. * - * @see #serialize(UWSSerializer, String) + * @see #serialize(UWSSerializer, JobOwner) */ - public void serialize(ServletOutputStream output, UWSSerializer serializer, JobOwner owner) throws UWSException{ + public void serialize(ServletOutputStream output, UWSSerializer serializer, JobOwner owner) throws UWSException, IOException, Exception{ if (output == null) - throw UWSExceptionFactory.missingOutputStream("impossible to serialize {" + toString() + "}."); + throw new NullPointerException("Missing serialization output stream!"); - try{ - String serialization = serialize(serializer, owner); - if (serialization != null){ - output.print(serialization); - output.flush(); - }else - throw UWSExceptionFactory.incorrectSerialization("NULL", "{" + toString() + "}"); - }catch(IOException ex){ - throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, ex, "IOException => impossible to serialize {" + toString() + "} !"); - } + String serialization = serialize(serializer, owner); + if (serialization != null){ + output.print(serialization); + output.flush(); + }else + throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, "Incorrect serialization value (=NULL) ! => impossible to serialize " + toString() + "."); } } diff --git a/src/uws/job/UWSJob.java b/src/uws/job/UWSJob.java index 9fe509a..b2a28c9 100644 --- a/src/uws/job/UWSJob.java +++ b/src/uws/job/UWSJob.java @@ -17,7 +17,7 @@ package uws.job; * along with UWSLibrary. If not, see . * * Copyright 2012-2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomisches Rechen Institute (ARI) + * Astronomisches Rechen Institut (ARI) */ import java.io.IOException; @@ -33,6 +33,7 @@ import java.util.Vector; import javax.servlet.ServletOutputStream; +import uws.ISO8601Format; import uws.UWSException; import uws.UWSExceptionFactory; import uws.UWSToolBox; @@ -45,6 +46,8 @@ import uws.service.UWSFactory; import uws.service.UWSUrl; import uws.service.file.UWSFileManager; import uws.service.log.UWSLog; +import uws.service.log.UWSLog.LogLevel; +import uws.service.request.UploadFile; /** *

    Brief description

    @@ -56,8 +59,8 @@ import uws.service.log.UWSLog; *
      *
    • * The job attributes startTime and endTime are automatically managed by {@link UWSJob}. You don't have to do anything ! - * However you can customize the used date/time format thanks to the function {@link #setDateFormat(DateFormat)}. The default date/time format is: - * yyyy-MM-dd'T'HH:mm:ss.SSSZ + * The date/time format is managed automatically by the library and can not be customized since it is imposed by the UWS + * protocol definition: ISO-8601. *
    • *
      *
    • Once set, the destruction and the executionDuration attributes are automatically managed. That is to say: @@ -90,13 +93,6 @@ import uws.service.log.UWSLog; *
    • *
      *
    • - * {@link #loadAdditionalParams()}: - * All parameters that are not managed by default are automatically stored in the job attribute {@link #additionalParameters} (a map). - * However if you want manage yourself some or all of these additional parameters (i.e. task parameters), you must override this method. - * (By default nothing is done.) - *
    • - *
      - *
    • * {@link #clearResources()}: * This method is called only at the destruction of the job. * By default, the job is stopped (if running), thread resources are freed, @@ -117,7 +113,7 @@ import uws.service.log.UWSLog; *
    * * @author Grégory Mantelet (CDS;ARI) - * @version 4.1 (04/2014) + * @version 4.1 (12/2014) */ public class UWSJob extends SerializableUWSObject { private static final long serialVersionUID = 1L; @@ -176,7 +172,9 @@ public class UWSJob extends SerializableUWSObject { /** Default value of {@link #owner} if no ID are given at the job creation. */ public final static String ANONYMOUS_OWNER = "anonymous"; - /** Default date format pattern. */ + /** Default date format pattern. + * @deprecated Replaced by {@link ISO8601Format}.*/ + @Deprecated public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"; /** The quote value that indicates the quote of this job is not known. */ @@ -189,7 +187,7 @@ public class UWSJob extends SerializableUWSObject { /* VARIABLES */ /* ********* */ /** The last generated job ID. It SHOULD be used ONLY by the function {@link #generateJobId()} ! */ - protected static String lastId = null; + protected static String lastId = System.currentTimeMillis() + "A"; /** The identifier of the job (it MUST be different from any other job).
    * Note: It is assigned automatically at the job creation in any job constructor @@ -205,11 +203,6 @@ public class UWSJob extends SerializableUWSObject { /** The jobs list which is supposed to managed this job. */ private JobList myJobList = null; - /* The name/label that the job creator uses to identify this job.
    - * Note: this is distinct from the Job Identifier that the UWS system itself - * assigns to each job ({@link #jobId}). It may not be unique ! * - protected String runId = null;*/ - /** *

    The current phase of the job.

    * Remember: A job is treated as a state machine thanks to this attribute. @@ -226,7 +219,9 @@ public class UWSJob extends SerializableUWSObject { */ private JobPhase phase; - /** The used date formatter. */ + /** The used date formatter. + * @deprecated Replaced by {@link ISO8601Format}. */ + @Deprecated public static final DateFormat dateFormat = new SimpleDateFormat(DEFAULT_DATE_FORMAT); /** @@ -242,37 +237,11 @@ public class UWSJob extends SerializableUWSObject { /** The time at which the job execution ended. */ private Date endTime = null; - /* - *

    This is the duration (in seconds) for which the job shall run.

    - * Notes: - *
      - *
    • An execution duration of 0 ({@link #UNLIMITED_DURATION}) implies unlimited execution duration.
    • - *
    • When a job is created, the service sets the initial execution duration.
    • - *
    • When the execution duration has been exceeded the service should automatically abort the job, - * which has the same effect as when a manual "Abort" is requested.
    • - *
    * - private long executionDuration = UNLIMITED_DURATION; - - /*

    This represents the instant when the job shall be destroyed.

    - * Notes: Destroying a job implies: - *
      - *
    • if the job is still executing, the execution is aborted
    • - *
    • any results from the job are destroyed and storage reclaimed
    • - *
    • the service forgets that the job existed.
    • - *
    - *

    The Destruction time should be viewed as a measure of the amount of time - * that a service is prepared to allocated storage for a job - typically this will be a longer duration - * that the amount of CPU time that a service would allocate.

    * - private Date destructionTime = null;*/ - /**

    This error summary gives a human-readable error message for the underlying job.

    * Note: This object is intended to be a detailed error message, and consequently, * might be a large piece of text such as a stack trace. */ protected ErrorSummary errorSummary = null; - /* This is an enumeration of the other Job parameters (given in POST queries). * - protected Map additionalParameters;*/ - /** This is a list of all results of this job. */ protected Map results; @@ -300,13 +269,11 @@ public class UWSJob extends SerializableUWSObject { *

    Note: if the parameter {@link UWSJob#PARAM_PHASE} (phase) is given with the value {@link UWSJob#PHASE_RUN} * the job execution starts immediately after the job has been added to a job list or after {@link #applyPhaseParam(JobOwner)} is called.

    * - * @param params UWS standard and non-standard parameters. - * - * @throws UWSException If a parameter is incorrect. + * @param params UWS standard and non-standard parameters. * - * @see UWSJob#AbstractJob(String, Map) + * @see UWSJob#UWSJob(JobOwner, UWSParameters) */ - public UWSJob(final UWSParameters params) throws UWSException{ + public UWSJob(final UWSParameters params){ this(null, params); } @@ -316,32 +283,31 @@ public class UWSJob extends SerializableUWSObject { *

    Note: if the parameter {@link #PARAM_PHASE} (phase) is given with the value {@link #PHASE_RUN} * the job execution starts immediately after the job has been added to a job list or after {@link #applyPhaseParam(JobOwner)} is called.

    * - * @param owner Job.owner ({@link #PARAM_OWNER}). - * @param params UWS standard and non-standard parameters. - * - * @throws UWSException If a parameter is incorrect. + * @param owner Job.owner ({@link #PARAM_OWNER}). + * @param params UWS standard and non-standard parameters. * - * @see #loadDefaultParams(Map) - * @see #loadAdditionalParams() + * @see UWSParameters#init() */ - public UWSJob(JobOwner owner, final UWSParameters params) throws UWSException{ + public UWSJob(JobOwner owner, final UWSParameters params){ this.owner = owner; phase = new JobPhase(this); - //additionalParameters = new HashMap(); results = new HashMap(); - /*Map others = loadDefaultParams(lstParam); - if (others != null){ - additionalParameters.putAll(others); - loadAdditionalParams(); - }*/ inputParams = params; inputParams.init(); jobId = generateJobId(); restorationDate = null; + + // Move all uploaded files in a location related with this job: + Iterator files = inputParams.getFiles(); + while(files.hasNext()){ + try{ + files.next().move(this); + }catch(IOException ioe){} + } } /** @@ -363,18 +329,16 @@ public class UWSJob extends SerializableUWSObject { * @param results Its results (if phase=COMPLETED). * @param error Its error (if phase=ERROR). * - * @throws UWSException If the given ID is null or if another error occurs while building this job. + * @throws NullPointerException If the given ID is NULL. */ - public UWSJob(final String jobID, final JobOwner owner, final UWSParameters params, final long quote, final long startTime, final long endTime, final List results, final ErrorSummary error) throws UWSException{ + public UWSJob(final String jobID, final JobOwner owner, final UWSParameters params, final long quote, final long startTime, final long endTime, final List results, final ErrorSummary error) throws NullPointerException{ if (jobID == null) - throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, "Missing job ID => impossible to build a Job without a valid ID !"); + throw new NullPointerException("Missing job ID => impossible to build a Job without a valid ID!"); this.jobId = jobID; this.owner = owner; this.quote = quote; - /*this.destructionTime = destruction; - this.executionDuration = (maxDuration<0)?UNLIMITED_DURATION:maxDuration;*/ if (startTime > 0) this.startTime = new Date(startTime); @@ -396,13 +360,6 @@ public class UWSJob extends SerializableUWSObject { inputParams = params; params.init(); - /*this.additionalParameters = new HashMap(); - Map others = loadDefaultParams(lstParams); - if (others != null){ - additionalParameters.putAll(others); - loadAdditionalParams(); - }*/ - ExecutionPhase p = ExecutionPhase.PENDING; if (startTime > 0 && endTime > 0){ if (this.results.isEmpty() && this.errorSummary == null) @@ -412,8 +369,13 @@ public class UWSJob extends SerializableUWSObject { else if (this.errorSummary != null) p = ExecutionPhase.ERROR; } - if (phase != null) - setPhase(p, true); + if (phase != null){ + try{ + setPhase(p, true); + }catch(UWSException ue){ + // Can never append because the "force" parameter is true! + } + } restorationDate = new Date(); } @@ -429,143 +391,18 @@ public class UWSJob extends SerializableUWSObject { * * @return A unique job identifier. */ - protected String generateJobId() throws UWSException{ - String generatedId = System.currentTimeMillis() + "A"; - if (lastId != null){ - while(lastId.equals(generatedId)) - generatedId = generatedId.substring(0, generatedId.length() - 1) + (char)(generatedId.charAt(generatedId.length() - 1) + 1); + protected String generateJobId(){ + synchronized(lastId){ + String generatedId = System.currentTimeMillis() + "A"; + if (lastId != null){ + while(lastId.equals(generatedId)) + generatedId = generatedId.substring(0, generatedId.length() - 1) + (char)(generatedId.charAt(generatedId.length() - 1) + 1); + } + lastId = generatedId; + return generatedId; } - lastId = generatedId; - return generatedId; } - /* - *

    Loads the given parameters: all known parameters (with write access) are updated - * whereas others are returned in a new map in which all keys are in lower case.

    - * - *

    Important: The phase parameter is NEVER managed here and is ALWAYS added immediately in the additional parameters attribute !

    - * - *

    Note: UWS parameters with write access are: - *

      - *
    • {@link UWSJob#PARAM_RUN_ID RUN_ID}
    • - *
    • {@link UWSJob#PARAM_EXECUTION_DURATION EXECUTION_DURATION}
    • - *
    • {@link UWSJob#PARAM_DESTRUCTION_TIME DESTRUCTION_TIME}
    • - *
    • {@link UWSJob#PARAM_PHASE PHASE} if equals to {@link UWSJob#PHASE_RUN} or {@link UWSJob#PHASE_ABORT}
    • - *

    - * - *

    Note: To check more DEFAULT parameters you just have to: - *

      - *
    1. override the function {@link UWSJob#loadDefaultParams(Map)}
    2. - *
    3. call super.loadParams(Map)
    4. - *
    5. add your own checking (do not forget to update the returned map and to return it).
    6. - *

    - * - * @param lstParam The list of parameters to load (UWS - included PHASE - and additional parameters). - * - * @return
      - *
    • a new map with all the parameters that have not been loaded (additional parameters and/or not known UWS parameter and/or the PHASE parameter)
    • - *
    • or an empty map
    • - *
    • or null if the job is executing or is ended (actually: all phase except PENDING)
    • - *
    - * - * @throws UWSException If a given UWS parameter is not correct. - * - @SuppressWarnings("unchecked") - protected Map loadDefaultParams(final Map lstParam) throws UWSException { - if (lstParam == null) - return new HashMap(); - - // Forbids the parameter modification if the job is already finished: - if (isFinished()) - throw UWSExceptionFactory.jobModificationForbidden(getJobId(), getPhase(), null); - - // Build a new map for all the ignored parameters (that's to say all non UWS parameters): - HashMap otherParams = new HashMap(); - - Set> paramSet = lstParam.entrySet(); - String paramName = null; - Object paramValue = null; - for(Map.Entry param : paramSet){ - paramName = param.getKey(); - paramValue = param.getValue(); - - if (paramName == null || paramValue == null) - continue; - - // PHASE: - if (paramName.equalsIgnoreCase(PARAM_PHASE)){ - if (!phase.isFinished()) - otherParams.put(PARAM_PHASE, paramValue); - - }// PARAMETERS: - else if (paramName.equalsIgnoreCase(PARAM_PARAMETERS)){ - if (paramValue instanceof Map){ - Map m = (Map)paramValue; - for(Map.Entry entry : (Set)m.entrySet()){ - if (entry.getKey() instanceof String) - otherParams.put((String)entry.getKey(), entry.getValue()); - } - } - - }// RUN ID: - else if (paramName.equalsIgnoreCase(PARAM_RUN_ID)){ - if (paramValue instanceof String) - setRunId((String)paramValue); - else - throw UWSExceptionFactory.badFormat(getJobId(), "RUN ID", paramValue.toString(), paramValue.getClass().getName(), "A String instance"); - - }// EXECUTION DURATION: - else if (paramName.equalsIgnoreCase(PARAM_EXECUTION_DURATION)){ - if (isRunning()) - throw UWSExceptionFactory.jobModificationForbidden(getJobId(), getPhase(), "EXECUTION DURATION"); - - if (!(paramValue instanceof String) && !(paramValue instanceof Long)) - throw UWSExceptionFactory.badFormat(getJobId(), "EXECUTION DURATION", paramValue.toString(), paramValue.getClass().getName(), "A Long or a String instance."); - - try{ - setExecutionDuration((paramValue instanceof String) ? Long.parseLong((String)paramValue) : (Long)paramValue); - }catch(NumberFormatException ex){ - setExecutionDuration(0); - throw UWSExceptionFactory.badFormat(getJobId(), "EXECUTION DURATION", paramValue.toString(), paramValue.getClass().getName(), "A long integer value"); - } - - }// DESTRUCTION TIME: - else if (paramName.equalsIgnoreCase(PARAM_DESTRUCTION_TIME)){ - if (isRunning()){ - try{ - throw UWSExceptionFactory.jobModificationForbidden(getJobId(), getPhase(), "DESTRUCTION TIME"); - }catch(UWSException ue){ - ue.printStackTrace(); - System.out.println(" => PARAM NAME = \""+paramName+"\" ; PARAM VALUE = "+paramValue); - throw ue; - } - } - - if (!(paramValue instanceof String) && !(paramValue instanceof Date)) - throw UWSExceptionFactory.badFormat(getJobId(), paramName, paramValue.toString(), paramValue.getClass().getName(), "A Date or a String instance."); - - try { - if (paramValue instanceof String){ - String time = (String)paramValue; - if (time != null && !time.trim().isEmpty()) - setDestructionTime(dateFormat.parse(time)); - }else - setDestructionTime((Date)paramValue); - } catch (ParseException e) { - throw UWSExceptionFactory.badFormat(getJobId(), paramName, paramValue.toString(), null, ((dateFormat instanceof SimpleDateFormat)?(((SimpleDateFormat)dateFormat).toPattern()):"A valid date (format: ???).")); - } - - }// READ-ONLY PARAMETERS: - else if (paramName.equalsIgnoreCase(PARAM_JOB_ID) && paramName.equalsIgnoreCase(PARAM_QUOTE) && paramName.equalsIgnoreCase(PARAM_START_TIME) && paramName.equalsIgnoreCase(PARAM_END_TIME) && paramName.equalsIgnoreCase(PARAM_RESULTS) && paramName.equalsIgnoreCase(PARAM_ERROR_SUMMARY)){ - continue; - - }// ADDITIONAL PARAMETERS - else - otherParams.put(paramName, paramValue); - } - return otherParams; - }*/ - /** *

    Gets the value of the specified parameter.

    * @@ -598,36 +435,17 @@ public class UWSJob extends SerializableUWSObject { return inputParams.get(name); } - /* - *

    Method called when updating one or several parameters using the functions {@link #addOrUpdateParameter(String, String)} and - * {@link #addOrUpdateParameters(Map)} or at the job creation.

    - * - *

    It is useful if you need to check or to process all or a part of the additional parameters stored in {@link #additionalParameters}.

    - * - *

    By default this function does nothing and always return true.

    - * - * @return true if all required additional parameters have been successfully updated, false otherwise. - * - * @throws UWSException If an error occurred during the updating of one parameter. - * - * @see #addOrUpdateParameter(String, String) - * @see #addOrUpdateParameters(Map) - * - protected boolean loadAdditionalParams() throws UWSException { - return true; - }*/ - /** *

    Looks for an additional parameters which corresponds to the Execution Phase. If it exists and:

    *
      - *
    • is equals to {@link UWSJob#PHASE_RUN RUN} => remove it from the attribute {@link #additionalParameters} and start the job.
    • - *
    • is equals to {@link UWSJob#PHASE_ABORT ABORT} => remove it from the attribute {@link #additionalParameters} and abort the job.
    • - *
    • is another value => the attribute stays in the attribute {@link #additionalParameters} and nothing is done.
    • + *
    • is equals to {@link UWSJob#PHASE_RUN RUN} => remove it from the attribute {@link #inputParams} and start the job.
    • + *
    • is equals to {@link UWSJob#PHASE_ABORT ABORT} => remove it from the attribute {@link #inputParams} and abort the job.
    • + *
    • is another value => the attribute is though removed from the attribute {@link #inputParams} but nothing is done.
    • *
    * * @param user The user who asks to apply the phase parameter (start/abort). (may be NULL) * - * @throws UWSException If it is impossible to change the Execution Phase + * @throws UWSException If it is impossible the state of this job (into EXECUTING or ABORTED) * or if the given user is not allowed to execute this job. * * @see UWSParameters#hasInputPhase() @@ -642,12 +460,12 @@ public class UWSJob extends SerializableUWSObject { if (inputPhase.equalsIgnoreCase(PHASE_RUN)){ // Forbids the execution if the user has not the required permission: if (user != null && !user.equals(owner) && !user.hasExecutePermission(this)) - throw UWSExceptionFactory.executePermissionDenied(user, getJobId()); + throw new UWSException(UWSException.PERMISSION_DENIED, UWSExceptionFactory.executePermissionDenied(user, jobId)); start(); }else if (inputPhase.equalsIgnoreCase(PHASE_ABORT)){ // Forbids the execution if the user has not the required permission: if (user != null && !user.equals(owner) && !user.hasExecutePermission(this)) - throw UWSExceptionFactory.executePermissionDenied(user, getJobId()); + throw new UWSException(UWSException.PERMISSION_DENIED, UWSExceptionFactory.executePermissionDenied(user, jobId)); abort(); } } @@ -765,6 +583,9 @@ public class UWSJob extends SerializableUWSObject { ExecutionPhase oldPhase = phase.getPhase(); phase.setPhase(p, force); + if (!force) + getLogger().logJob(LogLevel.INFO, this, "CHANGE_PHASE", "The job \"" + getJobId() + "\" goes from " + oldPhase + " to " + p, null); + // Notify the execution manager: if (phase.isFinished() && getJobList() != null) getJobList().getExecutionManager().remove(this); @@ -840,7 +661,7 @@ public class UWSJob extends SerializableUWSObject { getJobList().getUWS().getBackupManager().saveOwner(owner); // Log the end of this job: - getLogger().jobFinished(this); + getLogger().logJob(LogLevel.INFO, this, "END", "Job \"" + jobId + "\" ended with the status " + phase, null); } /** @@ -890,19 +711,19 @@ public class UWSJob extends SerializableUWSObject { * If known the jobs list is notify of this destruction time update. *

    * - * @param destructionTime The destruction time of this job. + * @param destructionTime The destruction time of this job. MUST NOT be NULL * * @see JobList#updateDestruction(UWSJob) * @see UWSParameters#set(String, Object) */ public final void setDestructionTime(Date destructionTime){ - if (phase.isJobUpdatable()){ + if (destructionTime != null && phase.isJobUpdatable()){ try{ inputParams.set(PARAM_DESTRUCTION_TIME, destructionTime); if (myJobList != null) myJobList.updateDestruction(this); }catch(UWSException ue){ - ; + getLogger().logJob(LogLevel.WARNING, this, "SET_DESTRUCTION", "Can not set the destruction time of the job \"" + getJobId() + "\" to \"" + destructionTime + "\"!", ue); } } } @@ -922,17 +743,21 @@ public class UWSJob extends SerializableUWSObject { *

    IMPORTANT: This function will have no effect if the job is finished, that is to say if the current phase is * {@link ExecutionPhase#ABORTED ABORTED}, {@link ExecutionPhase#ERROR ERROR} or {@link ExecutionPhase#COMPLETED COMPLETED}..

    * - * @param errorSummary A summary of the error. + * @param errorSummary A summary of the error. MUST NOT be NULL * * @throws UWSException If the job execution is finished that is to say if the phase is ABORTED, ERROR or COMPLETED. * * @see #isFinished() */ public final void setErrorSummary(ErrorSummary errorSummary) throws UWSException{ - if (!isFinished()) + if (errorSummary == null) + return; + else if (!isFinished()) this.errorSummary = errorSummary; - else - throw UWSExceptionFactory.jobModificationForbidden(getJobId(), getPhase(), "ERROR SUMMARY"); + else{ + getLogger().logJob(LogLevel.ERROR, this, "SET_ERROR", "Can not set an error summary when the job is finished (or not yet started)! The current phase is: " + getPhase() + " ; the summary of the error to set is: \"" + errorSummary.message + "\".", null); + throw new UWSException(UWSException.NOT_ALLOWED, UWSExceptionFactory.jobModificationForbidden(jobId, getPhase(), "ERROR SUMMARY")); + } } /** @@ -1049,11 +874,51 @@ public class UWSJob extends SerializableUWSObject { * @throws UWSException If a parameter value is incorrect. * * @see JobPhase#isJobUpdatable() - * @see UWSJob#addOrUpdateParameters(Map) */ public final boolean addOrUpdateParameter(String paramName, Object paramValue) throws UWSException{ - if (!phase.isFinished()){ + return addOrUpdateParameter(paramName, paramValue, null); + } + + /** + * Adds or updates the specified parameter with the given value ONLY IF the job can be updated (considering its current execution phase, see {@link JobPhase#isJobUpdatable()}). + * + * @param paramName The name of the parameter to add or to update. + * @param paramValue The (new) value of the specified parameter. + * @param user The user who asks for this update. + * + * @return
    • true if the parameter has been successfully added/updated,
    • + *
    • false otherwise (particularly if paramName=null or paramName="" or paramValue=null).
    + * + * @throws UWSException If a parameter value is incorrect. + * + * @since 4.1 + * + * @see JobPhase#isJobUpdatable() + */ + public final boolean addOrUpdateParameter(String paramName, Object paramValue, final JobOwner user) throws UWSException{ + if (paramValue != null && !phase.isFinished()){ + + // Set the parameter: inputParams.set(paramName, paramValue); + + // If it is a file or an array containing files, they must be moved in a location related to this job: + try{ + if (paramValue instanceof UploadFile) + ((UploadFile)paramValue).move(this); + else if (paramValue.getClass().isArray()){ + for(Object o : (Object[])paramValue){ + if (o != null && o instanceof UploadFile) + ((UploadFile)o).move(this); + } + } + }catch(IOException ioe){ + getLogger().logJob(LogLevel.WARNING, this, "MOVE_UPLOAD", "Can not move an uploaded file in the job \"" + jobId + "\"!", ioe); + return false; + } + + // Apply the retrieved phase: + applyPhaseParam(user); + return true; }else return false; @@ -1062,11 +927,11 @@ public class UWSJob extends SerializableUWSObject { /** *

    Adds or updates the given parameters ONLY IF the job can be updated (considering its current execution phase, see {@link JobPhase#isJobUpdatable()}).

    * - *

    Whatever is the result of {@link #loadDefaultParams(Map)} the method {@link #applyPhaseParam()} is called so that if there is an additional parameter {@link #PARAM_PHASE} with the value: + *

    At the end of this function, the method {@link #applyPhaseParam(JobOwner)} is called so that if there is an additional parameter {@link #PARAM_PHASE} with the value: *

      *
    • {@link UWSJob#PHASE_RUN RUN} then the job is starting and the phase goes to {@link ExecutionPhase#EXECUTING EXECUTING}.
    • *
    • {@link UWSJob#PHASE_ABORT ABORT} then the job is aborting.
    • - *
    • otherwise the parameter {@link UWSJob#PARAM_PHASE PARAM_PHASE} remains in the {@link UWSJob#additionalParameters additionalParameters} list.
    • + *
    • otherwise the parameter {@link UWSJob#PARAM_PHASE PARAM_PHASE} is removed from {@link UWSJob#inputParams inputParams} and nothing is done.
    • *

    * * @param params A list of parameters to add/update. @@ -1075,7 +940,7 @@ public class UWSJob extends SerializableUWSObject { * * @throws UWSException If a parameter value is incorrect. * - * @see #addOrUpdateParameters(Map) + * @see #addOrUpdateParameters(UWSParameters, JobOwner) */ public boolean addOrUpdateParameters(UWSParameters params) throws UWSException{ return addOrUpdateParameters(params, null); @@ -1084,38 +949,55 @@ public class UWSJob extends SerializableUWSObject { /** *

    Adds or updates the given parameters ONLY IF the job can be updated (considering its current execution phase, see {@link JobPhase#isJobUpdatable()}).

    * - *

    Whatever is the result of {@link #loadDefaultParams(Map)} the method {@link #applyPhaseParam()} is called so that if there is an additional parameter {@link #PARAM_PHASE} with the value: + *

    At the end of this function, the method {@link #applyPhaseParam(JobOwner)} is called so that if there is an additional parameter {@link #PARAM_PHASE} with the value: *

      *
    • {@link UWSJob#PHASE_RUN RUN} then the job is starting and the phase goes to {@link ExecutionPhase#EXECUTING EXECUTING}.
    • *
    • {@link UWSJob#PHASE_ABORT ABORT} then the job is aborting.
    • - *
    • otherwise the parameter {@link UWSJob#PARAM_PHASE PARAM_PHASE} remains in the {@link UWSJob#additionalParameters additionalParameters} list.
    • + *
    • otherwise the parameter {@link UWSJob#PARAM_PHASE PARAM_PHASE} is removed from {@link UWSJob#inputParams inputParams} and nothing is done.
    • *

    * * @param params The UWS parameters to update. + * @param user The user who asks for this update. + * * @return
    • true if all the given parameters have been successfully added/updated,
    • *
    • false if some parameters have not been managed.
    * * @throws UWSException If a parameter value is incorrect or if the given user can not update or execute this job. * - * @see #loadDefaultParams(Map) * @see JobPhase#isJobUpdatable() - * @see #loadAdditionalParams() - * @see #applyPhaseParam() + * @see #applyPhaseParam(JobOwner) */ public boolean addOrUpdateParameters(UWSParameters params, final JobOwner user) throws UWSException{ + // The job can be modified ONLY IF in PENDING phase: + if (!phase.isJobUpdatable()) + throw new UWSException(UWSException.FORBIDDEN, "Forbidden parameters modification: the job is not any more in the PENDING phase!"); + // Forbids the update if the user has not the required permission: if (user != null && !user.equals(owner) && !user.hasWritePermission(this)) - throw UWSExceptionFactory.writePermissionDenied(user, false, getJobId()); + throw new UWSException(UWSException.PERMISSION_DENIED, UWSExceptionFactory.writePermissionDenied(user, false, getJobId())); // Load all parameters: String[] updated = inputParams.update(params); // If the destruction time has been updated, the modification must be propagated to the jobs list: + Object newValue; for(String updatedParam : updated){ + // CASE DESTRUCTION_TIME: update the thread dedicated to the destruction: if (updatedParam.equals(PARAM_DESTRUCTION_TIME)){ if (myJobList != null) myJobList.updateDestruction(this); - break; + } + // DEFAULT: test whether the parameter is a file, and if yes, move it in a location related to this job: + else{ + newValue = inputParams.get(updatedParam); + if (newValue != null && newValue instanceof UploadFile){ + try{ + ((UploadFile)newValue).move(this); + }catch(IOException ioe){ + getLogger().logJob(LogLevel.WARNING, this, "MOVE_UPLOAD", "Can not move an uploaded file in the job \"" + jobId + "\"!", ioe); + inputParams.remove(updatedParam); + } + } } } @@ -1139,7 +1021,16 @@ public class UWSJob extends SerializableUWSObject { if (phase.isFinished() || paramName == null) return false; else{ - inputParams.remove(paramName); + // Remove the parameter from the map: + Object removed = inputParams.remove(paramName); + // If the parameter value was an uploaded file, delete it physically: + if (removed != null && removed instanceof UploadFile){ + try{ + ((UploadFile)removed).deleteFile(); + }catch(IOException ioe){ + getLogger().logJob(LogLevel.WARNING, this, "MOVE_UPLOAD", "Can not delete the uploaded file \"" + paramName + "\" of the job \"" + jobId + "\"!", ioe); + } + } return true; } } @@ -1189,9 +1080,11 @@ public class UWSJob extends SerializableUWSObject { public boolean addResult(Result res) throws UWSException{ if (res == null) return false; - else if (isFinished()) - throw UWSExceptionFactory.jobModificationForbidden(getJobId(), getPhase(), "RESULT"); - else{ + else if (isFinished()){ + UWSException ue = new UWSException(UWSException.NOT_ALLOWED, UWSExceptionFactory.jobModificationForbidden(getJobId(), getPhase(), "RESULT")); + getLogger().logJob(LogLevel.ERROR, this, "ADD_RESULT", "Can not add the result \"" + res.getId() + "\" to the job \"" + getJobId() + "\": this job is already finished (or not yet started). Current phase: " + getPhase(), ue); + throw ue; + }else{ synchronized(results){ if (results.containsKey(res.getId())) return false; @@ -1228,7 +1121,7 @@ public class UWSJob extends SerializableUWSObject { *

    note 2: this job is removed from its previous job list, if there is one.

    *

    note 3: this job is NOT automatically added into the new jobs list. Indeed, this function should be called by {@link JobList#addNewJob(UWSJob)}.

    * - * @param jobList Its new jobs list. note: if NULL, nothing is done ! + * @param jobList Its new jobs list. note: if NULL, nothing is done ! * * @throws IllegalStateException If this job is not PENDING. * @@ -1302,7 +1195,8 @@ public class UWSJob extends SerializableUWSObject { * * @param useManager true to let the execution manager deciding whether the job starts immediately or whether it must be put in a queue until enough resources are available, false to start the execution immediately. * - * @throws UWSException If there is an error while changing the execution phase or when starting the corresponding thread. + * @throws NullPointerException If this job is not associated with a job list or the associated job list is not part of a UWS service or if no thread is created. + * @throws UWSException If there is an error while changing the execution phase or when starting the corresponding thread. * * @see #isRunning() * @see UWSFactory#createJobThread(UWSJob) @@ -1314,7 +1208,7 @@ public class UWSJob extends SerializableUWSObject { public void start(boolean useManager) throws UWSException{ // This job must know its jobs list and this jobs list must know its UWS: if (myJobList == null || myJobList.getUWS() == null) - throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, "A UWSJob can not start if it is not part of a job list or if its job list is not part of a UWS."); + throw new IllegalStateException("A UWSJob can not start if it is not linked to a job list or if its job list is not linked to a UWS."); // If already running do nothing: else if (isRunning()) @@ -1326,24 +1220,32 @@ public class UWSJob extends SerializableUWSObject { }// Otherwise start directly the execution: else{ - // Try to change the phase: - setPhase(ExecutionPhase.EXECUTING); - - // Create and run its corresponding thread: + // Create its corresponding thread: thread = getFactory().createJobThread(this); if (thread == null) - throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, "Missing job work ! The thread created by the factory is NULL => The job can't be executed !"); - thread.start(); - (new JobTimeOut()).start(); + throw new NullPointerException("Missing job work! The thread created by the factory is NULL => The job can't be executed!"); + + // Change the job phase: + setPhase(ExecutionPhase.EXECUTING); // Set the start time: setStartTime(new Date()); + // Run the job: + thread.start(); + (new JobTimeOut()).start(); + // Log the start of this job: - getLogger().jobStarted(this); + getLogger().logJob(LogLevel.INFO, this, "START", "Job \"" + jobId + "\" started.", null); } } + /** + * Stop/Cancel this job when its maximum execution duration has been reached. + * + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (09/2014) + */ protected final class JobTimeOut extends Thread { public JobTimeOut(){ super(JobThread.tg, "TimeOut_" + jobId); @@ -1358,9 +1260,9 @@ public class UWSJob extends SerializableUWSObject { if (!isFinished()) UWSJob.this.abort(); }catch(InterruptedException ie){ - getLogger().error("Unexpected InterruptedException while waiting the end of the execution of the job \"" + jobId + "\" (thread ID: " + thread.getId() + ") !", ie); + /* Not needed to report any interruption while waiting. */ }catch(UWSException ue){ - getLogger().error("Unexpected UWSException while waiting the end of the execution of the job \"" + jobId + "\" (thread ID: " + thread.getId() + ") !", ue); + getLogger().logJob(LogLevel.WARNING, UWSJob.this, "EXECUTING", "Unexpected error while waiting the end of the execution of the job \"" + jobId + "\" (thread ID: " + thread.getId() + ")!", ue); } } } @@ -1398,7 +1300,7 @@ public class UWSJob extends SerializableUWSObject { *

    Stops immediately the job, sets its phase to {@link ExecutionPhase#ABORTED ABORTED} and sets its end time.

    * *

    IMPORTANT: If the thread does not stop immediately the phase and the end time are not modified. However it can be done by calling one more time {@link #abort()}. - * Besides you should check that you test regularly the interrupted flag of the thread in {@link #jobWork()} !

    + * Besides you should check that you test regularly the interrupted flag of the thread in {@link JobThread#jobWork()} !

    * * @throws UWSException If there is an error while changing the execution phase. * @@ -1419,8 +1321,9 @@ public class UWSJob extends SerializableUWSObject { // Set the end time: setEndTime(new Date()); }else if (thread == null || (thread != null && !thread.isAlive())) - throw UWSExceptionFactory.incorrectPhaseTransition(getJobId(), phase.getPhase(), ExecutionPhase.ABORTED); - } + throw new UWSException(UWSException.BAD_REQUEST, UWSExceptionFactory.incorrectPhaseTransition(getJobId(), phase.getPhase(), ExecutionPhase.ABORTED)); + }else + getLogger().logJob(LogLevel.WARNING, this, "ABORT", "Abortion of the job \"" + getJobId() + "\" asked but not yet effective (after having waited " + waitForStop + "ms)!", null); } /** @@ -1428,7 +1331,7 @@ public class UWSJob extends SerializableUWSObject { * *

    IMPORTANT: If the thread does not stop immediately the phase, the error summary and the end time are not modified. * However it can be done by calling one more time {@link #error(ErrorSummary)}. - * Besides you should check that you test regularly the interrupted flag of the thread in {@link #jobWork()} !

    + * Besides you should check that you test regularly the interrupted flag of the thread in {@link JobThread#jobWork()} !

    * * @param error The error that has interrupted this job. * @@ -1456,8 +1359,9 @@ public class UWSJob extends SerializableUWSObject { // Set the end time: setEndTime(new Date()); }else if (thread != null && !thread.isAlive()) - throw UWSExceptionFactory.incorrectPhaseTransition(jobId, phase.getPhase(), ExecutionPhase.ERROR); - } + throw new UWSException(UWSException.BAD_REQUEST, UWSExceptionFactory.incorrectPhaseTransition(jobId, phase.getPhase(), ExecutionPhase.ERROR)); + }else + getLogger().logJob(LogLevel.WARNING, this, "ERROR", "Stopping of the job \"" + getJobId() + "\" with error asked but not yet effective (after having waited " + waitForStop + "ms)!", null); } /** Used by the thread to known whether the {@link #stop()} method has already been called, and so, that the job is stopping. */ @@ -1479,7 +1383,7 @@ public class UWSJob extends SerializableUWSObject { try{ thread.join(waitForStop); }catch(InterruptedException ie){ - getLogger().error("Unexpected InterruptedException while waiting the end of the execution of the job \"" + jobId + "\" (thread ID: " + thread.getId() + ") !", ie); + getLogger().logJob(LogLevel.WARNING, this, "END", "Unexpected InterruptedException while waiting for the end of the execution of the job \"" + jobId + "\" (thread ID: " + thread.getId() + ")!", ie); } } } @@ -1499,7 +1403,7 @@ public class UWSJob extends SerializableUWSObject { *

    Stops the job if running, removes the job from the execution manager, stops the timer for the execution duration * and may clear all files or any other resources associated to this job.

    * - *

    By default the job is aborted, only the {@link UWSJob#thread} attribute is set to null and the timers are stopped; no other operations (i.e. clear result files and error files) is done.

    + *

    By default the job is aborted, the {@link UWSJob#thread} attribute is set to null, the timers are stopped and uploaded files, results and the error summary are deleted.

    */ public void clearResources(){ // If still running, abort/stop the job: @@ -1507,27 +1411,35 @@ public class UWSJob extends SerializableUWSObject { try{ abort(); }catch(UWSException e){ - getLogger().error("Impossible to abort the job" + jobId + " => trying to stop it...", e); + getLogger().logJob(LogLevel.WARNING, this, "CLEAR_RESOURCES", "Impossible to abort the job \"" + jobId + "\" => trying to stop it...", e); stop(); } } // Remove this job from its execution manager: - try{ - if (getJobList() != null) - getJobList().getExecutionManager().remove(this); - }catch(UWSException ue){ - getLogger().error("Impossible to remove the job " + jobId + " from its execution manager !", ue); - } + if (getJobList() != null) + getJobList().getExecutionManager().remove(this); thread = null; + // Clear all uploaded files: + Iterator files = inputParams.getFiles(); + UploadFile upl; + while(files.hasNext()){ + upl = files.next(); + try{ + upl.deleteFile(); + }catch(IOException ioe){ + getLogger().logJob(LogLevel.ERROR, this, "CLEAR_RESOURCES", "Impossible to delete the file uploaded as parameter \"" + upl.paramName + "\" (" + upl.getLocation() + ") of the job \"" + jobId + "\"!", null); + } + } + // Clear all results file: for(Result r : results.values()){ try{ getFileManager().deleteResult(r, this); }catch(IOException ioe){ - getLogger().error("Impossible to delete the file associated with the result '" + r.getId() + "' of the job " + jobId + " !", ioe); + getLogger().logJob(LogLevel.ERROR, this, "CLEAR_RESOURCES", "Impossible to delete the file associated with the result '" + r.getId() + "' of the job \"" + jobId + "\"!", ioe); } } @@ -1536,9 +1448,11 @@ public class UWSJob extends SerializableUWSObject { try{ getFileManager().deleteError(errorSummary, this); }catch(IOException ioe){ - getLogger().error("Impossible to delete the file associated with the error '" + errorSummary.message + "' of the job " + jobId + " !", ioe); + getLogger().logJob(LogLevel.ERROR, this, "CLEAR_RESOURCES", "Impossible to delete the file associated with the error '" + errorSummary.message + "' of the job \"" + jobId + "\"!", ioe); } } + + getLogger().logJob(LogLevel.INFO, this, "CLEAR_RESOURCES", "Resources associated with the job \"" + getJobId() + "\" have been successfully freed.", null); } /* ******************* */ @@ -1629,7 +1543,7 @@ public class UWSJob extends SerializableUWSObject { } if (errors != null) - getLogger().error("Some observers of \"" + jobId + "\" can not have been updated:\n" + errors); + getLogger().logJob(LogLevel.WARNING, this, "NOTIFY", "Some observers of the job \"" + jobId + "\" can not have been updated:\n" + errors, null); } /* **************** */ @@ -1652,9 +1566,9 @@ public class UWSJob extends SerializableUWSObject { /* SERIALIZATION */ /* ************* */ @Override - public String serialize(UWSSerializer serializer, JobOwner user) throws UWSException{ + public String serialize(UWSSerializer serializer, JobOwner user) throws UWSException, Exception{ if (user != null && !user.equals(getOwner()) && !user.hasReadPermission(this)) - throw UWSExceptionFactory.readPermissionDenied(user, false, getJobId()); + throw new UWSException(UWSException.PERMISSION_DENIED, UWSExceptionFactory.readPermissionDenied(user, false, getJobId())); return serializer.getJob(this, true); } @@ -1667,11 +1581,11 @@ public class UWSJob extends SerializableUWSObject { * * @return The serialized job attribute (or the whole job if attributes is an empty array or is null). * - * @throws UWSException If there is an error during the serialization. + * @throws Exception If there is an unexpected error during the serialization. * * @see UWSSerializer#getJob(UWSJob, String[], boolean) */ - public String serialize(String[] attributes, UWSSerializer serializer) throws UWSException{ + public String serialize(String[] attributes, UWSSerializer serializer) throws Exception{ return serializer.getJob(this, attributes, true); } @@ -1682,30 +1596,27 @@ public class UWSJob extends SerializableUWSObject { * @param attributes The name of the attribute to serialize (if null, the whole job will be serialized). * @param serializer The serializer to use. * - * @throws UWSException If there is an error during the serialization. + * @throws Exception If there is an unexpected error during the serialization. * * @see #serialize(String[], UWSSerializer) */ - public void serialize(ServletOutputStream output, String[] attributes, UWSSerializer serializer) throws UWSException{ + public void serialize(ServletOutputStream output, String[] attributes, UWSSerializer serializer) throws UWSException, IOException, Exception{ String errorMsgPart = null; if (attributes == null || attributes.length <= 0) - errorMsgPart = "the job " + toString(); + errorMsgPart = "the job \"" + getJobId() + "\""; else - errorMsgPart = "the given attribute \"" + errorMsgPart + "\" of {" + toString() + "}"; + errorMsgPart = "the given attribute \"" + attributes[0] + "\" of the job \"" + getJobId() + "\""; if (output == null) - throw UWSExceptionFactory.missingOutputStream("impossible to serialize " + errorMsgPart + "."); - - try{ - String serialization = serialize(attributes, serializer); - if (serialization == null) - throw UWSExceptionFactory.incorrectSerialization("NULL", errorMsgPart); - else{ - output.print(serialization); - output.flush(); - } - }catch(IOException ex){ - throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, ex, "IOException => impossible to serialize " + errorMsgPart + "."); + throw new NullPointerException("Missing serialization output stream when serializing " + errorMsgPart + "!"); + + String serialization = serialize(attributes, serializer); + if (serialization == null){ + getLogger().logJob(LogLevel.ERROR, this, "SERIALIZE", "Error while serializing " + errorMsgPart + ": NULL was returned.", null); + throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, "Incorrect serialization value (=NULL) ! => impossible to serialize " + errorMsgPart + "."); + }else{ + output.print(serialization); + output.flush(); } } diff --git a/src/uws/job/manager/AbstractQueuedExecutionManager.java b/src/uws/job/manager/AbstractQueuedExecutionManager.java index a2e1d40..41f84a8 100644 --- a/src/uws/job/manager/AbstractQueuedExecutionManager.java +++ b/src/uws/job/manager/AbstractQueuedExecutionManager.java @@ -16,7 +16,8 @@ package uws.job.manager; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.util.Iterator; @@ -26,28 +27,34 @@ import java.util.Vector; import uws.UWSException; import uws.UWSToolBox; - -import uws.job.ErrorType; import uws.job.ExecutionPhase; import uws.job.UWSJob; import uws.service.log.UWSLog; +import uws.service.log.UWSLog.LogLevel; /** *

    Abstract implementation of the interface {@link ExecutionManager} which lets managing an execution queue.

    + * *

    * When calling {@link #execute(UWSJob)}, ALL jobs are put into the list of queued jobs (so their phase is changed * to {@link ExecutionPhase#QUEUED}). A call to {@link #refresh()}, reads this list and tries to execute the first job of the list. * The function {@link #isReadyForExecution(UWSJob)} decides whether the first job of the queue can be executed NOW or not. *

    - *

    - * NOTE: The order of queued jobs is preserved: it is implemented by a FIFO queue. - *

    * - * @author Grégory Mantelet (CDS) - * @version 05/2012 + *

    Note: + * The order of queued jobs is preserved: it is implemented by a FIFO queue. + *

    + * + *

    Note: + * After a call to {@link #stopAll()}, this manager is still able to execute new jobs. + * Except if it was not possible to stop them properly, stopped jobs could be executed again by calling + * afterwards {@link #execute(UWSJob)} with these jobs in parameter. + *

    + * + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (12/2014) */ public abstract class AbstractQueuedExecutionManager implements ExecutionManager { - private static final long serialVersionUID = 1L; /** List of running jobs. */ protected Map runningJobs; @@ -72,18 +79,22 @@ public abstract class AbstractQueuedExecutionManager implements ExecutionManager /* ***************** */ /* GETTERS & SETTERS */ /* ***************** */ + @Override public final Iterator getRunningJobs(){ return runningJobs.values().iterator(); } + @Override public final int getNbRunningJobs(){ return runningJobs.size(); } + @Override public final Iterator getQueuedJobs(){ return queuedJobs.iterator(); } + @Override public final int getNbQueuedJobs(){ return queuedJobs.size(); } @@ -102,6 +113,7 @@ public abstract class AbstractQueuedExecutionManager implements ExecutionManager * of the result of this function, the given job will be put in the queue or it will be executed. * * @param jobToExecute + * * @return true if the given job can be executed NOW (=> it will be executed), false otherwise (=> it will be put in the queue). */ public abstract boolean isReadyForExecution(UWSJob jobToExecute); @@ -111,11 +123,15 @@ public abstract class AbstractQueuedExecutionManager implements ExecutionManager /* **************************** */ /** *

    Removes the first queued job(s) from the queue and executes it (them) - * ONLY IF it (they) can be executed (see {@link #isReadyForExecution(AbstractJob)}).

    + * ONLY IF it (they) can be executed (see {@link #isReadyForExecution(UWSJob)}).

    * - *

    Note: Nothing is done if there is no queue.

    + *

    Note: + * Nothing is done if there is no queue. + *

    * - * @throws UWSException If there is an error during the phase transition of one or more jobs. + *

    Note: + * If any error occurs while refreshing this manager, it SHOULD be logged using the service logger. + *

    * * @see #hasQueue() * @see #isReadyForExecution(UWSJob) @@ -123,25 +139,22 @@ public abstract class AbstractQueuedExecutionManager implements ExecutionManager * * @see uws.job.manager.ExecutionManager#refresh() */ - public synchronized final void refresh() throws UWSException{ + @Override + public synchronized final void refresh(){ // Return immediately if no queue: if (!hasQueue()) return; - String allMsg = null; // the concatenation of all errors which may occur - // Start the first job of the queue while it can be executed: + UWSJob jobToStart; while(!queuedJobs.isEmpty() && isReadyForExecution(queuedJobs.firstElement())){ + jobToStart = queuedJobs.remove(0); try{ - startJob(queuedJobs.remove(0)); + startJob(jobToStart); }catch(UWSException ue){ - allMsg = ((allMsg == null) ? "ERRORS THAT OCCURED WHILE REFRESHING THE EXECUTION MANAGER:" : allMsg) + "\n\t- " + ue.getMessage(); + logger.logJob(LogLevel.ERROR, jobToStart, "START", "Can not start the job \"" + jobToStart.getJobId() + "\"! This job is not any more part of its execution manager.", ue); } } - - // Throw one error for all jobs that can not have been executed: - if (allMsg != null) - throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, allMsg, ErrorType.TRANSIENT); } /** @@ -165,28 +178,28 @@ public abstract class AbstractQueuedExecutionManager implements ExecutionManager /** *

    Refreshes this manager and then put the given job into the queue (if it is not already into it).

    * + *

    Note: + * If any error occurs while executing the given job, it SHOULD be logged using the service logger. + *

    + * * @param jobToExecute The job to execute. - * @return The resulting execution phase of the given job ({@link ExecutionPhase#EXECUTING EXECUTING} or {@link ExecutionPhase#QUEUED QUEUED} or null if the given job is null). * - * @throws UWSException If there is an error while changing the execution phase of the given job or if the job is already finished. + * @return The resulting execution phase of the given job ({@link ExecutionPhase#EXECUTING EXECUTING} or {@link ExecutionPhase#QUEUED QUEUED} or null if the given job is null). * * @see #refresh() - * @see AbstractJob#isRunning() + * @see UWSJob#isRunning() * @see #isReadyForExecution(UWSJob) * @see UWSJob#setPhase(ExecutionPhase) * - * @see uws.job.manager.ExecutionManager#execute(AbstractJob) + * @see uws.job.manager.ExecutionManager#execute(UWSJob) */ - public synchronized final ExecutionPhase execute(final UWSJob jobToExecute) throws UWSException{ + @Override + public synchronized final ExecutionPhase execute(final UWSJob jobToExecute){ if (jobToExecute == null) return null; // Refresh the list of running jobs before all: - try{ - refresh(); - }catch(UWSException ue){ - logger.error("Impossible to refresh the execution manager !", ue); - } + refresh(); // If the job is already running, ensure it is in the list of running jobs: if (jobToExecute.isRunning()) @@ -199,12 +212,16 @@ public abstract class AbstractQueuedExecutionManager implements ExecutionManager }// Otherwise, change the phase to QUEUED, put it into the queue and then refresh the queue: else{ - if (jobToExecute.getPhase() != ExecutionPhase.QUEUED) - jobToExecute.setPhase(ExecutionPhase.QUEUED); + try{ + if (jobToExecute.getPhase() != ExecutionPhase.QUEUED) + jobToExecute.setPhase(ExecutionPhase.QUEUED); - if (!queuedJobs.contains(jobToExecute)){ - queuedJobs.add(jobToExecute); - refresh(); + if (!queuedJobs.contains(jobToExecute)){ + queuedJobs.add(jobToExecute); + refresh(); + } + }catch(UWSException ue){ + logger.logJob(LogLevel.ERROR, jobToExecute, "QUEUE", "Can not set the job \"" + jobToExecute.getJobId() + "\" in the QUEUED phase!", ue); } } @@ -212,15 +229,52 @@ public abstract class AbstractQueuedExecutionManager implements ExecutionManager } /** - * Removes the given job from the lists of queued and running jobs and then refreshes the manager. + *

    Removes the given job from the lists of queued and running jobs and then refreshes the manager.

    + * + *

    Note: + * If any error occurs while removing a job from this manager, it SHOULD be logged using the service logger. + *

    * * @see uws.job.manager.ExecutionManager#remove(uws.job.UWSJob) */ - public final synchronized void remove(final UWSJob jobToRemove) throws UWSException{ + @Override + public final synchronized void remove(final UWSJob jobToRemove){ if (jobToRemove != null){ runningJobs.remove(jobToRemove.getJobId()); queuedJobs.remove(jobToRemove); refresh(); } } + + @Override + public final synchronized void stopAll(){ + // Set back all queued jobs to the PENDING phase: + for(UWSJob qj : queuedJobs){ + try{ + qj.setPhase(ExecutionPhase.PENDING, true); + }catch(UWSException ue){ + if (logger != null) + logger.logJob(LogLevel.WARNING, qj, "ABORT", "Can not set back the job to the PENDING phase.", ue); + } + } + + // Empty the queue: + queuedJobs.clear(); + + // Stop all running jobs and set them back to the PENDING phase: + for(UWSJob rj : runningJobs.values()){ + try{ + // Stop the job: + rj.abort(); + // Set its phase back to PENDING: + rj.setPhase(ExecutionPhase.PENDING, true); + }catch(UWSException ue){ + if (logger != null) + logger.logJob(LogLevel.WARNING, rj, "ABORT", "Can not stop the job nicely. The thread may continue to run until its end.", ue); + } + } + + // Empty the list of running jobs: + runningJobs.clear(); + } } diff --git a/src/uws/job/manager/DefaultDestructionManager.java b/src/uws/job/manager/DefaultDestructionManager.java index 24bff3e..8a0314c 100644 --- a/src/uws/job/manager/DefaultDestructionManager.java +++ b/src/uws/job/manager/DefaultDestructionManager.java @@ -16,11 +16,11 @@ package uws.job.manager; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.io.Serializable; - import java.util.Comparator; import java.util.Date; import java.util.Timer; @@ -34,19 +34,27 @@ import uws.job.UWSJob; * The default implementation of the {@link DestructionManager} interface. * Its goal is to manage the automatic destruction any given jobs. *

    + * *

    * Jobs can be added thanks to {@link #update(UWSJob)} and removed with {@link #remove(UWSJob)}. * All added jobs are stored in a {@link TreeSet} which sorts them by ascending destruction time. * The job which must be destroyed in first is used to start a timer. * This one will destroyed the job once its destruction time is reached. *

    + * *

    * The list of jobs to destroy is supposed to be updated each time the destruction time of a job is changed. This update works only if * the job knows its jobs list ({@link UWSJob#getJobList()} != null) and its jobs list has a destruction manager. *

    * - * @author Grégory Mantelet (CDS) - * @version 05/2012 + *

    Note: + * The {@link #stop()} function lets stop this manager to watch for destructions of job until {@link #refresh()} or + * {@link #update(UWSJob)} or {@link #remove(UWSJob)} is called. When stopped, the inner timer is canceled and set + * to NULL ; no more thread resources is used. + *

    + * + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (12/2014) */ public class DefaultDestructionManager implements DestructionManager { private static final long serialVersionUID = 1L; @@ -83,7 +91,8 @@ public class DefaultDestructionManager implements DestructionManager { /** * Stops the timer if running and set to null {@link #timDestruction}, {@link #currentDate} and {@link #currentJob}. */ - protected synchronized final void stop(){ + @Override + public synchronized final void stop(){ if (timDestruction != null) timDestruction.cancel(); timDestruction = null; @@ -111,18 +120,22 @@ public class DefaultDestructionManager implements DestructionManager { /** *

    Returns true if {@link #currentDate} is different from null.

    */ + @Override public final boolean isRunning(){ return currentDate != null; } + @Override public final Date getNextDestruction(){ return currentDate; } + @Override public final String getNextJobToDestroy(){ return (currentJob == null) ? null : currentJob.getJobId(); } + @Override public final int getNbJobsToDestroy(){ return jobsToDestroy.size() + (isRunning() ? 1 : 0); } @@ -147,6 +160,7 @@ public class DefaultDestructionManager implements DestructionManager { * @see #stop() * @see #destroyJob(UWSJob) */ + @Override public synchronized void refresh(){ // Finish the current timer if... if (isRunning()){ @@ -196,6 +210,7 @@ public class DefaultDestructionManager implements DestructionManager { * @see #destroyJob(UWSJob) * @see #refresh() */ + @Override public synchronized void update(UWSJob job){ if (job != null && job.getJobList() != null && job.getDestructionTime() != null){ if (job.getDestructionTime().before(new Date())) @@ -217,6 +232,7 @@ public class DefaultDestructionManager implements DestructionManager { * @see #stop() * @see #refresh() */ + @Override public synchronized void remove(UWSJob job){ if (job == null) return; diff --git a/src/uws/job/manager/DefaultExecutionManager.java b/src/uws/job/manager/DefaultExecutionManager.java index e00bda0..7049aaf 100644 --- a/src/uws/job/manager/DefaultExecutionManager.java +++ b/src/uws/job/manager/DefaultExecutionManager.java @@ -16,7 +16,8 @@ package uws.job.manager; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.util.Iterator; @@ -24,38 +25,53 @@ import java.util.LinkedHashMap; import java.util.Map; import uws.UWSException; -import uws.UWSExceptionFactory; - +import uws.UWSToolBox; import uws.job.ExecutionPhase; import uws.job.UWSJob; +import uws.service.log.UWSLog; +import uws.service.log.UWSLog.LogLevel; /** *

    Default implementation of the ExecutionManager interface.

    * *

    This manager does not have a queue. That is to say that all jobs are always immediately starting. * Consequently this manager is just used to gather all running jobs.

    + * + *

    Note: + * After a call to {@link #stopAll()}, this manager is still able to execute new jobs. + * Except if it was not possible to stop them properly, stopped jobs could be executed again by calling + * afterwards {@link #execute(UWSJob)} with these jobs in parameter. + *

    * - * @author Grégory Mantelet (CDS) - * @version 05/2012 + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (12/2014) */ public class DefaultExecutionManager implements ExecutionManager { - private static final long serialVersionUID = 1L; /** List of running jobs. */ protected Map runningJobs; + protected final UWSLog logger; + public DefaultExecutionManager(){ + this(null); + } + + public DefaultExecutionManager(final UWSLog logger){ runningJobs = new LinkedHashMap(10); + this.logger = (logger == null) ? UWSToolBox.getDefaultLogger() : logger; } /* ******* */ /* GETTERS */ /* ******* */ + @Override public final Iterator getRunningJobs(){ return runningJobs.values().iterator(); } + @Override public final int getNbRunningJobs(){ return runningJobs.size(); } @@ -65,6 +81,7 @@ public class DefaultExecutionManager implements ExecutionManager { * * @see uws.job.manager.ExecutionManager#getQueuedJobs() */ + @Override public final Iterator getQueuedJobs(){ return new Iterator(){ @Override @@ -89,6 +106,7 @@ public class DefaultExecutionManager implements ExecutionManager { * * @see uws.job.manager.ExecutionManager#getNbQueuedJobs() */ + @Override public final int getNbQueuedJobs(){ return 0; } @@ -98,11 +116,13 @@ public class DefaultExecutionManager implements ExecutionManager { * * @see uws.job.manager.ExecutionManager#refresh() */ - public final void refresh() throws UWSException{ + @Override + public final void refresh(){ ; } - public synchronized ExecutionPhase execute(final UWSJob jobToExecute) throws UWSException{ + @Override + public synchronized ExecutionPhase execute(final UWSJob jobToExecute){ if (jobToExecute == null) return null; @@ -113,19 +133,42 @@ public class DefaultExecutionManager implements ExecutionManager { // If the job is already finished, ensure it is not any more in the list of running jobs: else if (jobToExecute.isFinished()){ runningJobs.remove(jobToExecute); - throw UWSExceptionFactory.incorrectPhaseTransition(jobToExecute.getJobId(), jobToExecute.getPhase(), ExecutionPhase.EXECUTING); + logger.logJob(LogLevel.WARNING, jobToExecute, "START", "Job \"" + jobToExecute.getJobId() + "\" already finished!", null); // Otherwise start it: }else{ - jobToExecute.start(false); - runningJobs.put(jobToExecute.getJobId(), jobToExecute); + try{ + jobToExecute.start(false); + runningJobs.put(jobToExecute.getJobId(), jobToExecute); + }catch(UWSException ue){ + logger.logJob(LogLevel.ERROR, jobToExecute, "START", "Can not start the job \"" + jobToExecute.getJobId() + "\"! This job is not any more part of its execution manager.", ue); + } } return jobToExecute.getPhase(); } - public synchronized void remove(final UWSJob jobToRemove) throws UWSException{ + @Override + public synchronized void remove(final UWSJob jobToRemove){ if (jobToRemove != null) runningJobs.remove(jobToRemove.getJobId()); } + + @Override + public synchronized void stopAll(){ + // Stop all running jobs: + for(UWSJob rj : runningJobs.values()){ + try{ + // Stop the job: + rj.abort(); + // Set its phase back to PENDING: + rj.setPhase(ExecutionPhase.PENDING, true); + }catch(UWSException ue){ + if (logger != null) + logger.logJob(LogLevel.WARNING, rj, "ABORT", "Can not stop the job nicely. The thread may continue to run until its end.", ue); + } + } + // Empty the list of running jobs: + runningJobs.clear(); + } } diff --git a/src/uws/job/manager/DestructionManager.java b/src/uws/job/manager/DestructionManager.java index 5bb8906..022ecbc 100644 --- a/src/uws/job/manager/DestructionManager.java +++ b/src/uws/job/manager/DestructionManager.java @@ -16,16 +16,15 @@ package uws.job.manager; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.io.Serializable; - import java.util.Date; import uws.job.JobList; import uws.job.UWSJob; - import uws.service.UWS; /** @@ -50,8 +49,8 @@ import uws.service.UWS; *
    *

    * - * @author Grégory Mantelet (CDS) - * @version 05/2012 + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (12/2014) * * @see DefaultDestructionManager */ @@ -114,4 +113,15 @@ public interface DestructionManager extends Serializable { * @param job The job to remove. */ public void remove(UWSJob job); + + /** + *

    Stop watching the destruction of jobs.

    + * + *

    Note: + * A subsequent call to {@link #update(UWSJob)} may enable again this manager. + *

    + * + * @since 4.1 + */ + public void stop(); } diff --git a/src/uws/job/manager/ExecutionManager.java b/src/uws/job/manager/ExecutionManager.java index 7232cb5..e50d86d 100644 --- a/src/uws/job/manager/ExecutionManager.java +++ b/src/uws/job/manager/ExecutionManager.java @@ -16,13 +16,12 @@ package uws.job.manager; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.util.Iterator; -import uws.UWSException; - import uws.job.ExecutionPhase; import uws.job.UWSJob; @@ -36,8 +35,8 @@ import uws.job.UWSJob; * and to end ({@link #remove(UWSJob)}). *

    * - * @author Grégory Mantelet (CDS) - * @version 05/2012 + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (12/2014) */ public interface ExecutionManager { @@ -70,31 +69,50 @@ public interface ExecutionManager { public int getNbQueuedJobs(); /** - * Refreshes the lists of running and queued jobs. + *

    Refreshes the lists of running and queued jobs.

    * - * @throws UWSException If there is an error while refreshing this manager. + *

    Note: + * If any error occurs while refreshing this manager, it SHOULD be logged using the service logger. + *

    */ - public void refresh() throws UWSException; + public void refresh(); /** *

    Lets deciding whether the given job can start immediately or whether it must be put in the queue.

    * + *

    Note: + * If any error occurs while executing the given job, it SHOULD be logged using the service logger. + *

    + * * @param job The job to execute. * @return The resulting execution phase of the given job. * - * @throws UWSException If there is an error while changing the execution phase of the given job or if any other error occurs. - * * @see UWSJob#start(boolean) * @see UWSJob#setPhase(ExecutionPhase) */ - public ExecutionPhase execute(final UWSJob job) throws UWSException; + public ExecutionPhase execute(final UWSJob job); /** - * Removes the job from this manager whatever is its current execution phase. + *

    Removes the job from this manager whatever is its current execution phase.

    + * + *

    Note: + * If any error occurs while removing a job from this manager, it SHOULD be logged using the service logger. + *

    * * @param jobToRemove The job to remove. + */ + public void remove(final UWSJob jobToRemove); + + /** + *

    Stop all running jobs. No more job, even the queued ones, must be executed after a call to this function. + * All stopped or aborted queued jobs should be set forcedly back to the PENDING status.

    + * + *

    Note: + * A call to {@link #execute(UWSJob)} would re-activate this manager. However jobs stopped or + * aborted using this function might not be starting again. These behaviors at implementation-dependent. + *

    * - * @throws UWSException If there is an error while refreshing the list of running jobs or if any other error occurs. + * @since 4.1 */ - public void remove(final UWSJob jobToRemove) throws UWSException; + public void stopAll(); } diff --git a/src/uws/job/manager/QueuedExecutionManager.java b/src/uws/job/manager/QueuedExecutionManager.java index 9fd9de4..51402c8 100644 --- a/src/uws/job/manager/QueuedExecutionManager.java +++ b/src/uws/job/manager/QueuedExecutionManager.java @@ -16,11 +16,11 @@ package uws.job.manager; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import uws.UWSException; - import uws.job.UWSJob; import uws.service.log.UWSLog; @@ -29,11 +29,10 @@ import uws.service.log.UWSLog; * if there are more running jobs than a given number, the jobs to execute are put in the queue until a running job stops. * The order of queued jobs are preserved: it is implemented by a FIFO queue.

    * - * @author Grégory Mantelet (CDS) - * @version 05/2012 + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (08/2014) */ public class QueuedExecutionManager extends AbstractQueuedExecutionManager { - private static final long serialVersionUID = 1L; /** The maximum number of running jobs. */ protected int nbMaxRunningJobs = NO_QUEUE; @@ -70,11 +69,7 @@ public class QueuedExecutionManager extends AbstractQueuedExecutionManager { public final void setNoQueue(){ nbMaxRunningJobs = NO_QUEUE; - try{ - refresh(); - }catch(UWSException ue){ - logger.error("Impossible to refresh the execution manager !", ue); - } + refresh(); } /** @@ -89,8 +84,10 @@ public class QueuedExecutionManager extends AbstractQueuedExecutionManager { /** *

    Sets the maximum number of running jobs.

    * - *

    Note: If the new maximum number of running jobs is increasing the list of running jobs is immediately updated - * BUT NOT IF it is decreasing (that is to say, running jobs will not be interrupted to be put in the queue, they continue to run) !

    + *

    Note: + * If the new maximum number of running jobs is increasing the list of running jobs is immediately updated + * BUT NOT IF it is decreasing (that is to say, running jobs will not be interrupted to be put in the queue, they continue to run) ! + *

    * * @param maxRunningJobs The new maximum number of running jobs ({@link #NO_QUEUE} or a negative value means no maximum number of running jobs: there will be no queue any more). * diff --git a/src/uws/job/parameters/DestructionTimeController.java b/src/uws/job/parameters/DestructionTimeController.java index 103b64c..bf86999 100644 --- a/src/uws/job/parameters/DestructionTimeController.java +++ b/src/uws/job/parameters/DestructionTimeController.java @@ -16,38 +16,42 @@ package uws.job.parameters; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.io.Serializable; - import java.text.ParseException; import java.util.Calendar; import java.util.Date; -import uws.UWSException; -import uws.UWSExceptionFactory; -import uws.job.UWSJob; +import uws.ISO8601Format; +import uws.UWSException; /** *

    - * Let's controlling the destruction time of all jobs managed by a UWS. Thus it is possible to set a default and a maximum value. + * Let controlling the destruction time of all jobs managed by a UWS. Thus it is possible to set a default and a maximum value. * Moreover you can indicate whether the destruction time of jobs can be modified by the user or not. *

    * - *

    - * Notes: - *

      - *
    • By default, the destruction time can be modified by anyone without any limitation. - * There is no default value (that means jobs may stay forever).
    • - *
    • You can specify a destruction time (default or maximum value) in two ways: - * by an exact date-time or by an interval of time from the initialization (expressed in the second, minutes, hours, days, months or years).
    • - *
    - * - *

    + *

    Notes: + *

      + *
    • By default, the destruction time can be modified by anyone without any limitation. + * There is no default value (that means jobs may stay forever).
    • + *
    • You can specify a destruction time (default or maximum value) in two ways: + * by an exact date-time or by an interval of time from the initialization (expressed in the second, minutes, hours, days, months or years).
    • + *
    + *

    * - * @author Grégory Mantelet (CDS) - * @version 05/2012 + *

    The logic of the destruction time is set in this class. Here it is:

    + *
      + *
    • If no value is specified by the UWS client, the default value is returned.
    • + *
    • If no default value is provided, the maximum destruction date is returned.
    • + *
    • If no maximum value is provided, there is no destruction.
    • + *
    + * + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (11/2014) */ public class DestructionTimeController implements InputParamController, Serializable { private static final long serialVersionUID = 1L; @@ -56,7 +60,7 @@ public class DestructionTimeController implements InputParamController, Serializ * Represents a date/time field. * * @author Grégory Mantelet (CDS) - * @version 02/2011 + * @version 4.0 (02/2011) * * @see Calendar */ @@ -95,33 +99,38 @@ public class DestructionTimeController implements InputParamController, Serializ protected boolean allowModification = true; @Override - public Object check(Object value) throws UWSException{ + public Object check(final Object value) throws UWSException{ + // If no value, return the default one: if (value == null) - return null; + return getDefault(); + // Otherwise, parse the date: Date date = null; if (value instanceof Date) date = (Date)value; else if (value instanceof String){ String strValue = (String)value; try{ - date = UWSJob.dateFormat.parse(strValue); + date = ISO8601Format.parseToDate(strValue); }catch(ParseException pe){ - throw UWSExceptionFactory.badFormat(null, UWSJob.PARAM_DESTRUCTION_TIME, strValue, null, "A date not yet expired."); + throw new UWSException(UWSException.BAD_REQUEST, pe, "Wrong date format for the destruction time parameter: \"" + strValue + "\"! Dates must be formatted in ISO8601 (\"yyyy-MM-dd'T'hh:mm:ss[.sss]['Z'|[+|-]hh:mm]\", fields inside brackets are optional)."); } }else - throw UWSExceptionFactory.badFormat(null, UWSJob.PARAM_DESTRUCTION_TIME, value.toString(), value.getClass().getName(), "A date not yet expired."); + throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, "Wrong type for the destruction time parameter: class \"" + value.getClass().getName() + "\"! It should be a Date or a string containing a date formatted in IS8601 (\"yyyy-MM-dd'T'hh:mm:ss[.sss]['Z'|[+|-]hh:mm]\", fields inside brackets are optional)."); + // Compare it to the maximum destruction time: if after, set the date to the maximum allowed date: Date maxDate = getMaxDestructionTime(); if (maxDate != null && date.after(maxDate)) - throw new UWSException(UWSException.BAD_REQUEST, "The UWS limits " + ((defaultInterval > NO_INTERVAL) ? ("the DESTRUCTION INTERVAL (since now) to " + maxInterval + " " + maxIntervalField.name().toLowerCase() + "s") : ("the DESTRUCTION TIME to " + maxDate)) + " !"); + date = maxDate; + // Return the parsed date: return date; } @Override public Object getDefault(){ - return getDefaultDestructionTime(); + Date defaultDate = getDefaultDestructionTime(); + return (defaultDate == null) ? getMaxDestructionTime() : defaultDate; } /* ***************** */ @@ -307,9 +316,15 @@ public class DestructionTimeController implements InputParamController, Serializ * @param timeField The unit of the interval (null means the job may stay forever). */ public final void setMaxDestructionInterval(int maxDestructionInterval, DateField timeField){ - this.maxInterval = maxDestructionInterval; - maxIntervalField = timeField; - maxTime = null; + if (maxDestructionInterval <= 0 || timeField == null){ + this.maxInterval = NO_INTERVAL; + maxIntervalField = null; + maxTime = null; + }else{ + this.maxInterval = maxDestructionInterval; + maxIntervalField = timeField; + maxTime = null; + } } /** @@ -317,6 +332,7 @@ public class DestructionTimeController implements InputParamController, Serializ * * @return true if the destruction time can be modified, false otherwise. */ + @Override public final boolean allowModification(){ return allowModification; } diff --git a/src/uws/job/parameters/ExecutionDurationController.java b/src/uws/job/parameters/ExecutionDurationController.java index 7c2d7ee..670d02f 100644 --- a/src/uws/job/parameters/ExecutionDurationController.java +++ b/src/uws/job/parameters/ExecutionDurationController.java @@ -16,48 +16,78 @@ package uws.job.parameters; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.io.Serializable; import uws.UWSException; -import uws.UWSExceptionFactory; - import uws.job.UWSJob; /** *

    - * Lets controlling the execution duration of all jobs managed by a UWS. Thus it is possible to set a default and a maximum value. + * Let controlling the execution duration of all jobs managed by a UWS. Thus it is possible to set a default and a maximum value. * Moreover you can indicate whether the execution duration of jobs can be modified by the user or not. *

    * - *

    - * Note: - * By default, the execution duration can be modified by anyone without any limitation. - * The default value is {@link UWSJob#UNLIMITED_DURATION}. - * - *

    + *

    Note: the execution duration is always expressed in seconds.

    + * + *

    Note: + * By default, the execution duration can be modified by anyone without any limitation. + * The default and maximum value is {@link UWSJob#UNLIMITED_DURATION}. + *

    * - * @author Grégory Mantelet (CDS) - * @version 05/2012 + *

    The logic of the execution duration is set in this class. Here it is:

    + *
      + *
    • If no value is specified by the UWS client, the default value is returned.
    • + *
    • If no default value is provided, the maximum duration is returned.
    • + *
    • If no maximum value is provided, there is no limit (={@link UWSJob#UNLIMITED_DURATION}).
    • + *
    + * + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (11/2014) */ public class ExecutionDurationController implements InputParamController, Serializable { private static final long serialVersionUID = 1L; - /** The default duration. */ + /** The default duration (in seconds). */ protected long defaultDuration = UWSJob.UNLIMITED_DURATION; - /** The maximum duration. */ + /** The maximum duration (in seconds). */ protected long maxDuration = UWSJob.UNLIMITED_DURATION; /** Indicates whether the execution duration of jobs can be modified. */ protected boolean allowModification = true; - public ExecutionDurationController(){ - ; - } + /** + *

    Create a controller for the execution duration. + * By default, there is no maximum value and the default duration is {@link UWSJob#UNLIMITED_DURATION}.

    + * + *

    + * A default and/or maximum value can be set after creation using {@link #setDefaultExecutionDuration(long)} + * and {@link #setMaxExecutionDuration(long)}. By default this parameter can always be modified, but it can + * be forbidden using {@link #allowModification(boolean)}. + *

    + */ + public ExecutionDurationController(){} + /** + *

    Create a controller for the execution duration. + * The default and the maximum duration are initialized with the given parameters. + * The third parameter allows also to forbid the modification of the execution duration by the user, + * if set to false.

    + * + *

    + * A default and/or maximum value can be modified after creation using {@link #setDefaultExecutionDuration(long)} + * and {@link #setMaxExecutionDuration(long)}. The flag telling whether this parameter can be modified by the user + * can be changed using {@link #allowModification(boolean)}. + *

    + * + * @param defaultDuration Duration (in seconds) set by default to a job, when none is specified. + * @param maxDuration Maximum duration (in seconds) that can be set. If a greater value is provided by the user, an exception will be thrown by {@link #check(Object)}. + * @param allowModification true to allow the user to modify this value when creating a job, false otherwise. + */ public ExecutionDurationController(final long defaultDuration, final long maxDuration, final boolean allowModification){ setDefaultExecutionDuration(defaultDuration); setMaxExecutionDuration(maxDuration); @@ -66,31 +96,38 @@ public class ExecutionDurationController implements InputParamController, Serial @Override public Object getDefault(){ - return defaultDuration; + return (defaultDuration > 0) ? defaultDuration : getMaxExecutionDuration(); } @Override - public Object check(Object value) throws UWSException{ + public Object check(final Object value) throws UWSException{ + // If no value, return the default one: if (value == null) - return null; + return getDefault(); + // Otherwise, parse the given duration: Long duration = null; if (value instanceof Long) duration = (Long)value; + else if (value instanceof Integer) + duration = (long)(Integer)value; else if (value instanceof String){ String strValue = (String)value; try{ duration = Long.parseLong(strValue); }catch(NumberFormatException nfe){ - throw UWSExceptionFactory.badFormat(null, UWSJob.PARAM_EXECUTION_DURATION, strValue, null, "A long value between " + UWSJob.UNLIMITED_DURATION + " and " + maxDuration + " (Default value: " + defaultDuration + ")."); + throw new UWSException(UWSException.BAD_REQUEST, "Wrong format for the maximum duration parameter: \"" + strValue + "\"! It should be a long numeric value between " + UWSJob.UNLIMITED_DURATION + " and " + maxDuration + " (Default value: " + defaultDuration + ")."); } }else - throw UWSExceptionFactory.badFormat(null, UWSJob.PARAM_EXECUTION_DURATION, null, value.getClass().getName(), "A long value between " + UWSJob.UNLIMITED_DURATION + " and " + maxDuration + " (Default value: " + defaultDuration + ")."); + throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, "Wrong type for the maximum duration parameter: class \"" + value.getClass().getName() + "\"! It should be long or a string containing only a long value."); - if (duration < UWSJob.UNLIMITED_DURATION) + // If the duration is negative or zero, set it to UNLIMITED: + if (duration <= 0) duration = UWSJob.UNLIMITED_DURATION; - else if (maxDuration > UWSJob.UNLIMITED_DURATION && duration > maxDuration) - throw new UWSException(UWSException.BAD_REQUEST, "The UWS limits the execution duration to maximum " + maxDuration + " seconds !"); + + // Set the maximum duration if the duration is greater than the maximum value: + if (maxDuration > 0 && (duration > maxDuration || duration <= 0)) + duration = maxDuration; return duration; } @@ -101,16 +138,19 @@ public class ExecutionDurationController implements InputParamController, Serial /** * Gets the default execution duration. * - * @return The default execution duration (0 or less mean an unlimited duration). + * @return The default execution duration (in seconds) (0 or less mean an unlimited duration). + * + * @deprecated This function is completely equivalent to {@link #getDefault()}. */ + @Deprecated public final long getDefaultExecutionDuration(){ - return defaultDuration; + return (Long)getDefault(); } /** * Sets the default execution duration. * - * @param defaultExecutionDuration The new default execution duration ({@link UWSJob#UNLIMITED_DURATION}, 0 or a negative value mean an unlimited duration). + * @param defaultExecutionDuration The new default execution duration (in seconds) ({@link UWSJob#UNLIMITED_DURATION}, 0 or a negative value mean an unlimited duration). */ public final boolean setDefaultExecutionDuration(long defaultExecutionDuration){ defaultExecutionDuration = (defaultExecutionDuration <= 0) ? UWSJob.UNLIMITED_DURATION : defaultExecutionDuration; @@ -126,7 +166,7 @@ public class ExecutionDurationController implements InputParamController, Serial /** * Gets the maximum execution duration. * - * @return The maximum execution duration (0 or less mean an unlimited duration). + * @return The maximum execution duration (in seconds) (0 or less mean an unlimited duration). */ public final long getMaxExecutionDuration(){ return maxDuration; @@ -135,7 +175,7 @@ public class ExecutionDurationController implements InputParamController, Serial /** * Sets the maximum execution duration. * - * @param maxExecutionDuration The maximum execution duration ({@link UWSJob#UNLIMITED_DURATION}, 0 or a negative value mean an unlimited duration). + * @param maxExecutionDuration The maximum execution duration (in seconds) ({@link UWSJob#UNLIMITED_DURATION}, 0 or a negative value mean an unlimited duration). */ public final void setMaxExecutionDuration(long maxExecutionDuration){ maxDuration = (maxExecutionDuration <= 0) ? UWSJob.UNLIMITED_DURATION : maxExecutionDuration; @@ -148,6 +188,7 @@ public class ExecutionDurationController implements InputParamController, Serial * * @return true if the execution duration can be modified, false otherwise. */ + @Override public final boolean allowModification(){ return allowModification; } diff --git a/src/uws/job/parameters/InputParamController.java b/src/uws/job/parameters/InputParamController.java index 72e6a33..5cf47ee 100644 --- a/src/uws/job/parameters/InputParamController.java +++ b/src/uws/job/parameters/InputParamController.java @@ -25,6 +25,7 @@ import uws.UWSException; *

    Lets controlling an input parameter of a UWS job.

    * * @author Grégory Mantelet (CDS) + * @version 4.0 */ public interface InputParamController { diff --git a/src/uws/job/parameters/StringParamController.java b/src/uws/job/parameters/StringParamController.java index 39b42bf..2eecd59 100644 --- a/src/uws/job/parameters/StringParamController.java +++ b/src/uws/job/parameters/StringParamController.java @@ -16,17 +16,17 @@ package uws.job.parameters; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import uws.UWSException; -import uws.UWSExceptionFactory; /** - * Lets controlling a String parameter. + * Let controlling a String parameter. * - * @author Grégory Mantelet (CDS) - * @version 06/2012 + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (09/2014) */ public class StringParamController implements InputParamController { @@ -133,11 +133,11 @@ public class StringParamController implements InputParamController { if (strValue.equalsIgnoreCase(v)) return v; } - throw UWSExceptionFactory.badFormat(null, paramName, strValue, null, getExpectedFormat()); + throw new UWSException(UWSException.BAD_REQUEST, "Unknown value for the parameter \"" + paramName + "\": \"" + strValue + "\". It should be " + getExpectedFormat()); }else return strValue; }else - throw UWSExceptionFactory.badFormat(null, paramName, value.toString(), value.getClass().getName(), getExpectedFormat()); + throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, "Wrong type for the parameter \"" + paramName + "\": \"" + value.getClass().getName() + "\"! It should be a String."); } /** @@ -146,13 +146,13 @@ public class StringParamController implements InputParamController { * @return A string which describes the format expected by this controller. */ protected final String getExpectedFormat(){ - if (possibleValues == null || possibleValues.length == 0){ - StringBuffer buffer = new StringBuffer("A String value among: "); + if (possibleValues != null && possibleValues.length > 0){ + StringBuffer buffer = new StringBuffer("a String value among: "); for(int i = 0; i < possibleValues.length; i++) buffer.append((i == 0) ? "" : ", ").append(possibleValues[i]); return buffer.toString(); }else - return "A String value."; + return "a String value."; } @Override diff --git a/src/uws/job/parameters/UWSParameters.java b/src/uws/job/parameters/UWSParameters.java index 767706a..734a79f 100644 --- a/src/uws/job/parameters/UWSParameters.java +++ b/src/uws/job/parameters/UWSParameters.java @@ -16,69 +16,91 @@ package uws.job.parameters; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ +import java.io.IOException; import java.text.ParseException; +import java.util.ArrayList; import java.util.Collection; +import java.util.Date; import java.util.Enumeration; +import java.util.HashMap; import java.util.Iterator; -import java.util.Date; -import java.util.Set; +import java.util.List; import java.util.Map; -import java.util.HashMap; - import java.util.Map.Entry; +import java.util.Set; import javax.servlet.http.HttpServletRequest; +import uws.ISO8601Format; import uws.UWSException; - import uws.job.UWSJob; - import uws.service.UWS; +import uws.service.request.UploadFile; /** - *

    Lets extracting all UWS standard and non-standard parameters from a map or a {@link HttpServletRequest}.

    + *

    Let extracting all UWS standard and non-standard parameters from a map.

    * *

    Input parameter check

    - *

    It is possible to check the value of some or all parameters by calling the function {@link InputParamController#check(Object)} - * of an {@link InputParamController} associated with the name of the parameter. Input parameter controllers can be - * provided at the creation of a {@link UWSParameters}. If none are given, default ones are used (see {@link #getDefaultUWSParamControllers()}).

    + *

    + * It is possible to check the value of some or all parameters by calling the function {@link InputParamController#check(Object)} + * of an {@link InputParamController} associated with the name of the parameter. Input parameter controllers can be + * provided at the creation of a {@link UWSParameters}. If none are given, default ones are used (see {@link #getDefaultControllers()}) + * for the standard UWS parameters (e.g. destruction time, duration, etc...). + *

    * *

    Default value

    - *

    By calling the function {@link #init()}, you set a default value to any parameter which has an {@link InputParamController} - * and which has not yet a value.

    - *

    The function {@link InputParamController#getDefault()} returns a default value for its associated parameter. - * This value must be obviously different from NULL.

    + *

    + * By calling the function {@link #init()}, you set a default value to any parameter which has an {@link InputParamController} + * and which has not yet a value. + *

    + *

    + * The function {@link InputParamController#getDefault()} returns a default value for its associated parameter. + * This value must be obviously different from NULL. + *

    * *

    Updating a {@link UWSParameters}

    - *

    It is possible to update a {@link UWSParameters} with another {@link UWSParameters} thanks to the function - * {@link #update(UWSParameters)}. In this case, no check is done since the values given by a - * {@link UWSParameters} should be theoretically already correct.

    - *

    In order to forbid the modification of some parameters after their initialization, you must associate an - * {@link InputParamController} with them and override the function {@link InputParamController#allowModification()} - * so that it returns false.

    + *

    + * It is possible to update a {@link UWSParameters} with another {@link UWSParameters} thanks to the function + * {@link #update(UWSParameters)}. In this case, no check is done since the values given by a + * {@link UWSParameters} should be theoretically already correct. + *

    + *

    + * In order to forbid the modification of some parameters after their initialization, you must associate an + * {@link InputParamController} with them and override the function {@link InputParamController#allowModification()} + * so that it returns false. + *

    * *

    Case sensitivity

    - *

    All UWS STANDARD parameters can be provided in any case: they will always be identified and updated. - * However any other parameter will be stored as it is provided: so with the same case. Thus, you must respect - * the case for all UWS additional parameters in your other operations on the parameters.

    - *

    If you want to identify your own parameters without case sensitivity, you must provides a list - * of all the additional parameters you are expected at the creation: see {@link #UWSParameters(HttpServletRequest, Collection, Map)} - * and {@link #UWSParameters(Map, Collection, Map)}.

    + *

    + * All UWS STANDARD parameters can be provided in any case: they will always be identified and updated. + * However any other parameter will be stored as it is provided: so with the same case. Thus, you must respect + * the case for all UWS additional parameters in your other operations on the parameters. + *

    + *

    + * If you want to identify your own parameters without case sensitivity, you must provides a list + * of all the additional parameters you are expected at the creation: see {@link #UWSParameters(HttpServletRequest, Collection, Map)} + * and {@link #UWSParameters(Map, Collection, Map)}. + *

    * *

    Additional parameters case normalization

    - *

    Indeed, the second parameter of these constructors (if != NULL) is used to normalize the name of the additional parameters so - * that they have exactly the given case.

    - *

    For instance, suppose that the given HttpServletRequest has a parameter named "foo" and - * you expect a parameter named "FOO" (only the case changes). By providing a second parameter - * which contains the entry "FOO", all parameters having the same name - even if the case is different - - * will be named "FOO".

    + *

    + * Indeed, the second parameter of these constructors (if != NULL) is used to normalize the name of the additional parameters so + * that they have exactly the given case. + *

    + *

    + * For instance, suppose that the request had a parameter named "foo" and + * you expect a parameter named "FOO" (only the case changes). By providing a second parameter + * which contains the entry "FOO", all parameters having the same name - even if the case is different - + * will be named "FOO". + *

    *

    In brief:

    *
      - *
    • With "FOO" in the second parameter of the constructor: {@link #get(String) get("FOO")} will return something if in the HttpServletRequest there is a parameter named: "foo", "FOO", "Foo", ...
    • - *
    • If the second parameter is empty, NULL or does not contain "FOO": {@link #get(String) get("FOO")} will return something if in the HttpServletRequest there is a parameter named exactly "FOO".
    • + *
    • With "FOO" in the second parameter of the constructor: {@link #get(String) get("FOO")} will return something if in the request there was a parameter named: "foo", "FOO", "Foo", ...
    • + *
    • If the second parameter is empty, NULL or does not contain "FOO": {@link #get(String) get("FOO")} will return something if in the request there was a parameter named exactly "FOO".
    • *
    * *

    UWS standard parameters

    @@ -89,11 +111,12 @@ import uws.service.UWS; *
  • executionDuration ({@link UWSJob#PARAM_EXECUTION_DURATION})
  • *
  • destruction ({@link UWSJob#PARAM_DESTRUCTION_TIME})
  • * - *

    note: All parameters stored under the parameter {@link UWSJob#PARAM_PARAMETERS} (that's to say, additional parameters) + *

    note 1: All parameters stored under the parameter {@link UWSJob#PARAM_PARAMETERS} (that's to say, additional parameters) * are also considered as READ/WRITE parameters !

    + *

    note 2: If several values have been submitted for the same UWS standard parameter, just the last occurrence is taken into account.

    * - * @author Grégory Mantelet (CDS) - * @version 06/2012 + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (12/2014) */ public class UWSParameters implements Iterable> { @@ -102,6 +125,10 @@ public class UWSParameters implements Iterable> { *

    Names of the UWS parameters whose the value can be modified by the user.

    */ protected final static String[] UWS_RW_PARAMETERS = new String[]{UWSJob.PARAM_PHASE,UWSJob.PARAM_RUN_ID,UWSJob.PARAM_EXECUTION_DURATION,UWSJob.PARAM_DESTRUCTION_TIME,UWSJob.PARAM_PARAMETERS}; + + /** Regular expression allowing to test which UWS parameters can be set. Actually, only: phase, runID, executionduration and destruction. */ + public final static String UWS_RW_PARAMETERS_REGEXP = ("(" + UWSJob.PARAM_PHASE + "|" + UWSJob.PARAM_RUN_ID + "|" + UWSJob.PARAM_EXECUTION_DURATION + "|" + UWSJob.PARAM_DESTRUCTION_TIME + ")").toLowerCase(); + /** *

    Read-Only parameters.

    *

    Names of the UWS parameters whose the value can NOT be modified by the user. These value are not kept. They are only ignored.

    @@ -119,7 +146,13 @@ public class UWSParameters implements Iterable> { * It is deleted (set to NULL) when there is a modification in the list of all parameters * (so in the function {@link #set(String, Object)}, {@link #update(UWSParameters)} and {@link #init()}).

    */ - private HashMap additionalParams = null; + private Map additionalParams = null; + + /** + * List of all uploaded files among the whole set of parameters. + * @since 4.1 + */ + protected List files = null; /** * List of the expected additional parameters. @@ -144,14 +177,14 @@ public class UWSParameters implements Iterable> { *

    Builds an empty list of UWS parameters.

    * *

    note: Even if no controllers is provided, this constructor sets the default - * input parameter controllers (see {@link #getDefaultUWSParamControllers()}).

    + * input parameter controllers (see {@link #getDefaultControllers()}).

    * * @param expectedAdditionalParams The names of all expected additional parameters (MAY BE NULL). * note: they will be identified with no case sensitivity * and stored with the same case as in this collection. * @param inputParamControllers Controllers of the input parameters (MAY BE NULL). * - * @see #getDefaultUWSParamControllers() + * @see #getDefaultControllers() */ public UWSParameters(final Collection expectedAdditionalParams, final Map inputParamControllers){ // Set the input parameter controllers: @@ -166,7 +199,7 @@ public class UWSParameters implements Iterable> { /** *

    Extracts and identifies all UWS standard and non-standard parameters from the given {@link HttpServletRequest}.

    * - *

    note: The default input parameter controllers are set by default (see {@link #getDefaultUWSParamControllers()}).

    + *

    note: The default input parameter controllers are set by default (see {@link #getDefaultControllers()}).

    * * @param request The request to parse to extract the parameters. * @@ -182,7 +215,7 @@ public class UWSParameters implements Iterable> { *

    Extracts and identifies all UWS standard and non-standard parameters from the given {@link HttpServletRequest}.

    * *

    note: Even if no controllers is provided, this constructor sets the default - * input parameter controllers (see {@link #getDefaultUWSParamControllers()}).

    + * input parameter controllers (see {@link #getDefaultControllers()}).

    * * @param request The request to parse to extract the parameters. * @param expectedAdditionalParams The names of all expected additional parameters. @@ -192,27 +225,66 @@ public class UWSParameters implements Iterable> { * * @throws UWSException If one of the given parameter is incorrect or badly formatted. * - * @see #UWSParameters(Collection, Map) + * @see #UWSParameters(Map, Collection, Map) */ - @SuppressWarnings("unchecked") public UWSParameters(final HttpServletRequest request, final Collection expectedAdditionalParams, final Map inputParamControllers) throws UWSException{ - this(expectedAdditionalParams, inputParamControllers); + this(getParameters(request), expectedAdditionalParams, inputParamControllers); + } - // Load all parameters: - if (request != null){ - Enumeration names = request.getParameterNames(); - String paramName; - while(names.hasMoreElements()){ - paramName = names.nextElement(); - set(paramName, request.getParameter(paramName)); - } + /** + *

    Get the parameters stored in the given HTTP request.

    + * + *

    + * Since the version 4.1, parameters are extracted immediately when the request is received. They are then stored in an attribute + * under the name of {@link UWS#REQ_ATTRIBUTE_PARAMETERS}. Thus, the map of parameters can be got in that way. However, if this attribute + * does not exist, this function will ask for the parameters extracted by {@link HttpServletRequest} ({@link HttpServletRequest#getParameterNames()} + * and {@link HttpServletRequest#getParameter(String)}). In this last case only the last non-null occurrence of any parameter will be kept. + *

    + * + * @param request HTTP request from which the parameters must be got. + * + * @return The extracted parameters. + * + * @since 4.1 + */ + @SuppressWarnings("unchecked") + protected static Map getParameters(final HttpServletRequest request){ + // No request => no parameters: + if (request == null) + return null; + + /* The UWS service has theoretically already extracted all parameters in function of the content-type. + * If so, these parameters can be found as a Map in the request attribute "UWS_PARAMETERS": */ + try{ + if (request.getAttribute(UWS.REQ_ATTRIBUTE_PARAMETERS) != null) + return (Map)request.getAttribute(UWS.REQ_ATTRIBUTE_PARAMETERS); + }catch(Exception e){} // 2 possible exceptions: ClassCastException and NullPointerException + + /* If there is no such attribute or if it is not of the good type, + * extract only application/x-www-form-urlencoded parameters: */ + Map map = new HashMap(request.getParameterMap().size()); + Enumeration names = request.getParameterNames(); + int i; + String n; + String[] values; + while(names.hasMoreElements()){ + n = names.nextElement(); + values = request.getParameterValues(n); + // search for the last non-null occurrence: + i = values.length - 1; + while(i >= 0 && values[i] == null) + i--; + // if there is one, keep it: + if (i >= 0) + map.put(n, values[i]); } + return map; } /** *

    Extracts and identifies all UWS standard and non-standard parameters from the map.

    * - *

    note: The default input parameter controllers are set by default (see {@link #getDefaultUWSParamControllers()}).

    + *

    note: The default input parameter controllers are set by default (see {@link #getDefaultControllers()}).

    * * @param params A map of parameters. * @@ -228,7 +300,7 @@ public class UWSParameters implements Iterable> { *

    Extracts and identifies all UWS standard and non-standard parameters from the map.

    * *

    note: Even if no controllers is provided, this constructor sets the default - * input parameter controllers (see {@link #getDefaultUWSParamControllers()}).

    + * input parameter controllers (see {@link #getDefaultControllers()}).

    * * @param params A map of parameters. * @param expectedAdditionalParams The names of all expected additional parameters. @@ -245,13 +317,11 @@ public class UWSParameters implements Iterable> { // Load all parameters: if (params != null && !params.isEmpty()){ - synchronized(params){ - Iterator> it = params.entrySet().iterator(); - Entry entry; - while(it.hasNext()){ - entry = it.next(); - set(entry.getKey(), entry.getValue()); - } + Iterator> it = params.entrySet().iterator(); + Entry entry; + while(it.hasNext()){ + entry = it.next(); + set(entry.getKey(), entry.getValue()); } } } @@ -271,30 +341,26 @@ public class UWSParameters implements Iterable> { /** *

    Must return the input parameter controller of the specified parameter.

    * - *

    note 1: This function is supposed to be case sensitive !

    - *

    note 2: By default, this function just asks to the {@link UWS} thanks to the function {@link UWS#getInputParamController(String)}.

    + *

    note: This function is supposed to be case sensitive !

    * * @param inputParamName The name of the parameter whose the controller is asked. * - * @return The corresponding controller or null if there is no controller for the specified parameter - * or if this {@link UWSParameters} instance doesn't know a {@link UWS}. + * @return The corresponding controller or null if there is no controller for the specified parameter. */ protected InputParamController getController(final String inputParamName){ return mapParamControllers.get(inputParamName); } /** - *

    Must return the list of all available input parameter controllers.

    + * Must return the list of all available input parameter controllers. * - *

    note: By default, this function just asks to the {@link UWS} thanks to the function {@link UWS#getInputParamControllers()}.

    - * - * @return The list of all available controllers or null if there is no controller - * or if this {@link UWSParameters} instance doesn't know a {@link UWS}. + * @return An iterator over all available controllers. */ protected Iterator> getControllers(){ return mapParamControllers.entrySet().iterator(); } + @Override public final Iterator> iterator(){ return params.entrySet().iterator(); } @@ -358,19 +424,36 @@ public class UWSParameters implements Iterable> { if (newParams != null && !newParams.params.isEmpty()){ synchronized(params){ additionalParams = null; + files = null; String[] updated = new String[newParams.params.size()]; + Object oldValue; int i = 0; for(Entry entry : newParams){ // Test whether this parameter is allowed to be modified after its initialization: InputParamController controller = getController(entry.getKey()); if (controller != null && !controller.allowModification()) - throw new UWSException("The parameter \"" + entry.getKey() + "\" can not be modified after initialization !"); - // If the value is NULL, removes this parameter: - if (entry.getValue() == null) - params.remove(entry.getKey()); - // Else set it: - else - params.put(entry.getKey(), entry.getValue()); + throw new UWSException(UWSException.FORBIDDEN, "The parameter \"" + entry.getKey() + "\" can not be modified after initialization!"); + // Determine whether the value already exists: + if (params.containsKey(entry.getKey()) || entry.getKey().toLowerCase().matches(UWS_RW_PARAMETERS_REGEXP)){ + // If the value is NULL, throw an error (no parameter can be removed after job creation): + if (entry.getValue() == null) + throw new UWSException(UWSException.FORBIDDEN, "Removing a parameter (here: \"" + entry.getKey() + "\") from a job is forbidden!"); + // Else update the parameter value: + else{ + // If the parameter to replace is an uploaded file, it must be physically removed before replacement: + oldValue = params.get(entry.getKey()); + if (oldValue != null && oldValue instanceof UploadFile){ + try{ + ((UploadFile)oldValue).deleteFile(); + }catch(IOException ioe){} + } + // Perform the replacement: + params.put(entry.getKey(), entry.getValue()); + } + }else + // No parameter can be added after job creation: + throw new UWSException(UWSException.FORBIDDEN, "Adding a parameter (here: \"" + entry.getKey() + "\") to an existing job is forbidden by the UWS protocol!"); + // Update the list of updated parameters: updated[i++] = entry.getKey(); } return updated; @@ -384,10 +467,12 @@ public class UWSParameters implements Iterable> { * *

    note 1: The case of the parameter name MUST BE correct EXCEPT FOR the standard UWS parameters (i.e. runId, executionDuration, destructionTime).

    *

    note 2: If the name of the parameter is {@link UWSJob#PARAM_PARAMETERS PARAMETERS}, this function will return exactly what {@link #getAdditionalParameters()} returns.

    + *

    note 3: Depending of the way the parameters are fetched from an HTTP request, the returned object may be an array. Each item of this array would then be an occurrence of the parameter in the request (MAYBE in the same order as submitted).

    * * @param name Name of the parameter to get. * - * @return Value of the specified parameter, or null if the given name is null, empty or has no value. + * @return Value of the specified parameter, or null if the given name is null, or an array or {@link Object}s if several values + * have been submitted for the same parameter, empty or has no value. * * @see #normalizeParamName(String) * @see #getAdditionalParameters() @@ -402,6 +487,34 @@ public class UWSParameters implements Iterable> { return params.get(normalizedName); } + /** + * Get the list of all uploaded files. + * + * @return An iterator over the list of uploaded files. + * + * @since 4.1 + */ + public final Iterator getFiles(){ + if (files == null){ + files = new ArrayList(3); + synchronized(params){ + for(Object v : params.values()){ + if (v == null) + continue; + else if (v instanceof UploadFile) + files.add((UploadFile)v); + else if (v.getClass().isArray()){ + for(Object o : (Object[])v){ + if (o instanceof UploadFile) + files.add((UploadFile)o); + } + } + } + } + } + return files.iterator(); + } + /** *

    Sets the given value to the specified parameter. * But if the given value is null, the specified parameter is merely removed.

    @@ -413,7 +526,7 @@ public class UWSParameters implements Iterable> { *

    note 5: If the parameter {@link UWSJob#PARAM_PARAMETERS PARAMETERS} is given, it must be a Map. In this case, the map is read and all its entries are added individually.

    * * @param name Name of the parameter to set (add, update or remove). note: not case sensitive ONLY FOR the standard UWS parameters ! - * @param value The value to set. note: NULL means that the specified parameter must be removed ! + * @param value The value to set. note: NULL means that the specified parameter must be removed ; several values may have been provided using an array of Objects. * * @return The old value of the specified parameter. null may mean that the parameter has just been added, but it may also mean that nothing has been done (because, the given name is null, empty or corresponds to a read-only parameter). * @@ -423,6 +536,10 @@ public class UWSParameters implements Iterable> { */ @SuppressWarnings("unchecked") public final Object set(final String name, Object value) throws UWSException{ + // If the given value is NULL, the parameter must be removed: + if (value == null) + return remove(name); + // Normalize (take into account the case ONLY FOR the non-standard UWS parameters) the given parameter name: String normalizedName = normalizeParamName(name); @@ -432,38 +549,42 @@ public class UWSParameters implements Iterable> { synchronized(params){ additionalParams = null; - - // If the given value is NULL, the parameter must be removed: - if (value == null) - return params.remove(normalizedName); - else{ - // Case of the PARAMETERS parameter: read all parameters and set them individually into this UWSParameters instance: - if (normalizedName.equals(UWSJob.PARAM_PARAMETERS)){ - // the value MUST BE a Map: - if (value instanceof Map){ - try{ - Map otherParams = (Map)value; - HashMap mapOldValues = new HashMap(otherParams.size()); - Object oldValue = null; - for(Entry entry : otherParams.entrySet()){ - oldValue = set(entry.getKey(), entry.getValue()); - mapOldValues.put(entry.getKey(), oldValue); - } - return mapOldValues; - }catch(ClassCastException cce){ - return null; + files = null; + + // Case of the PARAMETERS parameter: read all parameters and set them individually into this UWSParameters instance: + if (normalizedName.equals(UWSJob.PARAM_PARAMETERS)){ + // the value MUST BE a Map: + if (value instanceof Map){ + try{ + Map otherParams = (Map)value; + HashMap mapOldValues = new HashMap(otherParams.size()); + Object oldValue = null; + for(Entry entry : otherParams.entrySet()){ + oldValue = set(entry.getKey(), entry.getValue()); + mapOldValues.put(entry.getKey(), oldValue); } - }else + return mapOldValues; + }catch(ClassCastException cce){ return null; - }else{ - // Check the value before setting it: - InputParamController controller = getController(normalizedName); - if (controller != null) - value = controller.check(value); - - // Set the new value: - return params.put(normalizedName, value); + } + }else + return null; + }else{ + // Check the value before setting it: + InputParamController controller = getController(normalizedName); + if (controller != null) + value = controller.check(value); + + // If the parameter already exists and it is an uploaded file, delete it before its replacement: + Object oldValue = params.get(normalizedName); + if (oldValue != null && oldValue instanceof UploadFile){ + try{ + ((UploadFile)oldValue).deleteFile(); + }catch(IOException ioe){} } + + // Set the new value: + return params.put(normalizedName, value); } } } @@ -487,7 +608,17 @@ public class UWSParameters implements Iterable> { synchronized(params){ additionalParams = null; - return params.remove(normalizedName); + files = null; + // Remove the file: + Object removed = params.remove(normalizedName); + // If the removed parameter was a file, remove it from the server: + if (removed != null && removed instanceof UploadFile){ + try{ + ((UploadFile)removed).deleteFile(); + }catch(IOException ioe){} + } + // Return the value of the removed parameter: + return removed; } } @@ -589,7 +720,7 @@ public class UWSParameters implements Iterable> { return (Date)value; else if (value instanceof String){ try{ - Date destruction = UWSJob.dateFormat.parse((String)value); + Date destruction = ISO8601Format.parseToDate((String)value); synchronized(params){ params.put(UWSJob.PARAM_DESTRUCTION_TIME, destruction); } diff --git a/src/uws/job/serializer/JSONSerializer.java b/src/uws/job/serializer/JSONSerializer.java index 9ffb832..48dc5db 100644 --- a/src/uws/job/serializer/JSONSerializer.java +++ b/src/uws/job/serializer/JSONSerializer.java @@ -16,29 +16,27 @@ package uws.job.serializer; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import org.json.JSONException; import org.json.Json4Uws; -import uws.UWSException; - +import uws.ISO8601Format; import uws.job.ErrorSummary; import uws.job.JobList; import uws.job.Result; import uws.job.UWSJob; - import uws.job.user.JobOwner; - import uws.service.UWS; import uws.service.UWSUrl; /** * Lets serializing any UWS resource in JSON. * - * @author Grégory Mantelet (CDS) - * @version 05/2012 + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (09/2014) * * @see Json4Uws */ @@ -51,178 +49,105 @@ public class JSONSerializer extends UWSSerializer { } @Override - public String getUWS(final UWS uws, final JobOwner user) throws UWSException{ - try{ - return Json4Uws.getJson(uws).toString(); - }catch(JSONException je){ - throw new UWSException(je); - } + public String getUWS(final UWS uws, final JobOwner user) throws JSONException{ + return Json4Uws.getJson(uws).toString(); } @Override - public String getJobList(final JobList jobsList, final JobOwner owner, final boolean root) throws UWSException{ - try{ - return Json4Uws.getJson(jobsList, owner).toString(); - }catch(JSONException je){ - throw new UWSException(je); - } + public String getJobList(final JobList jobsList, final JobOwner owner, final boolean root) throws JSONException{ + return Json4Uws.getJson(jobsList, owner).toString(); } @Override - public String getJob(final UWSJob job, final boolean root) throws UWSException{ - try{ - return Json4Uws.getJson(job, null, false).toString(); - }catch(JSONException je){ - throw new UWSException(je); - } + public String getJob(final UWSJob job, final boolean root) throws JSONException{ + return Json4Uws.getJson(job, null, false).toString(); } @Override - public String getJobRef(final UWSJob job, final UWSUrl jobsListUrl) throws UWSException{ - try{ - return Json4Uws.getJson(job, jobsListUrl, true).toString(); - }catch(JSONException je){ - throw new UWSException(je); - } + public String getJobRef(final UWSJob job, final UWSUrl jobsListUrl) throws JSONException{ + return Json4Uws.getJson(job, jobsListUrl, true).toString(); } @Override - public String getJobID(final UWSJob job, final boolean root) throws UWSException{ - try{ - return Json4Uws.getJson(UWSJob.PARAM_JOB_ID, job.getJobId()).toString(); - }catch(JSONException je){ - throw new UWSException(je); - } + public String getJobID(final UWSJob job, final boolean root) throws JSONException{ + return Json4Uws.getJson(UWSJob.PARAM_JOB_ID, job.getJobId()).toString(); } @Override - public String getRunID(final UWSJob job, final boolean root) throws UWSException{ - try{ - return Json4Uws.getJson(UWSJob.PARAM_RUN_ID, job.getRunId()).toString(); - }catch(JSONException je){ - throw new UWSException(je); - } + public String getRunID(final UWSJob job, final boolean root) throws JSONException{ + return Json4Uws.getJson(UWSJob.PARAM_RUN_ID, job.getRunId()).toString(); } @Override - public String getOwnerID(final UWSJob job, final boolean root) throws UWSException{ + public String getOwnerID(final UWSJob job, final boolean root) throws JSONException{ if (job.getOwner() == null) return "{}"; - else{ - try{ - return Json4Uws.getJson(UWSJob.PARAM_OWNER, job.getOwner().getPseudo()).toString(); - }catch(JSONException je){ - throw new UWSException(je); - } - } + else + return Json4Uws.getJson(UWSJob.PARAM_OWNER, job.getOwner().getPseudo()).toString(); } @Override - public String getPhase(final UWSJob job, final boolean root) throws UWSException{ - try{ - return Json4Uws.getJson(UWSJob.PARAM_PHASE, job.getPhase().toString()).toString(); - }catch(JSONException je){ - throw new UWSException(je); - } + public String getPhase(final UWSJob job, final boolean root) throws JSONException{ + return Json4Uws.getJson(UWSJob.PARAM_PHASE, job.getPhase().toString()).toString(); } @Override - public String getQuote(final UWSJob job, final boolean root) throws UWSException{ - try{ - return Json4Uws.getJson(UWSJob.PARAM_QUOTE, job.getQuote()).toString(); - }catch(JSONException je){ - throw new UWSException(je); - } + public String getQuote(final UWSJob job, final boolean root) throws JSONException{ + return Json4Uws.getJson(UWSJob.PARAM_QUOTE, job.getQuote()).toString(); } @Override - public String getExecutionDuration(final UWSJob job, final boolean root) throws UWSException{ - try{ - return Json4Uws.getJson(UWSJob.PARAM_EXECUTION_DURATION, job.getExecutionDuration()).toString(); - }catch(JSONException je){ - throw new UWSException(je); - } + public String getExecutionDuration(final UWSJob job, final boolean root) throws JSONException{ + return Json4Uws.getJson(UWSJob.PARAM_EXECUTION_DURATION, job.getExecutionDuration()).toString(); } @Override - public String getDestructionTime(final UWSJob job, final boolean root) throws UWSException{ + public String getDestructionTime(final UWSJob job, final boolean root) throws JSONException{ if (job.getDestructionTime() != null){ - try{ - return Json4Uws.getJson(UWSJob.PARAM_DESTRUCTION_TIME, UWSJob.dateFormat.format(job.getDestructionTime())).toString(); - }catch(JSONException je){ - throw new UWSException(je); - } + return Json4Uws.getJson(UWSJob.PARAM_DESTRUCTION_TIME, ISO8601Format.format(job.getDestructionTime())).toString(); }else return "{}"; } @Override - public String getStartTime(final UWSJob job, final boolean root) throws UWSException{ - if (job.getDestructionTime() != null){ - try{ - return Json4Uws.getJson(UWSJob.PARAM_START_TIME, UWSJob.dateFormat.format(job.getDestructionTime())).toString(); - }catch(JSONException je){ - throw new UWSException(je); - } - }else + public String getStartTime(final UWSJob job, final boolean root) throws JSONException{ + if (job.getDestructionTime() != null) + return Json4Uws.getJson(UWSJob.PARAM_START_TIME, ISO8601Format.format(job.getDestructionTime())).toString(); + else return "{}"; } @Override - public String getEndTime(final UWSJob job, final boolean root) throws UWSException{ - if (job.getDestructionTime() != null){ - try{ - return Json4Uws.getJson(UWSJob.PARAM_END_TIME, UWSJob.dateFormat.format(job.getDestructionTime())).toString(); - }catch(JSONException je){ - throw new UWSException(je); - } - }else + public String getEndTime(final UWSJob job, final boolean root) throws JSONException{ + if (job.getDestructionTime() != null) + return Json4Uws.getJson(UWSJob.PARAM_END_TIME, ISO8601Format.format(job.getDestructionTime())).toString(); + else return "{}"; } @Override - public String getErrorSummary(final ErrorSummary error, final boolean root) throws UWSException{ - try{ - return Json4Uws.getJson(error).toString(); - }catch(JSONException je){ - throw new UWSException(je); - } + public String getErrorSummary(final ErrorSummary error, final boolean root) throws JSONException{ + return Json4Uws.getJson(error).toString(); } @Override - public String getAdditionalParameters(final UWSJob job, final boolean root) throws UWSException{ - try{ - return Json4Uws.getJobParamsJson(job).toString(); - }catch(JSONException je){ - throw new UWSException(je); - } + public String getAdditionalParameters(final UWSJob job, final boolean root) throws JSONException{ + return Json4Uws.getJobParamsJson(job).toString(); } @Override - public String getAdditionalParameter(final String paramName, final Object paramValue, final boolean root) throws UWSException{ - try{ - return Json4Uws.getJson(paramName, (paramValue == null) ? null : paramValue.toString()).toString(); - }catch(JSONException je){ - throw new UWSException(je); - } + public String getAdditionalParameter(final String paramName, final Object paramValue, final boolean root) throws JSONException{ + return Json4Uws.getJson(paramName, (paramValue == null) ? null : paramValue.toString()).toString(); } @Override - public String getResults(final UWSJob job, final boolean root) throws UWSException{ - try{ - return Json4Uws.getJobResultsJson(job).toString(); - }catch(JSONException je){ - throw new UWSException(je); - } + public String getResults(final UWSJob job, final boolean root) throws JSONException{ + return Json4Uws.getJobResultsJson(job).toString(); } @Override - public String getResult(final Result result, final boolean root) throws UWSException{ - try{ - return Json4Uws.getJobResultJson(result).toString(); - }catch(JSONException je){ - throw new UWSException(je); - } + public String getResult(final Result result, final boolean root) throws JSONException{ + return Json4Uws.getJobResultJson(result).toString(); } } diff --git a/src/uws/job/serializer/UWSSerializer.java b/src/uws/job/serializer/UWSSerializer.java index 3303987..6a191d7 100644 --- a/src/uws/job/serializer/UWSSerializer.java +++ b/src/uws/job/serializer/UWSSerializer.java @@ -16,21 +16,19 @@ package uws.job.serializer; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.io.Serializable; +import uws.ISO8601Format; import uws.UWSException; -import uws.UWSExceptionFactory; - import uws.job.ErrorSummary; import uws.job.JobList; import uws.job.Result; import uws.job.UWSJob; import uws.job.user.JobOwner; - -import uws.service.UWSService; import uws.service.UWS; import uws.service.UWSUrl; @@ -42,8 +40,8 @@ import uws.service.UWSUrl; *
  • JSON by the class {@link JSONSerializer}
  • * * - * @author Grégory Mantelet (CDS) - * @version 05/2012 + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (12/2014) * * @see XMLSerializer * @see JSONSerializer @@ -52,7 +50,7 @@ public abstract class UWSSerializer implements Serializable { private static final long serialVersionUID = 1L; /** MIME type for XML: application/xml */ - public static final String MIME_TYPE_XML = "application/xml"; + public static final String MIME_TYPE_XML = "text/xml"; /** MIME type for JSON: application/json */ public static final String MIME_TYPE_JSON = "application/json"; /** MIME type for TEXT: text/plain */ @@ -72,9 +70,9 @@ public abstract class UWSSerializer implements Serializable { * @return The serialization of the given attribute * or the serialization of the whole job if the given attributes array is empty or null. * - * @throws UWSException If the specified attribute/parameter/result does not exist. + * @throws Exception If an error occurs while serializing the specified job/attribute/parameter/result. */ - public String getJob(final UWSJob job, final String[] attributes, final boolean root) throws UWSException{ + public String getJob(final UWSJob job, final String[] attributes, final boolean root) throws Exception{ if (attributes == null || attributes.length <= 0) return getJob(job, root); @@ -97,16 +95,16 @@ public abstract class UWSSerializer implements Serializable { return job.getQuote() + ""; // START TIME: else if (firstAttribute.equalsIgnoreCase(UWSJob.PARAM_START_TIME)) - return (job.getStartTime() == null) ? "" : UWSJob.dateFormat.format(job.getStartTime()); + return (job.getStartTime() == null) ? "" : ISO8601Format.format(job.getStartTime()); // END TIME: else if (firstAttribute.equalsIgnoreCase(UWSJob.PARAM_END_TIME)) - return (job.getEndTime() == null) ? "" : UWSJob.dateFormat.format(job.getEndTime()); + return (job.getEndTime() == null) ? "" : ISO8601Format.format(job.getEndTime()); // EXECUTION DURATION: else if (firstAttribute.equalsIgnoreCase(UWSJob.PARAM_EXECUTION_DURATION)) return job.getExecutionDuration() + ""; // DESTRUCTION TIME: else if (firstAttribute.equalsIgnoreCase(UWSJob.PARAM_DESTRUCTION_TIME)) - return (job.getDestructionTime() == null) ? "" : UWSJob.dateFormat.format(job.getDestructionTime()); + return (job.getDestructionTime() == null) ? "" : ISO8601Format.format(job.getDestructionTime()); // PARAMETERS LIST: else if (firstAttribute.equalsIgnoreCase(UWSJob.PARAM_PARAMETERS)){ if (attributes.length <= 1) @@ -115,10 +113,23 @@ public abstract class UWSSerializer implements Serializable { // PARAMETER: String secondAttribute = attributes[1]; Object value = job.getAdditionalParameterValue(secondAttribute); - if (value != null) - return value.toString(); - else - throw UWSExceptionFactory.incorrectJobParameter(job.getJobId(), secondAttribute); + if (value != null){ + // CASE: array value + if (value.getClass().isArray()){ + Object[] items = (Object[])value; + StringBuffer arrayAsString = new StringBuffer(); + for(Object item : items){ + if (arrayAsString.length() > 0) + arrayAsString.append(' ').append(';').append(' '); + arrayAsString.append(item.toString()); + } + return arrayAsString.toString(); + } + // DEFAULT: + else + return value.toString(); + }else + throw new UWSException(UWSException.NOT_FOUND, "No parameter named \"" + secondAttribute + "\" in the job \"" + job.getJobId() + "\"!"); } // RESULTS LIST: }else if (firstAttribute.equalsIgnoreCase(UWSJob.PARAM_RESULTS)){ @@ -131,7 +142,7 @@ public abstract class UWSSerializer implements Serializable { if (r != null) return getResult(r, root); else - throw UWSExceptionFactory.incorrectJobResult(job.getJobId(), secondAttribute); + throw new UWSException(UWSException.NOT_FOUND, "No result named \"" + secondAttribute + "\" in the job \"" + job.getJobId() + "\"!"); } // ERROR DETAILS or ERROR SUMMARY: }else if (firstAttribute.equalsIgnoreCase(UWSJob.PARAM_ERROR_SUMMARY)) @@ -141,7 +152,7 @@ public abstract class UWSSerializer implements Serializable { return getErrorSummary(job.getErrorSummary(), root); // OTHERS: else - throw UWSExceptionFactory.incorrectJobParameter(job.getJobId(), firstAttribute); + throw new UWSException(UWSException.NOT_FOUND, "No job attribute named \"" + firstAttribute + "\" in the job \"" + job.getJobId() + "\"!"); } @Override @@ -160,12 +171,14 @@ public abstract class UWSSerializer implements Serializable { * Serializes the given UWS. * * @param uws The UWS to serialize. + * * @return The serialization of the given UWS. - * @throws UWSException If there is an error during the serialization. * - * @see UWSSerializer#getUWS(UWSService, String) + * @throws Exception If there is an error during the serialization. + * + * @see UWSSerializer#getUWS(UWS, JobOwner) */ - public String getUWS(final UWS uws) throws UWSException{ + public String getUWS(final UWS uws) throws Exception{ return getUWS(uws, null); } @@ -176,9 +189,10 @@ public abstract class UWSSerializer implements Serializable { * @param user The user which has asked the serialization of the given UWS. * * @return The serialization of the UWS. - * @throws UWSException If there is an error during the serialization. + * + * @throws Exception If there is an error during the serialization. */ - public abstract String getUWS(final UWS uws, final JobOwner user) throws UWSException; + public abstract String getUWS(final UWS uws, final JobOwner user) throws Exception; /** * Serializes the given jobs list. @@ -188,9 +202,10 @@ public abstract class UWSSerializer implements Serializable { * in a top level serialization (for a jobs list: uws), true otherwise. * * @return The serialization of the given jobs list. - * @throws UWSException If there is an error during the serialization. + * + * @throws Exception If there is an error during the serialization. */ - public String getJobList(final JobList jobsList, final boolean root) throws UWSException{ + public String getJobList(final JobList jobsList, final boolean root) throws Exception{ return getJobList(jobsList, null, root); } @@ -203,10 +218,9 @@ public abstract class UWSSerializer implements Serializable { * in a top level serialization (for a jobs list: uws), true otherwise. * @return The serialization of the given jobs list. * - * - * @throws UWSException If there is an error during the serialization. + * @throws Exception If there is an error during the serialization. */ - public abstract String getJobList(final JobList jobsList, JobOwner owner, final boolean root) throws UWSException; + public abstract String getJobList(final JobList jobsList, JobOwner owner, final boolean root) throws Exception; /** * Serializes the whole given job. @@ -217,9 +231,9 @@ public abstract class UWSSerializer implements Serializable { * * @return The serialization of the given job. * - * @throws UWSException If there is an error during the serialization. + * @throws Exception If there is an error during the serialization. */ - public abstract String getJob(final UWSJob job, final boolean root) throws UWSException; + public abstract String getJob(final UWSJob job, final boolean root) throws Exception; /** * Serializes just a reference on the given job. @@ -229,11 +243,11 @@ public abstract class UWSSerializer implements Serializable { * * @return The serialization of a reference on the given job. * - * @throws UWSException If there is an error during the serialization. + * @throws Exception If there is an error during the serialization. * * @since 3.1 */ - public abstract String getJobRef(final UWSJob job, final UWSUrl jobsListUrl) throws UWSException; + public abstract String getJobRef(final UWSJob job, final UWSUrl jobsListUrl) throws Exception; /** * Serializes the ID of the given job. @@ -244,9 +258,9 @@ public abstract class UWSSerializer implements Serializable { * * @return The serialization of the job ID. * - * @throws UWSException If there is an error during the serialization. + * @throws Exception If there is an error during the serialization. */ - public abstract String getJobID(final UWSJob job, final boolean root) throws UWSException; + public abstract String getJobID(final UWSJob job, final boolean root) throws Exception; /** * Serializes the run ID of the given job. @@ -257,9 +271,9 @@ public abstract class UWSSerializer implements Serializable { * * @return The serialization of the run ID. * - * @throws UWSException If there is an error during the serialization. + * @throws Exception If there is an error during the serialization. */ - public abstract String getRunID(final UWSJob job, final boolean root) throws UWSException; + public abstract String getRunID(final UWSJob job, final boolean root) throws Exception; /** * Serializes the owner ID of the given job. @@ -270,9 +284,9 @@ public abstract class UWSSerializer implements Serializable { * * @return The serialization of the owner ID. * - * @throws UWSException If there is an error during the serialization. + * @throws Exception If there is an error during the serialization. */ - public abstract String getOwnerID(final UWSJob job, final boolean root) throws UWSException; + public abstract String getOwnerID(final UWSJob job, final boolean root) throws Exception; /** * Serializes the phase of the given job. @@ -283,9 +297,9 @@ public abstract class UWSSerializer implements Serializable { * * @return The serialization of the phase. * - * @throws UWSException If there is an error during the serialization. + * @throws Exception If there is an error during the serialization. */ - public abstract String getPhase(final UWSJob job, final boolean root) throws UWSException; + public abstract String getPhase(final UWSJob job, final boolean root) throws Exception; /** * Serializes the quote of the given job. @@ -296,9 +310,9 @@ public abstract class UWSSerializer implements Serializable { * * @return The serialization of the quote. * - * @throws UWSException If there is an error during the serialization. + * @throws Exception If there is an error during the serialization. */ - public abstract String getQuote(final UWSJob job, final boolean root) throws UWSException; + public abstract String getQuote(final UWSJob job, final boolean root) throws Exception; /** * Serializes the start time of the given job. @@ -309,9 +323,9 @@ public abstract class UWSSerializer implements Serializable { * * @return The serialization of the start time. * - * @throws UWSException If there is an error during the serialization. + * @throws Exception If there is an error during the serialization. */ - public abstract String getStartTime(final UWSJob job, final boolean root) throws UWSException; + public abstract String getStartTime(final UWSJob job, final boolean root) throws Exception; /** * Serializes the end time of the given job. @@ -322,9 +336,9 @@ public abstract class UWSSerializer implements Serializable { * * @return The serialization of the end time. * - * @throws UWSException If there is an error during the serialization. + * @throws Exception If there is an error during the serialization. */ - public abstract String getEndTime(final UWSJob job, final boolean root) throws UWSException; + public abstract String getEndTime(final UWSJob job, final boolean root) throws Exception; /** * Serializes the execution duration of the given job. @@ -335,9 +349,9 @@ public abstract class UWSSerializer implements Serializable { * * @return The serialization of the execution duration. * - * @throws UWSException If there is an error during the serialization. + * @throws Exception If there is an error during the serialization. */ - public abstract String getExecutionDuration(final UWSJob job, final boolean root) throws UWSException; + public abstract String getExecutionDuration(final UWSJob job, final boolean root) throws Exception; /** * Serializes the destruction time of the given job. @@ -348,9 +362,9 @@ public abstract class UWSSerializer implements Serializable { * * @return The serialization of the destruction time. * - * @throws UWSException If there is an error during the serialization. + * @throws Exception If there is an error during the serialization. */ - public abstract String getDestructionTime(final UWSJob job, final boolean root) throws UWSException; + public abstract String getDestructionTime(final UWSJob job, final boolean root) throws Exception; /** * Serializes the given error summary. @@ -361,9 +375,9 @@ public abstract class UWSSerializer implements Serializable { * * @return The serialization of the error summary. * - * @throws UWSException If there is an error during the serialization. + * @throws Exception If there is an error during the serialization. */ - public abstract String getErrorSummary(final ErrorSummary error, final boolean root) throws UWSException; + public abstract String getErrorSummary(final ErrorSummary error, final boolean root) throws Exception; /** * Serializes the results of the given job. @@ -374,9 +388,9 @@ public abstract class UWSSerializer implements Serializable { * * @return The serialization of the results. * - * @throws UWSException If there is an error during the serialization. + * @throws Exception If there is an error during the serialization. */ - public abstract String getResults(final UWSJob job, final boolean root) throws UWSException; + public abstract String getResults(final UWSJob job, final boolean root) throws Exception; /** * Serializes the given result. @@ -387,9 +401,9 @@ public abstract class UWSSerializer implements Serializable { * * @return The serialization of the result. * - * @throws UWSException If there is an error during the serialization. + * @throws Exception If there is an error during the serialization. */ - public abstract String getResult(final Result result, final boolean root) throws UWSException; + public abstract String getResult(final Result result, final boolean root) throws Exception; /** * Serializes the parameters of the given job. @@ -400,9 +414,9 @@ public abstract class UWSSerializer implements Serializable { * * @return The serialization of the parameters. * - * @throws UWSException If there is an error during the serialization. + * @throws Exception If there is an error during the serialization. */ - public abstract String getAdditionalParameters(final UWSJob job, final boolean root) throws UWSException; + public abstract String getAdditionalParameters(final UWSJob job, final boolean root) throws Exception; /** * Serializes the specified parameter. @@ -414,7 +428,7 @@ public abstract class UWSSerializer implements Serializable { * * @return The serialization of the parameter. * - * @throws UWSException If there is an error during the serialization. + * @throws Exception If there is an error during the serialization. */ - public abstract String getAdditionalParameter(final String paramName, final Object paramValue, final boolean root) throws UWSException; + public abstract String getAdditionalParameter(final String paramName, final Object paramValue, final boolean root) throws Exception; } diff --git a/src/uws/job/serializer/XMLSerializer.java b/src/uws/job/serializer/XMLSerializer.java index 2107b54..15fe433 100644 --- a/src/uws/job/serializer/XMLSerializer.java +++ b/src/uws/job/serializer/XMLSerializer.java @@ -16,29 +16,29 @@ package uws.job.serializer; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.Iterator; -import uws.UWSException; - +import uws.ISO8601Format; import uws.job.ErrorSummary; import uws.job.JobList; import uws.job.Result; import uws.job.UWSJob; import uws.job.user.JobOwner; - import uws.service.UWS; import uws.service.UWSUrl; +import uws.service.request.UploadFile; /** * Lets serializing any UWS resource in XML. * - * @author Grégory Mantelet (CDS) - * @version 05/2012 + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (02/2015) */ public class XMLSerializer extends UWSSerializer { private static final long serialVersionUID = 1L; @@ -106,10 +106,10 @@ public class XMLSerializer extends UWSSerializer { /** * Gets all UWS namespaces declarations needed for an XML representation of a UWS object. * - * @return The UWS namespaces:
    (i.e. = "xmlns:uws=[...] xmlns:xlink=[...] xmlns:xs=[...] xmlns:xsi=[...]"). + * @return The UWS namespaces:
    (i.e. = "xmlns:uws=[...] xmlns:xlink=[...] xmlns:xs=[...] xmlns:xsi=[...] xsi:schemaLocation=[...]"). */ public String getUWSNamespace(){ - return "xmlns:uws=\"http://www.ivoa.net/xml/UWS/v1.0\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""; + return "xmlns=\"http://www.ivoa.net/xml/UWS/v1.0\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://www.ivoa.net/xml/UWS/v1.0 http://www.ivoa.net/xml/UWS/v1.0 http://www.w3.org/1999/xlink http://www.w3.org/1999/xlink.xsd http://www.w3.org/2001/XMLSchema http://www.w3.org/2001/XMLSchema.xsd\""; } /** @@ -151,8 +151,8 @@ public class XMLSerializer extends UWSSerializer { for(JobList jobList : uws){ UWSUrl jlUrl = jobList.getUrl(); xml.append("\t\t\n"); } xml.append("\t\n"); @@ -163,13 +163,15 @@ public class XMLSerializer extends UWSSerializer { } @Override - public String getJobList(final JobList jobsList, final JobOwner owner, final boolean root) throws UWSException{ - String name = jobsList.getName(); + public String getJobList(final JobList jobsList, final JobOwner owner, final boolean root){ StringBuffer xml = new StringBuffer(getHeader()); - xml.append(""); UWSUrl jobsListUrl = jobsList.getUrl(); @@ -177,7 +179,7 @@ public class XMLSerializer extends UWSSerializer { while(it.hasNext()) xml.append("\n\t").append(getJobRef(it.next(), jobsListUrl)); - xml.append("\n"); + xml.append("\n"); return xml.toString(); } @@ -188,9 +190,10 @@ public class XMLSerializer extends UWSSerializer { String newLine = "\n\t"; // general information: - xml.append(""); + xml.append(""); xml.append(newLine).append(getJobID(job, false)); - xml.append(newLine).append(getRunID(job, false)); + if (job.getRunId() != null) + xml.append(newLine).append(getRunID(job, false)); xml.append(newLine).append(getOwnerID(job, false)); xml.append(newLine).append(getPhase(job, false)); xml.append(newLine).append(getQuote(job, false)); @@ -212,7 +215,7 @@ public class XMLSerializer extends UWSSerializer { xml.append(newLine).append(getErrorSummary(job.getErrorSummary(), false)); tabPrefix = ""; - return xml.append("\n").toString(); + return xml.append("\n").toString(); } @Override @@ -223,160 +226,199 @@ public class XMLSerializer extends UWSSerializer { url = jobsListUrl.getRequestURL(); } - StringBuffer xml = new StringBuffer(" 0) - xml.append("\" runId=\"").append(escapeXMLAttribute(job.getRunId())); + /* NOTE: NO ATTRIBUTE "runId" IN THE XML SCHEMA! + * if (job.getRunId() != null && job.getRunId().length() > 0) + * xml.append("\" runId=\"").append(escapeXMLAttribute(job.getRunId())); + */ xml.append("\" xlink:href=\""); if (url != null) - xml.append(escapeURL(url)); - xml.append("\">").append(getPhase(job, false)).append(""); + xml.append(escapeXMLAttribute(url)); + xml.append("\">\n\t\t").append(getPhase(job, false)).append("\n\t"); return xml.toString(); } @Override public String getJobID(final UWSJob job, final boolean root){ - return (new StringBuffer(root ? getHeader() : "")).append("").append(escapeXMLData(job.getJobId())).append("").toString(); + return (new StringBuffer(root ? getHeader() : "")).append("").append(escapeXMLData(job.getJobId())).append("").toString(); } @Override public String getRunID(final UWSJob job, final boolean root){ - StringBuffer xml = new StringBuffer(root ? getHeader() : ""); - xml.append(""); - else - xml.append(">").append(escapeXMLData(job.getRunId())).append(""); - return xml.toString(); + if (job.getRunId() != null){ + StringBuffer xml = new StringBuffer(root ? getHeader() : ""); + xml.append("").append(escapeXMLData(job.getRunId())).append(""); + return xml.toString(); + }else + return ""; } @Override public String getOwnerID(final UWSJob job, final boolean root){ StringBuffer xml = new StringBuffer(root ? getHeader() : ""); - xml.append(""); else - xml.append(">").append(escapeXMLData(job.getOwner().getPseudo())).append(""); + xml.append(">").append(escapeXMLData(job.getOwner().getPseudo())).append(""); return xml.toString(); } @Override public String getPhase(final UWSJob job, final boolean root){ - return (new StringBuffer(root ? getHeader() : "")).append("").append(job.getPhase()).append("").toString(); + return (new StringBuffer(root ? getHeader() : "")).append("").append(job.getPhase()).append("").toString(); } @Override public String getQuote(final UWSJob job, final boolean root){ StringBuffer xml = new StringBuffer(root ? getHeader() : ""); - xml.append(""); else - xml.append(">").append(job.getQuote()).append(""); + xml.append(">").append(job.getQuote()).append(""); return xml.toString(); } @Override public String getStartTime(final UWSJob job, final boolean root){ StringBuffer xml = new StringBuffer(root ? getHeader() : ""); - xml.append(""); else - xml.append(">").append(UWSJob.dateFormat.format(job.getStartTime())).append(""); + xml.append(">").append(ISO8601Format.format(job.getStartTime())).append(""); return xml.toString(); } @Override public String getEndTime(final UWSJob job, final boolean root){ StringBuffer xml = new StringBuffer(root ? getHeader() : ""); - xml.append(""); else - xml.append(">").append(UWSJob.dateFormat.format(job.getEndTime())).append(""); + xml.append(">").append(ISO8601Format.format(job.getEndTime())).append(""); return xml.toString(); } @Override public String getDestructionTime(final UWSJob job, final boolean root){ StringBuffer xml = new StringBuffer(root ? getHeader() : ""); - xml.append(""); else - xml.append(">").append(UWSJob.dateFormat.format(job.getDestructionTime())).append(""); + xml.append(">").append(ISO8601Format.format(job.getDestructionTime())).append(""); return xml.toString(); } @Override public String getExecutionDuration(final UWSJob job, final boolean root){ - return (new StringBuffer(root ? getHeader() : "")).append("").append(job.getExecutionDuration()).append("").toString(); + return (new StringBuffer(root ? getHeader() : "")).append("").append(job.getExecutionDuration()).append("").toString(); } @Override public String getErrorSummary(final ErrorSummary error, final boolean root){ - StringBuffer xml = new StringBuffer(root ? getHeader() : ""); - xml.append(tabPrefix).append(""); - xml.append("\n\t").append(tabPrefix).append("").append(escapeXMLData(error.getMessage())).append(""); - xml.append("\n").append(tabPrefix).append(""); + xml.append("\n\t").append(tabPrefix).append("").append(escapeXMLData(error.getMessage())).append(""); + xml.append("\n").append(tabPrefix).append(""); + return xml.toString(); }else - xml.append(" xsi:nil=\"true\" />"); - return xml.toString(); + return ""; } @Override public String getAdditionalParameters(final UWSJob job, final boolean root){ StringBuffer xml = new StringBuffer(root ? getHeader() : ""); - xml.append(tabPrefix).append(""); + xml.append(tabPrefix).append(""); String newLine = "\n\t" + tabPrefix; for(String paramName : job.getAdditionalParameters()) xml.append(newLine).append(getAdditionalParameter(paramName, job.getAdditionalParameterValue(paramName), false)); - xml.append("\n").append(tabPrefix).append(""); + xml.append("\n").append(tabPrefix).append(""); return xml.toString(); } @Override public String getAdditionalParameter(final String paramName, final Object paramValue, final boolean root){ if (paramName != null && paramValue != null){ - if (root) - return paramValue.toString(); - else - return (new StringBuffer("").append(escapeXMLData(paramValue.toString())).append("").toString(); - }else + // If ROOT, just the value must be returned: + if (root){ + if (paramValue.getClass().isArray()){ + StringBuffer buf = new StringBuffer(); + for(Object o : (Object[])paramValue){ + if (buf.length() > 0) + buf.append(';'); + buf.append(o.toString()); + } + return buf.toString(); + }else + return paramValue.toString(); + } + // OTHERWISE, return the XML description: + else{ + StringBuffer buf = new StringBuffer(); + // if array (=> multiple occurrences of the parameter), each item must be one individual parameter: + if (paramValue.getClass().isArray()){ + for(Object o : (Object[])paramValue){ + if (buf.length() > 0) + buf.append("\n\t").append(tabPrefix); + buf.append(getAdditionalParameter(paramName, o, root)); + } + } + // otherwise, just return the XML parameter description: + else{ + buf.append("").append(escapeXMLData(paramValue.toString())).append(""); + } + return buf.toString(); + } + } + // If NO VALUE or NO NAME, return an empty string: + else return ""; } @Override public String getResults(final UWSJob job, final boolean root){ StringBuffer xml = new StringBuffer(root ? getHeader() : ""); - xml.append(tabPrefix).append(""); + xml.append(tabPrefix).append(""); Iterator it = job.getResults(); String newLine = "\n\t" + tabPrefix; while(it.hasNext()) xml.append(newLine).append(getResult(it.next(), false)); - xml.append("\n").append(tabPrefix).append(""); + xml.append("\n").append(tabPrefix).append(""); return xml.toString(); } @Override public String getResult(final Result result, final boolean root){ StringBuffer xml = new StringBuffer(root ? getHeader() : ""); - xml.append("= 0) - xml.append(" size=\"").append(result.getSize()).append("\""); + + /* NOTE: THE FOLLOWING ATTRIBUTES MAY PROVIDE USEFUL INFORMATION TO USERS, BUT THEY ARE NOT ALLOWED BY THE CURRENT UWS STANDARD. + * HOWEVER, IF, ONE DAY, THEY ARE, THE FOLLOWING LINES SHOULD BE UNCOMNENTED. + * + * if (result.getMimeType() != null) + * xml.append(" mime=\"").append(escapeXMLAttribute(result.getMimeType())).append("\""); + * if (result.getSize() >= 0) + * xml.append(" size=\"").append(result.getSize()).append("\""); + */ + return xml.append(" />").toString(); } @@ -384,16 +426,31 @@ public class XMLSerializer extends UWSSerializer { /* ESCAPE METHODS */ /* ************** */ /** - *

    Escapes the content of a node (data between the open and the close tags).

    - * - *

    By default: surrounds the given data by "<![CDATA[" and "]]>".

    + * Escapes the content of a node (data between the open and the close tags). * * @param data Data to escape. * * @return Escaped data. */ public static String escapeXMLData(final String data){ - return ""; + StringBuffer encoded = new StringBuffer(); + for(int i = 0; i < data.length(); i++){ + char c = data.charAt(i); + switch(c){ + case '&': + encoded.append("&"); + break; + case '<': + encoded.append("<"); + break; + case '>': + encoded.append(">"); + break; + default: + encoded.append(ensureLegalXml(c)); + } + } + return encoded.toString(); } /** @@ -420,11 +477,8 @@ public class XMLSerializer extends UWSSerializer { case '"': encoded.append("""); break; - case '\'': - encoded.append("'"); - break; default: - encoded.append(c); + encoded.append(ensureLegalXml(c)); } } return encoded.toString(); @@ -448,4 +502,22 @@ public class XMLSerializer extends UWSSerializer { } } + /** + *

    Returns a legal XML character corresponding to an input character. + * Certain characters are simply illegal in XML (regardless of encoding). + * If the input character is legal in XML, it is returned; + * otherwise some other weird but legal character + * (currently the inverted question mark, "\u00BF") is returned instead.

    + * + *

    Note: copy of the STILTS VOSerializer.ensureLegalXml(char) function.

    + * + * @param c input character + * @return legal XML character, c if possible + * + * @since 4.1 + */ + public static char ensureLegalXml(char c){ + return ((c >= '\u0020' && c <= '\uD7FF') || (c >= '\uE000' && c <= '\uFFFD') || ((c) == 0x09 || (c) == 0x0A || (c) == 0x0D)) ? c : '\u00BF'; + } + } diff --git a/src/uws/service/AbstractUWSFactory.java b/src/uws/service/AbstractUWSFactory.java index 04fbe8a..86d1112 100644 --- a/src/uws/service/AbstractUWSFactory.java +++ b/src/uws/service/AbstractUWSFactory.java @@ -16,7 +16,8 @@ package uws.service; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.util.ArrayList; @@ -28,26 +29,26 @@ import java.util.Map; import javax.servlet.http.HttpServletRequest; import uws.UWSException; - import uws.job.ErrorSummary; import uws.job.JobThread; import uws.job.Result; import uws.job.UWSJob; - import uws.job.parameters.DestructionTimeController; +import uws.job.parameters.DestructionTimeController.DateField; import uws.job.parameters.ExecutionDurationController; import uws.job.parameters.InputParamController; import uws.job.parameters.UWSParameters; -import uws.job.parameters.DestructionTimeController.DateField; - import uws.job.user.JobOwner; +import uws.service.file.UWSFileManager; +import uws.service.request.RequestParser; +import uws.service.request.UWSRequestParser; /** *

    Abstract implementation of {@link UWSFactory}. * Only the function which creates a {@link JobThread} from a {@link UWSJob} needs to be implemented.

    * - * @author Grégory Mantelet (CDS) - * @version 06/2012 + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (11/2014) */ public abstract class AbstractUWSFactory implements UWSFactory { @@ -101,6 +102,11 @@ public abstract class AbstractUWSFactory implements UWSFactory { return new UWSParameters(req, expectedAdditionalParams, inputParamControllers); } + @Override + public RequestParser createRequestParser(final UWSFileManager fileManager) throws UWSException{ + return new UWSRequestParser(fileManager); + } + /** * Adds the name of an additional parameter which must be identified without taking into account its case * and then stored with the case of the given name. diff --git a/src/uws/service/UWS.java b/src/uws/service/UWS.java index 492b17b..09d5e24 100644 --- a/src/uws/service/UWS.java +++ b/src/uws/service/UWS.java @@ -16,19 +16,18 @@ package uws.service; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import uws.UWSException; - import uws.job.JobList; - import uws.job.serializer.UWSSerializer; - import uws.service.backup.UWSBackupManager; import uws.service.file.UWSFileManager; - import uws.service.log.UWSLog; +import uws.service.request.RequestParser; +import uws.service.request.UWSRequestParser; /** *

    @@ -59,11 +58,28 @@ import uws.service.log.UWSLog; * the UWS and the servlet. *

    * - * @author Grégory Mantelet (CDS) - * @version 05/2012 + *

    IMPORTANT: + * All implementations of this interface should implement properly the function {@link #destroy()} and should call it + * when the JVM or the HTTP server application is closing. + *

    + * + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (02/2015) */ public interface UWS extends Iterable { + /** Attribute of the HttpServletRequest to set and to get in order to access the request ID set by the UWS library. + * @since 4.1 */ + public static final String REQ_ATTRIBUTE_ID = "UWS_REQUEST_ID"; + + /** Attribute of the HttpServletRequest to set and to get in order to access the parameters extracted by the UWS library (using a RequestParser). + * @since 4.1 */ + public static final String REQ_ATTRIBUTE_PARAMETERS = "UWS_PARAMETERS"; + + /** Attribute of the HttpServletRequest to set and to get in order to access the user at the origin of the HTTP request. + * @since 4.1 */ + public static final String REQ_ATTRIBUTE_USER = "UWS_USER"; + /** * Gets the name of this UWS. * @@ -78,6 +94,28 @@ public interface UWS extends Iterable { */ public String getDescription(); + /* ***************** */ + /* RESOURCES RELEASE */ + /* ***************** */ + + /** + *

    + * End properly this UWS: jobs should be backuped (if this feature is enable), timers and threads should be stopped, + * open files and database connections should be closed, etc... + * In brief, this function should release all used resources. + *

    + * + *

    IMPORTANT: This function should be called only when the JVM or the Web Application Server is stopping.

    + * + *

    Note: + * A call to this function may prevent this instance of {@link UWS} to execute any subsequent HTTP request, or the behavior + * would be unpredictable. + *

    + * + * @since 4.1 + */ + public void destroy(); + /* ******************* */ /* JOB LIST MANAGEMENT */ /* ******************* */ @@ -85,7 +123,7 @@ public interface UWS extends Iterable { /** * Adds a jobs list to this UWS. * - * @param jl The jobs list to add. + * @param newJL The jobs list to add. * * @return true if the jobs list has been successfully added, * false if the given jobs list is null or if a jobs list with this name already exists @@ -110,17 +148,6 @@ public interface UWS extends Iterable { */ public int getNbJobList(); - /* - *

    Removes the specified jobs list.

    - *

    note: After the call of this function, the UWS reference of the given jobs list should be removed (see {@link JobList#setUWS(UWS)}).

    - * - * @param name Name of the jobs list to remove. - * - * @return The removed jobs list - * or null if no jobs list with the given name has been found. - * - public JobList removeJobList(final String name) throws UWSException;*/ - /** *

    Destroys the specified jobs list.

    *

    note: After the call of this function, the UWS reference of the given jobs list should be removed (see {@link JobList#setUWS(UWS)}).

    @@ -183,8 +210,9 @@ public interface UWS extends Iterable { /* ******************* */ /** + * Gets the object which is able to identify a user from an HTTP request. * - * @return + * @return Its user identifier. */ public UserIdentifier getUserIdentifier(); @@ -202,6 +230,22 @@ public interface UWS extends Iterable { */ public UWSFactory getFactory(); + /* ******************** */ + /* HTTP REQUEST PARSING */ + /* ******************** */ + + /** + *

    Get its HTTP request parser.

    + *

    note: This parser is the only one to be able to extract UWS and TAP parameters from any HTTP request. + * Its behavior is adapted in function of the used HTTP method and of the content-type. A default implementation is + * provided by the UWS library: {@link UWSRequestParser}.

    + * + * @return Its request parser. + * + * @since 4.1 + */ + public RequestParser getRequestParser(); + /* *************** */ /* FILE MANAGEMENT */ /* *************** */ diff --git a/src/uws/service/UWSFactory.java b/src/uws/service/UWSFactory.java index 4cf384c..73507c2 100644 --- a/src/uws/service/UWSFactory.java +++ b/src/uws/service/UWSFactory.java @@ -16,7 +16,8 @@ package uws.service; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.util.List; @@ -25,20 +26,20 @@ import java.util.Map; import javax.servlet.http.HttpServletRequest; import uws.UWSException; - import uws.job.ErrorSummary; import uws.job.JobThread; import uws.job.Result; import uws.job.UWSJob; - import uws.job.parameters.UWSParameters; import uws.job.user.JobOwner; +import uws.service.file.UWSFileManager; +import uws.service.request.RequestParser; /** * Let's creating UWS jobs, their threads and extracting their parameters from {@link HttpServletRequest}. * - * @author Grégory Mantelet (CDS) - * @version 06/2012 + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (11/2014) * * @see UWS#getFactory() */ @@ -95,7 +96,7 @@ public interface UWSFactory { /** * Lets extracting all parameters from the given request. * - * @param req The request from which parameters must be extracted. + * @param request The request from which parameters must be extracted. * * @return The extracted parameters. * @@ -114,4 +115,18 @@ public interface UWSFactory { */ public UWSParameters createUWSParameters(final Map params) throws UWSException; + /** + * Create a parser of HTTP requests. This object is able to deal with the different formats + * in which parameters are provided in an HTTP request. + * + * @param manager File manager to use if files are uploaded in an HTTP request. + * + * @return The request parser to use. + * + * @throws UWSException If an error occurs while creating the parser. + * + * @since 4.1 + */ + public RequestParser createRequestParser(final UWSFileManager manager) throws UWSException; + } diff --git a/src/uws/service/UWSService.java b/src/uws/service/UWSService.java index b6555c8..2cfac56 100644 --- a/src/uws/service/UWSService.java +++ b/src/uws/service/UWSService.java @@ -16,177 +16,130 @@ package uws.service; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.io.IOException; - import java.net.URL; - import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; import java.util.Vector; + +import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; + import uws.AcceptHeader; import uws.UWSException; -import uws.UWSExceptionFactory; -import uws.job.ExecutionPhase; +import uws.UWSToolBox; import uws.job.JobList; -import uws.job.UWSJob; - -import uws.job.manager.DefaultExecutionManager; +import uws.job.JobThread; import uws.job.serializer.JSONSerializer; import uws.job.serializer.UWSSerializer; import uws.job.serializer.XMLSerializer; import uws.job.user.JobOwner; - import uws.service.actions.AddJob; import uws.service.actions.DestroyJob; import uws.service.actions.GetJobParam; import uws.service.actions.JobSummary; import uws.service.actions.ListJobs; import uws.service.actions.SetJobParam; +import uws.service.actions.SetUWSParameter; import uws.service.actions.ShowHomePage; import uws.service.actions.UWSAction; import uws.service.backup.UWSBackupManager; - import uws.service.error.DefaultUWSErrorWriter; import uws.service.error.ServiceErrorWriter; import uws.service.file.UWSFileManager; import uws.service.log.DefaultUWSLog; import uws.service.log.UWSLog; +import uws.service.log.UWSLog.LogLevel; +import uws.service.request.RequestParser; /** - *

    General description

    - * - *

    An abstract facility to implement the Universal Worker Service pattern.

    + *

    This class implements directly the interface {@link UWS} and so, it represents the core of a UWS service.

    * - *

    It can manage several jobs lists (create new, get and remove).

    + *

    Usage

    * - *

    It also interprets {@link HttpServletRequest}, applies the action specified in its given URL and parameters - * (according to the IVOA Proposed Recommendation of 2010-02-10) - * and returns the corresponding response in a {@link HttpServletResponse}.

    - * - *

    The UWS URL interpreter

    - * - *

    Any subclass of {@link UWSService} has one object called the UWS URL interpreter. It is stored in the field {@link #urlInterpreter}. - * It lets interpreting the URL of any received request. Thus you can know on which jobs list, job and/or job attribute(s) - * the request applies.

    - * - *

    This interpreter must be initialized with the base URL/URI of this UWS. By using the default constructor (the one with no parameter), - * the URL interpreter will be built at the first request (see {@link UWSUrl#UWSUrl(HttpServletRequest)}) and so the base URI is - * extracted directly from the request).

    - * - *

    You want to set another base URI or to use a custom URL interpreter, you have to set yourself the interpreter - * by using the method {@link #setUrlInterpreter(UWSUrl)}.

    + *

    + * Using this class is very simple! An instance must be created by providing at a factory - {@link UWSFactory} - and a file manager - {@link UWSFileManager}. + * This creation must be done in the init() function of a {@link HttpServlet}. Then, still in init(), at least one job list must be created. + * Finally, in order to ensure that all requests are interpreted by the UWS service, they must be sent to the created {@link UWSService} in the function + * {@link #executeRequest(HttpServletRequest, HttpServletResponse)}. + *

    + *

    Here is an example of what should look like the servlet class:

    + *
    + * public class MyUWSService extends HttpServlet {
    + * 	private UWS uws;
      * 
    - * 

    Create a job

    + * public void init(ServletConfig config) throws ServletException { + * try{ + * // Create the UWS service: + * uws = new UWSService(new MyUWSFactory(), new LocalUWSFileManager(new File(config.getServletContext().getRealPath("UWSFiles")))); + * // Create at least one job list (otherwise no job can be started): + * uws.addJobList("jobList"); + * }catch(UWSException ue){ + * throw new ServletException("Can not initialize the UWS service!", ue); + * } + * } * - *

    The most important abstract function of this class is {@link UWSService#createJob(Map)}. It allows to create an instance - * of the type of job which is managed by this UWS. The only parameter is a map of a job attributes. It is the same map that - * take the functions {@link UWSJob#UWSJob(Map)} and {@link UWSJob#addOrUpdateParameters(Map)}.

    + * public void destroy(){ + * if (uws != null) + * uws.destroy(); + * } * - *

    There are two convenient implementations of this abstract method in {@link BasicUWS} and {@link ExtendedUWS}. These two implementations - * are based on the Java Reflection.

    + * public void service(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException{ + * try{ + * service.executeRequest(request, response); + * }catch(UWSException ue){ + * response.sendError(ue.getHttpErrorCode(), ue.getMessage()); + * } + * } + * } + *
    * *

    UWS actions

    * - *

    All the actions described in the IVOA recommendation are already managed. Each of these actions are defined in - * an instance of {@link UWSAction}:

    + *

    + * All standard UWS actions are already implemented in this class. However, it is still possible to modify their implementation and/or to + * add or remove some actions. + *

    + *

    + * A UWS action is actually implemented here by a class extending the abstract class {@link UWSAction}. Here is the full list of all + * the available and already implemented actions: + *

    *
      - *
    • {@link UWSAction#LIST_JOBS LIST_JOBS}: see the class {@link ListJobs}
    • - *
    • {@link UWSAction#ADD_JOB ADD_JOB}: see the class {@link AddJob}
    • - *
    • {@link UWSAction#DESTROY_JOB DESTROY_JOB}: see the class {@link DestroyJob}
    • - *
    • {@link UWSAction#JOB_SUMMARY JOB_SUMMARY}: see the class {@link JobSummary}
    • - *
    • {@link UWSAction#GET_JOB_PARAM GET_JOB_PARAM}: see the class {@link GetJobParam}
    • - *
    • {@link UWSAction#SET_JOB_PARAM SET_JOB_PARAM}: see the class {@link SetJobParam}
    • - *
    • {@link UWSAction#HOME_PAGE HOME_PAGE}: see the class {@link ShowHomePage}
    • + *
    • {@link AddJob}
    • + *
    • {@link DestroyJob}
    • + *
    • {@link JobSummary}
    • + *
    • {@link GetJobParam}
    • + *
    • {@link SetJobParam}
    • + *
    • {@link ListJobs}
    • *
    - * - *

    However you can add your own UWS actions ! To do that you just need to implement the abstract class {@link UWSAction} - * and to call the method {@link #addUWSAction(UWSAction)} with an instance of this implementation.

    - * - *

    IMPORTANT: You must be careful when you override the function {@link UWSAction#match(UWSUrl, String, HttpServletRequest)} - * so that your test is as precise as possible ! Indeed the order in which the actions of a UWS are evaluated is very important !
    - * If you want to be sure your custom UWS action is always evaluated before any other UWS action you can use the function - * {@link #addUWSAction(int, UWSAction)} with 0 as first parameter !

    - * - *

    Note: You can also replace an existing UWS action thanks to the method {@link #replaceUWSAction(UWSAction)} or - * {@link #setUWSAction(int, UWSAction)} !

    - * - *

    User identification

    - * - *

    Some UWS actions need to know the current user so that they can adapt their response (i.e. LIST_JOBS must return the jobs of only - * one user: the current one). Thus, before executing a UWS action (and even before choosing the good action in function of the request) - * the function {@link UserIdentifier#extractUserId(UWSUrl, HttpServletRequest)} is called. Its goal - * is to identify the current user in function of the received request.

    - * *

    - * Notes: - *

      - *
    • If this function returns NULL, the UWS actions must be executed on all jobs, whatever is their owner !
    • - *
    • {@link UserIdentifier} is an interface. So you must implement it and then set its extension to this UWS - * by using {@link #setUserIdentifier(UserIdentifier)}.
    • - *
    - *

    + * To add an action, you should use the function {@link #addUWSAction(UWSAction)}, to remove one {@link #removeUWSAction(int)} or {@link #removeUWSAction(String)}. + * Note that this last function takes a String parameter. This parameter is the name of the UWS action to remove. Indeed, each UWS action must have an internal + * name representing the action. Thus, it is possible to replace a UWS action implementation by using the function {@link #replaceUWSAction(UWSAction)} ; this + * function will replace the action having the same name as the given action. *

    * - *

    Queue management

    - * - *

    One of the goals of a UWS is to manage an execution queue for all managed jobs. This task is given to an instance - * of {@link DefaultExecutionManager}, stored in the field {@link #executionManager}. Each time a job is created, - * the UWS sets it the execution manager (see {@link AddJob}). Thus the {@link UWSJob#start()} method will ask to the manager - * whether it can execute now or whether it must be put in a {@link ExecutionPhase#QUEUED QUEUED} phase until enough resources are available for its execution.

    - * - *

    By extending the class {@link DefaultExecutionManager} and by overriding {@link DefaultExecutionManager#isReadyForExecution(UWSJob)} - * you can change the condition which puts a job in the {@link ExecutionPhase#EXECUTING EXECUTING} or in the {@link ExecutionPhase#QUEUED QUEUED} phase. By default, a job is put - * in the {@link ExecutionPhase#QUEUED QUEUED} phase if there are more running jobs than a given number.

    - * - *

    With this manager it is also possible to list all running jobs in addition of all queued jobs, thanks to the methods: - * {@link DefaultExecutionManager#getRunningJobs()}, {@link DefaultExecutionManager#getQueuedJobs()}, {@link DefaultExecutionManager#getNbRunningJobs()} - * and {@link DefaultExecutionManager#getNbQueuedJobs()}.

    - * - *

    Serializers & MIME types

    - * - *

    According to the IVOA recommendation, the XML format is the default format in which each UWS resource must be returned. However it - * is told that other formats can also be managed. To allow that, {@link UWSService} manages a list of {@link UWSSerializer} and - * lets define which is the default one to use. By default, there are two serializers: {@link XMLSerializer} (the default choice) - * and {@link JSONSerializer}.

    - * - *

    One proposed way to choose automatically the format to use is to look at the Accept header of a HTTP-Request. This header field is - * a list of MIME types (each one with a quality - a sort of priority). Thus each {@link UWSSerializer} is associated with a MIME type so - * that {@link UWSService} can choose automatically the preferred format and so, the serializer to use.

    - * - *

    WARNING: Only one {@link UWSSerializer} can be associated with a given MIME type in an {@link UWSService} instance ! - * Thus, if you add a {@link UWSSerializer} to a UWS, and this UWS has already a serializer for the same MIME type, - * it will be replaced by the added one.

    + *

    Home page

    * - *

    Note: A XML document can be linked to a XSLT style-sheet. By using the method {@link XMLSerializer#setXSLTPath(String)} - * you can define the path/URL of the XLST to link to each UWS resource.
    - * Since the {@link XMLSerializer} is the default format for a UWS resource you can also use the function - * {@link UWSService#setXsltURL(String)} !

    - * - *

    The UWS Home page

    - * - *

    As for a job or a jobs list, a UWS is also a UWS resource. That's why it can also be serialized !

    - * - *

    However in some cases it could more interesting to substitute this resource by a home page of the whole UWS by using the function: - * {@link #setHomePage(String)} or {@link #setHomePage(URL, boolean)}. + *

    + * In addition of all the actions listed above, a last action is automatically added: {@link ShowHomePage}. This is the action which will display the home page of + * the UWS service. It is called when the root resource of the web service is asked. To change it, you can either overwrite this action + * (see {@link #replaceUWSAction(UWSAction)}) or set an home page URL with the function {@link #setHomePage(String)} (the parameter is a URI pointing on either + * a local or a remote resource) or {@link #setHomePage(URL, boolean)}. *

    * - *

    Note: To go back to the UWS serialization (that is to say to abort a call to {@link #setHomePage(String)}), - * use the method {@link #setDefaultHomePage()} !

    - * - * - * @author Grégory Mantelet (CDS) - * @version 06/2012 + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (04/2015) */ public class UWSService implements UWS { - private static final long serialVersionUID = 1L; /** Name of this UWS. */ protected String name = null; @@ -236,30 +189,41 @@ public class UWSService implements UWS { /** Lets logging info/debug/warnings/errors about this UWS. */ protected UWSLog logger; + /** Lets extract all parameters from an HTTP request, whatever is its content-type. + * @since 4.1*/ + protected final RequestParser requestParser; + /** Lets writing/formatting any exception/throwable in a HttpServletResponse. */ protected ServiceErrorWriter errorWriter; + /** Last generated request ID. If the next generated request ID is equivalent to this one, + * a new one will generate in order to ensure the unicity. + * @since 4.1 */ + protected static String lastRequestID = null; + /* ************ */ /* CONSTRUCTORS */ - /* ************ *//** - *

    Builds a UWS (the base URI will be extracted at the first request directly from the request itself).

    - * - *

    - * By default, this UWS has 2 serialization formats: XML ({@link XMLSerializer}) and JSON ({@link JSONSerializer}). - * All the default actions of a UWS are also already implemented. - * However, you still have to create at least one job list ! - *

    - * - *

    note: since no logger is provided, a default one is set automatically (see {@link DefaultUWSLog}).

    - * - * @param jobFactory Object which lets creating the UWS jobs managed by this UWS and their thread/task. - * @param fileManager Object which lets managing all files managed by this UWS (i.e. log, result, backup, error, ...). - * - * @throws NullPointerException If at least one of the parameters is null. - * - * @see #UWSService(UWSFactory, UWSFileManager, UWSLog) - */ - public UWSService(final UWSFactory jobFactory, final UWSFileManager fileManager){ + /* ************ */ + /** + *

    Builds a UWS (the base URI will be extracted at the first request directly from the request itself).

    + * + *

    + * By default, this UWS has 2 serialization formats: XML ({@link XMLSerializer}) and JSON ({@link JSONSerializer}). + * All the default actions of a UWS are also already implemented. + * However, you still have to create at least one job list ! + *

    + * + *

    note: since no logger is provided, a default one is set automatically (see {@link DefaultUWSLog}).

    + * + * @param jobFactory Object which lets creating the UWS jobs managed by this UWS and their thread/task. + * @param fileManager Object which lets managing all files managed by this UWS (i.e. log, result, backup, error, ...). + * + * @throws NullPointerException If at least one of the parameters is null. + * @throws UWSException If unable to create a request parser using the factory (see {@link UWSFactory#createRequestParser(UWSFileManager)}). + * + * @see #UWSService(UWSFactory, UWSFileManager, UWSLog) + */ + public UWSService(final UWSFactory jobFactory, final UWSFileManager fileManager) throws UWSException{ this(jobFactory, fileManager, (UWSLog)null); } @@ -277,18 +241,22 @@ public class UWSService implements UWS { * @param logger Object which lets printing any message (error, info, debug, warning). * * @throws NullPointerException If at least one of the parameters is null. + * @throws UWSException If unable to create a request parser using the factory (see {@link UWSFactory#createRequestParser(UWSFileManager)}). */ - public UWSService(final UWSFactory jobFactory, final UWSFileManager fileManager, final UWSLog logger){ + public UWSService(final UWSFactory jobFactory, final UWSFileManager fileManager, final UWSLog logger) throws UWSException{ if (jobFactory == null) - throw new NullPointerException("Missing UWS factory ! Can not create a UWSService."); + throw new NullPointerException("Missing UWS factory! Can not create a UWSService."); factory = jobFactory; if (fileManager == null) - throw new NullPointerException("Missing UWS file manager ! Can not create a UWSService."); + throw new NullPointerException("Missing UWS file manager! Can not create a UWSService."); this.fileManager = fileManager; this.logger = (logger == null) ? new DefaultUWSLog(this) : logger; - errorWriter = new DefaultUWSErrorWriter(this); + + requestParser = jobFactory.createRequestParser(fileManager); + + errorWriter = new DefaultUWSErrorWriter(this.logger); // Initialize the list of jobs: mapJobLists = new LinkedHashMap(); @@ -305,6 +273,7 @@ public class UWSService implements UWS { uwsActions.add(new ShowHomePage(this)); uwsActions.add(new ListJobs(this)); uwsActions.add(new AddJob(this)); + uwsActions.add(new SetUWSParameter(this)); uwsActions.add(new DestroyJob(this)); uwsActions.add(new JobSummary(this)); uwsActions.add(new GetJobParam(this)); @@ -345,11 +314,22 @@ public class UWSService implements UWS { // Extract the name of the UWS: try{ + // Set the URL interpreter: urlInterpreter = new UWSUrl(baseURI); + + // ...and the name of this service: name = urlInterpreter.getUWSName(); - getLogger().uwsInitialized(this); - }catch(UWSException ex){ - throw new UWSException(UWSException.BAD_REQUEST, ex, "Invalid base UWS URI (" + baseURI + ") !"); + + // Log the successful initialization: + logger.logUWS(LogLevel.INFO, this, "INIT", "UWS successfully initialized!", null); + + }catch(NullPointerException ex){ + // Log the exception: + // (since the first constructor has already been called successfully, the logger is now NOT NULL): + logger.logUWS(LogLevel.FATAL, null, "INIT", "Invalid base UWS URI: " + baseURI + "! You should check the configuration of the service.", ex); + + // Throw a new UWSException with a more understandable message: + throw new UWSException(UWSException.BAD_REQUEST, ex, "Invalid base UWS URI (" + baseURI + ")!"); } } @@ -362,9 +342,11 @@ public class UWSService implements UWS { * @param fileManager Object which lets managing all files managed by this UWS (i.e. log, result, backup, error, ...). * @param urlInterpreter The UWS URL interpreter to use in this UWS. * + * @throws UWSException If unable to create a request parser using the factory (see {@link UWSFactory#createRequestParser(UWSFileManager)}). + * * @see #UWSService(UWSFactory, UWSFileManager, UWSLog, UWSUrl) */ - public UWSService(final UWSFactory jobFactory, final UWSFileManager fileManager, final UWSUrl urlInterpreter){ + public UWSService(final UWSFactory jobFactory, final UWSFileManager fileManager, final UWSUrl urlInterpreter) throws UWSException{ this(jobFactory, fileManager, null, urlInterpreter); } @@ -375,17 +357,49 @@ public class UWSService implements UWS { * @param fileManager Object which lets managing all files managed by this UWS (i.e. log, result, backup, error, ...). * @param logger Object which lets printing any message (error, info, debug, warning). * @param urlInterpreter The UWS URL interpreter to use in this UWS. + * + * @throws UWSException If unable to create a request parser using the factory (see {@link UWSFactory#createRequestParser(UWSFileManager)}). */ - public UWSService(final UWSFactory jobFactory, final UWSFileManager fileManager, final UWSLog logger, final UWSUrl urlInterpreter){ + public UWSService(final UWSFactory jobFactory, final UWSFileManager fileManager, final UWSLog logger, final UWSUrl urlInterpreter) throws UWSException{ this(jobFactory, fileManager, logger); setUrlInterpreter(urlInterpreter); if (this.urlInterpreter != null) - getLogger().uwsInitialized(this); + logger.logUWS(LogLevel.INFO, this, "INIT", "UWS successfully initialized.", null); + } + + @Override + public void destroy(){ + // Backup all jobs: + /* Jobs are backuped now so that running jobs are set back to the PENDING phase in the backup. + * Indeed, the "stopAll" operation of the ExecutionManager may fail and would set the phase to ERROR + * for the wrong reason. */ + if (backupManager != null){ + // save all jobs: + backupManager.setEnabled(true); + backupManager.saveAll(); + // stop the automatic backup, if there is one: + backupManager.setEnabled(false); + } + + // Stop all jobs and stop watching for the jobs' destruction: + for(JobList jl : mapJobLists.values()){ + jl.getExecutionManager().stopAll(); + jl.getDestructionManager().stop(); + } + + // Just in case that previous clean "stop"s did not work, try again an interruption for all running threads: + /* note: timers are not part of this ThreadGroup and so, they won't be affected by this function call. */ + JobThread.tg.interrupt(); + + // Log the service is stopped: + if (logger != null) + logger.logUWS(LogLevel.INFO, this, "STOP", "UWS Service \"" + getName() + "\" stopped!", null); } /* ************** */ /* LOG MANAGEMENT */ /* ************** */ + @Override public UWSLog getLogger(){ return logger; } @@ -414,6 +428,7 @@ public class UWSService implements UWS { /* ***************** */ /* GETTERS & SETTERS */ /* ***************** */ + @Override public final String getName(){ return name; } @@ -427,6 +442,7 @@ public class UWSService implements UWS { this.name = name; } + @Override public final String getDescription(){ return description; } @@ -451,6 +467,7 @@ public class UWSService implements UWS { return (urlInterpreter == null) ? null : urlInterpreter.getBaseURI(); } + @Override public final UWSUrl getUrlInterpreter(){ return urlInterpreter; } @@ -464,6 +481,8 @@ public class UWSService implements UWS { this.urlInterpreter = urlInterpreter; if (name == null && urlInterpreter != null) name = urlInterpreter.getUWSName(); + if (this.urlInterpreter != null) + this.urlInterpreter.setUwsURI(null); } /** @@ -472,6 +491,7 @@ public class UWSService implements UWS { * * @return The used UserIdentifier (MAY BE NULL). */ + @Override public final UserIdentifier getUserIdentifier(){ return userIdentifier; } @@ -485,14 +505,17 @@ public class UWSService implements UWS { userIdentifier = identifier; } + @Override public final UWSFactory getFactory(){ return factory; } + @Override public final UWSFileManager getFileManager(){ return fileManager; } + @Override public final UWSBackupManager getBackupManager(){ return backupManager; } @@ -500,7 +523,7 @@ public class UWSService implements UWS { /** *

    * Sets its backup manager. - * This manager will be called at each user action to save only its own jobs list by calling {@link UWSBackupManager#saveOwner(String)}. + * This manager will be called at each user action to save only its own jobs list by calling {@link UWSBackupManager#saveOwner(JobOwner)}. *

    * * @param backupManager Its new backup manager. @@ -509,6 +532,11 @@ public class UWSService implements UWS { this.backupManager = backupManager; } + @Override + public final RequestParser getRequestParser(){ + return requestParser; + } + /* ******************** */ /* HOME PAGE MANAGEMENT */ /* ******************** */ @@ -596,7 +624,7 @@ public class UWSService implements UWS { if (serializers.containsKey(mimeType)) defaultSerializer = mimeType; else - throw UWSExceptionFactory.missingSerializer(mimeType, "Impossible to set the default serializer."); + throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, "Missing UWS serializer for the MIME types: " + mimeType + "! The default serializer won't be set."); } /** @@ -646,6 +674,7 @@ public class UWSService implements UWS { return serializers.values().iterator(); } + @Override public final UWSSerializer getSerializer(String mimeTypes) throws UWSException{ choosenSerializer = null; @@ -725,14 +754,17 @@ public class UWSService implements UWS { * * @see java.lang.Iterable#iterator() */ + @Override public final Iterator iterator(){ return mapJobLists.values().iterator(); } + @Override public final JobList getJobList(String name){ return mapJobLists.get(name); } + @Override public final int getNbJobList(){ return mapJobLists.size(); } @@ -746,9 +778,10 @@ public class UWSService implements UWS { * false if the given jobs list is null or if a jobs list with this name already exists * or if a UWS is already associated with another UWS. * - * @see JobList#setUWS(AbstractUWS) + * @see JobList#setUWS(UWS) * @see UWS#addJobList(JobList) */ + @Override public final boolean addJobList(JobList jl){ if (jl == null) return false; @@ -759,44 +792,14 @@ public class UWSService implements UWS { jl.setUWS(this); mapJobLists.put(jl.getName(), jl); }catch(IllegalStateException ise){ - logger.error("The jobs list \"" + jl.getName() + "\" can not be added into the UWS " + getName() + " !", ise); + logger.logUWS(LogLevel.ERROR, jl, "ADD_JOB_LIST", "The jobs list \"" + jl.getName() + "\" can not be added into the UWS " + getName() + ": it may already be associated with one!", ise); return false; } return true; } - /*public final JobList removeJobList(String name){ - JobList jl = mapJobLists.get(name); - if (jl != null){ - if (removeJobList(jl)) - return jl; - } - return null; - }*/ - - /* - * Removes the given jobs list from this UWS. - * - * @param jl The jobs list to remove. - * - * @return true if the jobs list has been successfully removed, false otherwise. - * - * @see JobList#removeAll() - * @see JobList#setUWS(UWSService) - * - public boolean removeJobList(JobList jl){ - if (jl == null) - return false; - - jl = mapJobLists.remove(jl.getName()); - if (jl != null){ - jl.removeAll(); - jl.setUWS(null); - } - return jl != null; - }*/ - + @Override public final boolean destroyJobList(String name){ return destroyJobList(mapJobLists.get(name)); } @@ -809,7 +812,7 @@ public class UWSService implements UWS { * @return true if the given jobs list has been destroyed, false otherwise. * * @see JobList#clear() - * @see JobList#setUWS(UWSService) + * @see JobList#setUWS(UWS) */ public boolean destroyJobList(JobList jl){ if (jl == null) @@ -821,23 +824,12 @@ public class UWSService implements UWS { jl.clear(); jl.setUWS(null); }catch(IllegalStateException ise){ - getLogger().warning("Impossible to erase completely the association between the jobs list \"" + jl.getName() + "\" and the UWS \"" + getName() + "\", because: " + ise.getMessage()); + logger.logUWS(LogLevel.WARNING, jl, "DESTROY_JOB_LIST", "Impossible to erase completely the association between the jobs list \"" + jl.getName() + "\" and the UWS \"" + getName() + "\"!", ise); } } return jl != null; } - /* - * Removes all managed jobs lists. - * - * @see #removeJobList(String) - * - public final void removeAllJobLists(){ - ArrayList jlNames = new ArrayList(mapJobLists.keySet()); - for(String jlName : jlNames) - removeJobList(jlName); - }*/ - /** * Destroys all managed jobs lists. * @@ -856,7 +848,7 @@ public class UWSService implements UWS { *

    Lets adding the given action to this UWS.

    * *

    WARNING: The action will be added at the end of the actions list of this UWS. That means, it will be evaluated (call of - * the method {@link UWSAction#match(UWSUrl, String, HttpServletRequest)}) lastly !

    + * the method {@link UWSAction#match(UWSUrl, JobOwner, HttpServletRequest)}) lastly !

    * * @param action The UWS action to add. * @@ -1000,6 +992,27 @@ public class UWSService implements UWS { /* ********************** */ /* UWS MANAGEMENT METHODS */ /* ********************** */ + + /** + *

    Generate a unique ID for the given request.

    + * + *

    By default, a timestamp is returned.

    + * + * @param request Request whose an ID is asked. + * + * @return The ID of the given request. + * + * @since 4.1 + */ + protected synchronized String generateRequestID(final HttpServletRequest request){ + String id; + do{ + id = System.currentTimeMillis() + ""; + }while(lastRequestID != null && lastRequestID.startsWith(id)); + lastRequestID = id; + return id; + } + /** *

    Executes the given request according to the IVOA Proposed Recommendation of 2010-02-10. * The result is returned in the given response.

    @@ -1009,7 +1022,7 @@ public class UWSService implements UWS { *
  • Load the request in the UWS URL interpreter (see {@link UWSUrl#load(HttpServletRequest)})
  • *
  • Extract the user ID (see {@link UserIdentifier#extractUserId(UWSUrl, HttpServletRequest)})
  • *
  • Iterate - in order - on all available actions and apply the first which matches. - * (see {@link UWSAction#match(UWSUrl, String, HttpServletRequest)} and {@link UWSAction#apply(UWSUrl, String, HttpServletRequest, HttpServletResponse)})
  • + * (see {@link UWSAction#match(UWSUrl, JobOwner, HttpServletRequest)} and {@link UWSAction#apply(UWSUrl, JobOwner, HttpServletRequest, HttpServletResponse)}) * * * @param request The UWS request. @@ -1023,27 +1036,52 @@ public class UWSService implements UWS { * @see UWSUrl#UWSUrl(HttpServletRequest) * @see UWSUrl#load(HttpServletRequest) * @see UserIdentifier#extractUserId(UWSUrl, HttpServletRequest) - * @see UWSAction#match(UWSUrl, String, HttpServletRequest) - * @see UWSAction#apply(UWSUrl, String, HttpServletRequest, HttpServletResponse) + * @see UWSAction#match(UWSUrl, JobOwner, HttpServletRequest) + * @see UWSAction#apply(UWSUrl, JobOwner, HttpServletRequest, HttpServletResponse) */ public boolean executeRequest(HttpServletRequest request, HttpServletResponse response) throws UWSException, IOException{ if (request == null || response == null) return false; + // Generate a unique ID for this request execution (for log purpose only): + final String reqID = generateRequestID(request); + if (request.getAttribute(UWS.REQ_ATTRIBUTE_ID) == null) + request.setAttribute(UWS.REQ_ATTRIBUTE_ID, reqID); + + // Extract all parameters: + if (request.getAttribute(UWS.REQ_ATTRIBUTE_PARAMETERS) == null){ + try{ + request.setAttribute(UWS.REQ_ATTRIBUTE_PARAMETERS, requestParser.parse(request)); + }catch(UWSException ue){ + logger.log(LogLevel.ERROR, "REQUEST_PARSER", "Can not extract the HTTP request parameters!", ue); + } + } + + // Log the reception of the request: + logger.logHttp(LogLevel.INFO, request, reqID, null, null); + boolean actionApplied = false; UWSAction action = null; JobOwner user = null; try{ - // Update the UWS URL interpreter: - if (urlInterpreter == null){ + if (this.urlInterpreter == null){ + // Initialize the URL interpreter if not already done: setUrlInterpreter(new UWSUrl(request)); - getLogger().uwsInitialized(this); + + // Log the successful initialization: + logger.logUWS(LogLevel.INFO, this, "INIT", "UWS successfully initialized.", null); } + + // Update the UWS URL interpreter: + UWSUrl urlInterpreter = new UWSUrl(this.urlInterpreter); urlInterpreter.load(request); // Identify the user: - user = (userIdentifier == null) ? null : userIdentifier.extractUserId(urlInterpreter, request); + user = UWSToolBox.getUser(request, userIdentifier); + + // Set the character encoding: + response.setCharacterEncoding(UWSToolBox.DEFAULT_CHAR_ENCODING); // Apply the appropriate UWS action: for(int i = 0; action == null && i < uwsActions.size(); i++){ @@ -1056,19 +1094,64 @@ public class UWSService implements UWS { // If no corresponding action has been found, throw an error: if (action == null) - throw new UWSException(UWSException.NOT_IMPLEMENTED, "Unknown UWS action ! This HTTP request can not be interpreted by this UWS service !"); + throw new UWSException(UWSException.NOT_IMPLEMENTED, "Unknown UWS action!"); response.flushBuffer(); - logger.httpRequest(request, user, (action != null) ? action.getName() : null, HttpServletResponse.SC_OK, "[OK]", null); + + // Log the successful execution of the action: + logger.logHttp(LogLevel.INFO, response, reqID, user, "UWS action \"" + ((action != null) ? action.getName() : null) + "\" successfully executed.", null); + + }catch(IOException ioe){ + /* + * Any IOException thrown while writing the HTTP response is generally caused by a client abortion (intentional or timeout) + * or by a connection closed with the client for another reason. + * Consequently, a such error should not be considered as a real error from the server or the library: the request is + * canceled, and so the response is not expected. It is anyway not possible any more to send it (header and/or body) totally + * or partially. + * Nothing can solve this error. So the "error" is just reported as a simple information and theoretically the action + * executed when this error has been thrown is already stopped. + */ + logger.logHttp(LogLevel.INFO, response, reqID, user, "HTTP request aborted or connection with the client closed => the UWS action \"" + action.getName() + "\" has stopped and the body of the HTTP response can not have been partially or completely written!", null); }catch(UWSException ex){ + /* + * Any known/"expected" UWS exception is logged but also returned to the HTTP client in an error document. + * Since the error is known, it is supposed to have already been logged with a full stack trace. Thus, there + * is no need to log again its stack trace...just its message is logged. + * Besides, this error may also be just a redirection and not a true error. In such case, the error message + * is not logged. + */ + // If redirection, flag the action as executed with success: if (ex.getHttpErrorCode() == UWSException.SEE_OTHER) actionApplied = true; - sendError(ex, request, user, (action != null) ? action.getName() : null, response); - }catch(Exception ex){ - sendError(ex, request, user, (action != null) ? action.getName() : null, response); + sendError(ex, request, reqID, user, ((action != null) ? action.getName() : null), response); + + }catch(IllegalStateException ise){ + /* + * Any IllegalStateException that reaches this point, is supposed coming from a HttpServletResponse operation which + * has to reset the response buffer (e.g. resetBuffer(), sendRedirect(), sendError()). + * If this exception happens, the library tried to rewrite the HTTP response body with a message or a result, + * while this body has already been partially sent to the client. It is then no longer possible to change its content. + * Consequently, the error is logged as FATAL and a message will be appended at the end of the already submitted response + * to alert the HTTP client that an error occurs and the response should not be considered as complete and reliable. + */ + // Write the error in the response and return the appropriate HTTP status code: + errorWriter.writeError(ise, response, request, reqID, user, ((action != null) ? action.getName() : null)); + // Log the error: + getLogger().logHttp(LogLevel.FATAL, response, reqID, user, "HTTP response already partially committed => the UWS action \"" + action.getName() + "\" has stopped and the body of the HTTP response can not have been partially or completely written!", (ise.getCause() != null) ? ise.getCause() : ise); + + }catch(Throwable t){ + /* + * Any other error is considered as unexpected if it reaches this point. Consequently, it has not yet been logged. + * So its stack trace will be fully logged, and an appropriate message will be returned to the HTTP client. The + * returned document should contain not too technical information which would be useless for the user. + */ + sendError(t, request, reqID, user, ((action != null) ? action.getName() : null), response); + }finally{ executedAction = action; + // Free resources about uploaded files ; only unused files will be deleted: + UWSToolBox.deleteUploads(request); } return actionApplied; @@ -1089,9 +1172,9 @@ public class UWSService implements UWS { public void redirect(String url, HttpServletRequest request, JobOwner user, String uwsAction, HttpServletResponse response) throws IOException, UWSException{ response.setStatus(HttpServletResponse.SC_SEE_OTHER); response.setContentType(request.getContentType()); + response.setCharacterEncoding(UWSToolBox.DEFAULT_CHAR_ENCODING); response.setHeader("Location", url); response.flushBuffer(); - logger.httpRequest(request, user, uwsAction, HttpServletResponse.SC_SEE_OTHER, "[Redirection toward " + url + "]", null); } /** @@ -1103,26 +1186,25 @@ public class UWSService implements UWS { * * @param error The error to send/display. * @param request The request which has caused the given error (not used by default). + * @param reqID ID of the request. * @param user The user which executes the given request. * @param uwsAction The UWS action corresponding to the given request. * @param response The response in which the error must be published. * * @throws IOException If there is an error when calling {@link #redirect(String, HttpServletRequest, JobOwner, String, HttpServletResponse)} or {@link HttpServletResponse#sendError(int, String)}. - * @throws UWSException If there is an error when calling {@link #redirect(String, HttpServletRequest, JobOwner, String, HttpServletResponse))}. + * @throws UWSException If there is an error when calling {@link #redirect(String, HttpServletRequest, JobOwner, String, HttpServletResponse)}. * * @see #redirect(String, HttpServletRequest, JobOwner, String, HttpServletResponse) - * @see {@link ServiceErrorWriter#writeError(Throwable, HttpServletResponse, HttpServletRequest, JobOwner, String)} + * @see #sendError(Throwable, HttpServletRequest, String, JobOwner, String, HttpServletResponse) */ - public final void sendError(UWSException error, HttpServletRequest request, JobOwner user, String uwsAction, HttpServletResponse response) throws IOException, UWSException{ - if (error.getHttpErrorCode() == UWSException.SEE_OTHER) + public final void sendError(UWSException error, HttpServletRequest request, String reqID, JobOwner user, String uwsAction, HttpServletResponse response) throws IOException, UWSException{ + if (error.getHttpErrorCode() == UWSException.SEE_OTHER){ + // Log the redirection, if any: + logger.logHttp(LogLevel.INFO, response, reqID, user, "HTTP " + UWSException.SEE_OTHER + " [Redirection toward " + error.getMessage() + "] - Action \"" + uwsAction + "\" successfully executed.", null); + // Apply the redirection: redirect(error.getMessage(), request, user, uwsAction, response); - else{ - errorWriter.writeError(error, response, request, user, uwsAction); - /*if (error.getHttpErrorCode() == UWSException.INTERNAL_SERVER_ERROR) - logger.error(error); - response.sendError(error.getHttpErrorCode(), error.getMessage()); - logger.httpRequest(request, user, uwsAction, error.getHttpErrorCode(), error.getMessage(), error);*/ - } + }else + sendError((Throwable)error, request, reqID, user, uwsAction, response); } /** @@ -1136,20 +1218,23 @@ public class UWSService implements UWS { * * @param error The error to send/display. * @param request The request which has caused the given error (not used by default). + * @param reqID ID of the request. * @param user The user which executes the given request. - * @param uwsAction The UWS action corresponding to the given request. + * @param uwsAction The UWS action corresponding to the given request. * @param response The response in which the error must be published. * * @throws IOException If there is an error when calling {@link HttpServletResponse#sendError(int, String)}. - * @throws UWSException * - * @see {@link ServiceErrorWriter#writeError(Throwable, HttpServletResponse, HttpServletRequest, JobOwner, String)} + * @see ServiceErrorWriter#writeError(Throwable, HttpServletResponse, HttpServletRequest, String, JobOwner, String) */ - public final void sendError(Exception error, HttpServletRequest request, JobOwner user, String uwsAction, HttpServletResponse response) throws IOException, UWSException{ - errorWriter.writeError(error, response, request, user, uwsAction); - /*logger.error(error); - response.sendError(UWSException.INTERNAL_SERVER_ERROR, error.getMessage()); - logger.httpRequest(request, user, uwsAction, UWSException.INTERNAL_SERVER_ERROR, error.getMessage(), error);*/ + public final void sendError(Throwable error, HttpServletRequest request, String reqID, JobOwner user, String uwsAction, HttpServletResponse response) throws IOException{ + // Write the error in the response and return the appropriate HTTP status code: + errorWriter.writeError(error, response, request, reqID, user, uwsAction); + // Log the error: + if (error instanceof UWSException) + logger.logHttp(LogLevel.ERROR, response, reqID, user, "UWS action \"" + uwsAction + "\" FAILED with the error: \"" + error.getMessage() + "\"!", null); + else + logger.logHttp(LogLevel.FATAL, response, reqID, user, "UWS action \"" + uwsAction + "\" execution FAILED with a GRAVE error!", error); } } diff --git a/src/uws/service/UWSServlet.java b/src/uws/service/UWSServlet.java index bacb480..52c4201 100644 --- a/src/uws/service/UWSServlet.java +++ b/src/uws/service/UWSServlet.java @@ -16,14 +16,14 @@ package uws.service; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; - import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; @@ -31,52 +31,44 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.lang.IllegalStateException; - import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; - import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.apache.catalina.connector.ClientAbortException; - import uws.AcceptHeader; import uws.UWSException; import uws.UWSExceptionFactory; import uws.UWSToolBox; - import uws.job.ErrorSummary; import uws.job.JobList; +import uws.job.JobThread; import uws.job.Result; import uws.job.UWSJob; - import uws.job.parameters.DestructionTimeController; +import uws.job.parameters.DestructionTimeController.DateField; import uws.job.parameters.ExecutionDurationController; import uws.job.parameters.InputParamController; import uws.job.parameters.UWSParameters; -import uws.job.parameters.DestructionTimeController.DateField; - import uws.job.serializer.JSONSerializer; import uws.job.serializer.UWSSerializer; import uws.job.serializer.XMLSerializer; - import uws.job.user.JobOwner; - import uws.service.actions.UWSAction; - import uws.service.backup.UWSBackupManager; - import uws.service.error.DefaultUWSErrorWriter; import uws.service.error.ServiceErrorWriter; import uws.service.file.LocalUWSFileManager; import uws.service.file.UWSFileManager; - import uws.service.log.DefaultUWSLog; import uws.service.log.UWSLog; +import uws.service.log.UWSLog.LogLevel; +import uws.service.request.RequestParser; +import uws.service.request.UWSRequestParser; +import uws.service.request.UploadFile; /** *

    @@ -88,7 +80,7 @@ import uws.service.log.UWSLog; *

    UWS Definition

    *

    * To create a such servlet, you have to extend this class. Once done, only two functions must be - * implemented: {@link #createJob(Map, JobOwner)} and {@link #initUWS()}. + * implemented: {@link #createJob(HttpServletRequest, JobOwner)} and {@link #initUWS()}. *

    *

    * The first one will be called by the library each time a job must be created. All the job parameters @@ -101,12 +93,37 @@ import uws.service.log.UWSLog; * * addJobList(new JobList<MyJob>("jlName")); * + *

    The below code show an example of usage of this class:

    + *
    + * public class MyUWSServlet extends UWSServlet {
    + * 
    + * 	// Initialize the UWS service by creating at least one job list.
    + * 	public void initUWS() throws UWSException {
    + * 		addJobList(new JobList("jobList"));
    + * 	}
    + * 
    + * 	// Create the job process corresponding to the job to execute ; generally, the process identification can be merely done by checking the job list name. 
    + * 	public JobThread createJobThread(UWSJob job) throws UWSException {
    + * 		if (job.getJobList().getName().equals("jobList"))
    + * 			return new MyJobThread(job);
    + * 		else
    + * 			throw new UWSException("Impossible to create a job inside the jobs list \"" + job.getJobList().getName() + "\" !");
    + * 	}
    + * }
    + * 
    *

    * The name and the description of the UWS may be specified in the web.xml file as init-param of the servlet: * name and description. The other way is to directly set the corresponding * attributes: {@link #name} and {@link #description}. *

    * + *

    Note: + * If any error occurs while the initialization or the creation of a {@link UWSServlet} instance, a {@link ServletException} + * will be thrown with a basic message dedicated to the service users. This basic and non-informative message is + * obviously not intended to the administrator which will be able to get the reason of the failure + * (with a stack trace when available) in the log files. + *

    + * *

    UWS customization

    *

    * As for the classic HTTP servlets, this servlet has one method for each method of the implemented protocol. @@ -114,12 +131,12 @@ import uws.service.log.UWSLog; * These functions are: *

    *
      - *
    • {@link #doAddJob(HttpServletRequest, HttpServletResponse, JobOwner)}
    • - *
    • {@link #doDestroyJob(HttpServletRequest, HttpServletResponse, JobOwner)}
    • - *
    • {@link #doGetJobParam(HttpServletRequest, HttpServletResponse, JobOwner)}
    • - *
    • {@link #doJobSummary(HttpServletRequest, HttpServletResponse, JobOwner)}
    • - *
    • {@link #doListJob(HttpServletRequest, HttpServletResponse, JobOwner)}
    • - *
    • {@link #doSetJobParam(HttpServletRequest, HttpServletResponse, JobOwner)}
    • + *
    • {@link #doAddJob(UWSUrl, HttpServletRequest, HttpServletResponse, JobOwner)}
    • + *
    • {@link #doDestroyJob(UWSUrl, HttpServletRequest, HttpServletResponse, JobOwner)}
    • + *
    • {@link #doGetJobParam(UWSUrl, HttpServletRequest, HttpServletResponse, JobOwner)}
    • + *
    • {@link #doJobSummary(UWSUrl, HttpServletRequest, HttpServletResponse, JobOwner)}
    • + *
    • {@link #doListJob(UWSUrl, HttpServletRequest, HttpServletResponse, JobOwner)}
    • + *
    • {@link #doSetJobParam(UWSUrl, HttpServletRequest, HttpServletResponse, JobOwner)}
    • *
    *

    * They are all already implemented following their definition in the IVOA document. However, @@ -131,8 +148,8 @@ import uws.service.log.UWSLog; * So, they can be overridden as in any HTTP servlet. *

    * - * @author Grégory Mantelet (CDS) - * @version 06/2012 + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (04/2015) */ public abstract class UWSServlet extends HttpServlet implements UWS, UWSFactory { private static final long serialVersionUID = 1L; @@ -173,6 +190,10 @@ public abstract class UWSServlet extends HttpServlet implements UWS, UWSFactory /** Lets logging info/debug/warnings/errors about this UWS. */ protected UWSLog logger; + /** Lets extract all parameters from an HTTP request, whatever is its content-type. + * @since 4.1*/ + protected RequestParser requestParser; + /** Lets writing/formatting any exception/throwable in a HttpServletResponse. */ protected ServiceErrorWriter errorWriter; @@ -183,6 +204,8 @@ public abstract class UWSServlet extends HttpServlet implements UWS, UWSFactory @Override public final void init() throws ServletException{ + final String INIT_ERROR_MSG = "UWS initialization ERROR! Contact the administrator of the service to figure out the failure."; + // Set the general information about this UWS: name = getServletConfig().getInitParameter("name"); description = getServletConfig().getInitParameter("description"); @@ -190,15 +213,26 @@ public abstract class UWSServlet extends HttpServlet implements UWS, UWSFactory // Set the file manager to use: try{ fileManager = createFileManager(); - if (fileManager == null) - throw new ServletException("Missing file manager ! The function createFileManager() MUST return a valid instanceof UWSFileManager !"); + if (fileManager == null){ + logger.logUWS(LogLevel.FATAL, null, "INIT", "Missing file manager! The function createFileManager() MUST return a valid instanceof UWSFileManager!", null); + throw new ServletException(INIT_ERROR_MSG); + } }catch(UWSException ue){ - throw new ServletException("Error while setting the file manager.", ue); + logger.logUWS(LogLevel.FATAL, null, "INIT", "Can't create a file manager!", ue); + throw new ServletException(INIT_ERROR_MSG, ue); } // Set the logger: logger = new DefaultUWSLog(this); - errorWriter = new DefaultUWSErrorWriter(this); + errorWriter = new DefaultUWSErrorWriter(logger); + + // Set the request parser: + try{ + requestParser = createRequestParser(fileManager); + }catch(UWSException ue){ + logger.logUWS(LogLevel.FATAL, null, "INIT", "Can't create a request parser!", ue); + throw new ServletException(INIT_ERROR_MSG, ue); + } // Initialize the list of jobs: mapJobLists = new LinkedHashMap(); @@ -209,16 +243,52 @@ public abstract class UWSServlet extends HttpServlet implements UWS, UWSFactory addSerializer(new JSONSerializer()); try{ + // Initialize the service: initUWS(); - logger.uwsInitialized(this); + + // Log the successful initialization: + logger.logUWS(LogLevel.INFO, this, "INIT", "UWS successfully initialized.", null); + }catch(UWSException ue){ - logger.error("UWS initialization impossible !", ue); - throw new ServletException("Error while initializing UWS ! See the log for more details."); + logger.logUWS(LogLevel.FATAL, null, "INIT", "Can't execute the custom initialization of this UWS service (UWSServlet.initUWS())!", ue); + throw new ServletException(INIT_ERROR_MSG); } } public abstract void initUWS() throws UWSException; + @Override + public void destroy(){ + // Backup all jobs: + /* Jobs are backuped now so that running jobs are set back to the PENDING phase in the backup. + * Indeed, the "stopAll" operation of the ExecutionManager may fail and would set the phase to ERROR + * for the wrong reason. */ + if (backupManager != null){ + // save all jobs: + backupManager.setEnabled(true); + backupManager.saveAll(); + // stop the automatic backup, if there is one: + backupManager.setEnabled(false); + } + + // Stop all jobs and stop watching for the jobs' destruction: + for(JobList jl : mapJobLists.values()){ + jl.getExecutionManager().stopAll(); + jl.getDestructionManager().stop(); + } + + // Just in case that previous clean "stop"s did not work, try again an interruption for all running threads: + /* note: timers are not part of this ThreadGroup and so, they won't be affected by this function call. */ + JobThread.tg.interrupt(); + + // Log the service is stopped: + if (logger != null) + logger.logUWS(LogLevel.INFO, this, "STOP", "UWS Service \"" + getName() + "\" stopped!", null); + + // Default destroy function: + super.destroy(); + } + public UWSFileManager createFileManager() throws UWSException{ UWSFileManager fm = null; String rootPath = getServletConfig().getInitParameter("rootDirectory"); @@ -236,16 +306,46 @@ public abstract class UWSServlet extends HttpServlet implements UWS, UWSFactory return fileManager; } + @Override + public RequestParser getRequestParser(){ + return requestParser; + } + @Override public final void service(ServletRequest req, ServletResponse resp) throws ServletException, IOException{ super.service(req, resp); } + protected static String lastRequestID = null; + + protected synchronized String generateRequestID(final HttpServletRequest request){ + String id; + do{ + id = System.currentTimeMillis() + ""; + }while(lastRequestID != null && lastRequestID.startsWith(id)); + lastRequestID = id; + return id; + } + @Override protected final void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException{ String uwsAction = null; JobOwner user = null; + // Generate a unique ID for this request execution (for log purpose only): + final String reqID = generateRequestID(req); + req.setAttribute(UWS.REQ_ATTRIBUTE_ID, reqID); + + // Extract all parameters: + try{ + req.setAttribute(UWS.REQ_ATTRIBUTE_PARAMETERS, requestParser.parse(req)); + }catch(UWSException ue){ + logger.log(LogLevel.WARNING, "REQUEST_PARSER", "Can not extract the HTTP request parameters!", ue); + } + + // Log the reception of the request: + logger.logHttp(LogLevel.INFO, req, reqID, null, null); + try{ String method = req.getMethod(); @@ -258,7 +358,10 @@ public abstract class UWSServlet extends HttpServlet implements UWS, UWSFactory requestUrl.load(req); // Identify the user: - user = (userIdentifier == null) ? null : userIdentifier.extractUserId(requestUrl, req); + user = UWSToolBox.getUser(req, userIdentifier); + + // Set the character encoding: + resp.setCharacterEncoding(UWSToolBox.DEFAULT_CHAR_ENCODING); // METHOD GET: if (method.equals("GET")){ @@ -282,11 +385,8 @@ public abstract class UWSServlet extends HttpServlet implements UWS, UWSFactory uwsAction = UWSAction.GET_JOB_PARAM; doGetJobParam(requestUrl, req, resp, user); - }else{ - logger.httpRequest(req, user, null, 0, null, null); - super.service(req, resp); - return; - } + }else + throw new UWSException(UWSException.NOT_IMPLEMENTED, "Unknown UWS action!"); }// METHOD POST: else if (method.equals("POST")){ @@ -300,34 +400,40 @@ public abstract class UWSServlet extends HttpServlet implements UWS, UWSFactory uwsAction = UWSAction.ADD_JOB; doAddJob(requestUrl, req, resp, user); - }// SET JOB PARAMETER: - else if (requestUrl.hasJobList() && requestUrl.hasJob() && (!requestUrl.hasAttribute() || requestUrl.getAttributes().length == 1) && req.getParameterMap().size() > 0){ - uwsAction = UWSAction.SET_JOB_PARAM; - doSetJobParam(requestUrl, req, resp, user); + }// SET JOB's UWS STANDARD PARAMETER + else if (requestUrl.hasJobList() && requestUrl.hasJob() && requestUrl.getAttributes().length == 1 && requestUrl.getAttributes()[0].toLowerCase().matches(UWSParameters.UWS_RW_PARAMETERS_REGEXP) && UWSToolBox.hasParameter(requestUrl.getAttributes()[0], req, false)){ + uwsAction = UWSAction.SET_UWS_PARAMETER; + doSetUWSParameter(requestUrl, req, resp, user); }// DESTROY JOB: - else if (requestUrl.hasJobList() && requestUrl.hasJob() && req.getParameter(UWSJob.PARAM_ACTION) != null && req.getParameter(UWSJob.PARAM_ACTION).equalsIgnoreCase(UWSJob.ACTION_DELETE)){ + else if (requestUrl.hasJobList() && requestUrl.hasJob() && UWSToolBox.hasParameter(UWSJob.PARAM_ACTION, UWSJob.ACTION_DELETE, req, false)){ uwsAction = UWSAction.DESTROY_JOB; doDestroyJob(requestUrl, req, resp, user); - }else{ - logger.httpRequest(req, user, null, 0, null, null); - super.service(req, resp); - return; - } + }// SET JOB PARAMETER: + else if (requestUrl.hasJobList() && requestUrl.hasJob() && (!requestUrl.hasAttribute() || requestUrl.getAttributes().length == 1 && requestUrl.getAttributes()[0].equalsIgnoreCase(UWSJob.PARAM_PARAMETERS)) && UWSToolBox.getNbParameters(req) > 0){ + uwsAction = UWSAction.SET_JOB_PARAM; + doSetJobParam(requestUrl, req, resp, user); + + }else + throw new UWSException(UWSException.NOT_IMPLEMENTED, "Unknown UWS action!"); }// METHOD PUT: else if (method.equals("PUT")){ // SET JOB PARAMETER: - if (requestUrl.hasJobList() && requestUrl.hasJob() && req.getMethod().equalsIgnoreCase("put") && requestUrl.getAttributes().length >= 2 && requestUrl.getAttributes()[0].equalsIgnoreCase(UWSJob.PARAM_PARAMETERS) && req.getParameter(requestUrl.getAttributes()[1]) != null){ + if (requestUrl.hasJobList() && requestUrl.hasJob() && requestUrl.getAttributes().length >= 2 && requestUrl.getAttributes()[0].equalsIgnoreCase(UWSJob.PARAM_PARAMETERS)){ uwsAction = UWSAction.SET_JOB_PARAM; + if (!UWSToolBox.hasParameter(requestUrl.getAttributes()[1], req, false)) + throw new UWSException(UWSException.BAD_REQUEST, "Wrong parameter name in the PUT request! Expected: " + requestUrl.getAttributes()[1]); doSetJobParam(requestUrl, req, resp, user); - }else{ - logger.httpRequest(req, user, null, 0, null, null); - super.service(req, resp); - return; - } + }// SET JOB's UWS STANDARD PARAMETER + else if (requestUrl.hasJobList() && requestUrl.hasJob() && requestUrl.getAttributes().length == 1 && requestUrl.getAttributes()[0].toLowerCase().matches(UWSParameters.UWS_RW_PARAMETERS_REGEXP) && UWSToolBox.hasParameter(requestUrl.getAttributes()[0], req, false)){ + uwsAction = UWSAction.SET_UWS_PARAMETER; + doSetUWSParameter(requestUrl, req, resp, user); + + }else + throw new UWSException(UWSException.NOT_IMPLEMENTED, "Unknown UWS action!"); }// METHOD DELETE: else if (method.equals("DELETE")){ @@ -336,31 +442,65 @@ public abstract class UWSServlet extends HttpServlet implements UWS, UWSFactory uwsAction = UWSAction.DESTROY_JOB; doDestroyJob(requestUrl, req, resp, user); - }else{ - logger.httpRequest(req, user, null, 0, null, null); - super.service(req, resp); - return; - } + }else + throw new UWSException(UWSException.NOT_IMPLEMENTED, "Unknown UWS action!"); - }// ELSE => DEFAULT BEHAVIOR: - else{ - logger.httpRequest(req, user, null, 0, null, null); - super.service(req, resp); - return; - } + }// ELSE ERROR: + else + throw new UWSException(UWSException.NOT_IMPLEMENTED, "Unknown UWS action!"); resp.flushBuffer(); - logger.httpRequest(req, user, uwsAction, HttpServletResponse.SC_OK, "[OK]", null); + + // Log the successful execution of the action: + logger.logHttp(LogLevel.INFO, resp, reqID, user, "UWS action \"" + uwsAction + "\" successfully executed.", null); + + }catch(IOException ioe){ + /* + * Any IOException thrown while writing the HTTP response is generally caused by a client abortion (intentional or timeout) + * or by a connection closed with the client for another reason. + * Consequently, a such error should not be considered as a real error from the server or the library: the request is + * canceled, and so the response is not expected. It is anyway not possible any more to send it (header and/or body) totally + * or partially. + * Nothing can solve this error. So the "error" is just reported as a simple information and theoretically the action + * executed when this error has been thrown is already stopped. + */ + logger.logHttp(LogLevel.INFO, resp, reqID, user, "HTTP request aborted or connection with the client closed => the UWS action \"" + uwsAction + "\" has stopped and the body of the HTTP response can not have been partially or completely written!", null); }catch(UWSException ex){ - sendError(ex, req, user, uwsAction, resp); - }catch(ClientAbortException cae){ - logger.info("Request aborted by the user !"); - logger.httpRequest(req, user, uwsAction, HttpServletResponse.SC_OK, "[Client abort => ClientAbortException]", null); + /* + * Any known/"expected" UWS exception is logged but also returned to the HTTP client in an error document. + * Since the error is known, it is supposed to have already been logged with a full stack trace. Thus, there + * is no need to log again its stack trace...just its message is logged. + * Besides, this error may also be just a redirection and not a true error. In such case, the error message + * is not logged. + */ + sendError(ex, req, reqID, user, uwsAction, resp); + + }catch(IllegalStateException ise){ + /* + * Any IllegalStateException that reaches this point, is supposed coming from a HttpServletResponse operation which + * has to reset the response buffer (e.g. resetBuffer(), sendRedirect(), sendError()). + * If this exception happens, the library tried to rewrite the HTTP response body with a message or a result, + * while this body has already been partially sent to the client. It is then no longer possible to change its content. + * Consequently, the error is logged as FATAL and a message will be appended at the end of the already submitted response + * to alert the HTTP client that an error occurs and the response should not be considered as complete and reliable. + */ + // Write the error in the response and return the appropriate HTTP status code: + errorWriter.writeError(ise, resp, req, reqID, user, uwsAction); + // Log the error: + getLogger().logHttp(LogLevel.FATAL, resp, reqID, user, "HTTP response already partially committed => the UWS action \"" + uwsAction + "\" has stopped and the body of the HTTP response can not have been partially or completely written!", (ise.getCause() != null) ? ise.getCause() : ise); + }catch(Throwable t){ - logger.error("Request unexpectedly aborted !", t); - logger.httpRequest(req, user, uwsAction, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, t.getMessage(), t); - resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, t.getMessage()); + /* + * Any other error is considered as unexpected if it reaches this point. Consequently, it has not yet been logged. + * So its stack trace will be fully logged, and an appropriate message will be returned to the HTTP client. The + * returned document should contain not too technical information which would be useless for the user. + */ + sendError(t, req, reqID, user, uwsAction, resp); + + }finally{ + // Free resources about uploaded files ; only unused files will be deleted: + UWSToolBox.deleteUploads(req); } } @@ -370,13 +510,23 @@ public abstract class UWSServlet extends HttpServlet implements UWS, UWSFactory protected void writeHomePage(UWSUrl requestUrl, HttpServletRequest req, HttpServletResponse resp, JobOwner user) throws UWSException, ServletException, IOException{ UWSSerializer serializer = getSerializer(req.getHeader("Accept")); resp.setContentType(serializer.getMimeType()); - String serialization = serializer.getUWS(this); + resp.setCharacterEncoding(UWSToolBox.DEFAULT_CHAR_ENCODING); + String serialization; + try{ + serialization = serializer.getUWS(this); + }catch(Exception e){ + if (!(e instanceof UWSException)){ + getLogger().logUWS(LogLevel.ERROR, requestUrl, "SERIALIZE", "Can't display the default home page, due to a serialization error!", e); + throw new UWSException(UWSException.NO_CONTENT, e, "No home page available for this UWS service!"); + }else + throw (UWSException)e; + } if (serialization != null){ PrintWriter output = resp.getWriter(); output.print(serialization); output.flush(); }else - throw UWSExceptionFactory.incorrectSerialization(serialization, "the UWS " + getName()); + throw new UWSException(UWSException.NO_CONTENT, "No home page available for this UWS service."); } protected void doListJob(UWSUrl requestUrl, HttpServletRequest req, HttpServletResponse resp, JobOwner user) throws UWSException, ServletException, IOException{ @@ -386,7 +536,16 @@ public abstract class UWSServlet extends HttpServlet implements UWS, UWSFactory // Write the jobs list: UWSSerializer serializer = getSerializer(req.getHeader("Accept")); resp.setContentType(serializer.getMimeType()); - jobsList.serialize(resp.getOutputStream(), serializer, user); + resp.setCharacterEncoding(UWSToolBox.DEFAULT_CHAR_ENCODING); + try{ + jobsList.serialize(resp.getOutputStream(), serializer, user); + }catch(Exception e){ + if (!(e instanceof UWSException)){ + getLogger().logUWS(LogLevel.ERROR, requestUrl, "SERIALIZE", "Can not serialize the jobs list \"" + jobsList.getName() + "\"!", e); + throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, e, "Can not format properly the jobs list \"" + jobsList.getName() + "\"!"); + }else + throw (UWSException)e; + } } protected void doAddJob(UWSUrl requestUrl, HttpServletRequest req, HttpServletResponse resp, JobOwner user) throws UWSException, ServletException, IOException{ @@ -395,25 +554,48 @@ public abstract class UWSServlet extends HttpServlet implements UWS, UWSFactory // Forbids the job creation if the user has not the WRITE permission for the specified jobs list: if (user != null && !user.hasWritePermission(jobsList)) - throw UWSExceptionFactory.writePermissionDenied(user, true, jobsList.getName()); + throw new UWSException(UWSException.PERMISSION_DENIED, UWSExceptionFactory.writePermissionDenied(user, true, jobsList.getName())); // Create the job: UWSJob newJob = createJob(req, user); // Add it to the jobs list: if (jobsList.addNewJob(newJob) != null){ + // Start the job if the phase parameter was provided with the "RUN" value: + if (UWSToolBox.hasParameter(UWSJob.PARAM_PHASE, UWSJob.PHASE_RUN, req, false)) + newJob.start(); // Make a redirection to the added job: redirect(requestUrl.jobSummary(jobsList.getName(), newJob.getJobId()).getRequestURL(), req, user, UWSAction.ADD_JOB, resp); }else throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, "Unable to add the new job " + newJob.getJobId() + " to the jobs list " + jobsList.getName() + ". (ID already used = " + (jobsList.getJob(newJob.getJobId()) != null) + ")"); } + protected void doSetUWSParameter(UWSUrl requestUrl, HttpServletRequest req, HttpServletResponse resp, JobOwner user) throws UWSException, ServletException, IOException{ + // Get the job: + UWSJob job = getJob(requestUrl); + + // Forbids the action if the user has not the WRITE permission for the specified job: + if (user != null && !user.hasWritePermission(job)) + throw new UWSException(UWSException.PERMISSION_DENIED, UWSExceptionFactory.writePermissionDenied(user, true, job.getJobId())); + + String name = requestUrl.getAttributes()[0]; + job.addOrUpdateParameter(name, UWSToolBox.getParameter(name, req, false), user); + + // Make a redirection to the job: + redirect(requestUrl.jobSummary(requestUrl.getJobListName(), job.getJobId()).getRequestURL(), req, user, getName(), resp); + } + protected void doDestroyJob(UWSUrl requestUrl, HttpServletRequest req, HttpServletResponse resp, JobOwner user) throws UWSException, ServletException, IOException{ // Get the jobs list: JobList jobsList = getJobList(requestUrl.getJobListName()); // Destroy the job: - jobsList.destroyJob(requestUrl.getJobId(), user); + try{ + jobsList.destroyJob(requestUrl.getJobId(), user); + }catch(UWSException ue){ + getLogger().logUWS(LogLevel.ERROR, requestUrl, "DESTROY_JOB", "Can not destroy the job \"" + requestUrl.getJobId() + "\"!", ue); + throw ue; + } // Make a redirection to the jobs list: redirect(requestUrl.listJobs(jobsList.getName()).getRequestURL(), req, user, UWSAction.DESTROY_JOB, resp); @@ -426,7 +608,16 @@ public abstract class UWSServlet extends HttpServlet implements UWS, UWSFactory // Write the job summary: UWSSerializer serializer = getSerializer(req.getHeader("Accept")); resp.setContentType(serializer.getMimeType()); - job.serialize(resp.getOutputStream(), serializer, user); + resp.setCharacterEncoding(UWSToolBox.DEFAULT_CHAR_ENCODING); + try{ + job.serialize(resp.getOutputStream(), serializer, user); + }catch(Exception e){ + if (!(e instanceof UWSException)){ + getLogger().logUWS(LogLevel.ERROR, requestUrl, "SERIALIZE", "Can not serialize the job \"" + job.getJobId() + "\"!", e); + throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, e, "Can not format properly the job \"" + job.getJobId() + "\"!"); + }else + throw (UWSException)e; + } } protected void doGetJobParam(UWSUrl requestUrl, HttpServletRequest req, HttpServletResponse resp, JobOwner user) throws UWSException, ServletException, IOException{ @@ -439,7 +630,7 @@ public abstract class UWSServlet extends HttpServlet implements UWS, UWSFactory if (attributes[0].equalsIgnoreCase(UWSJob.PARAM_RESULTS) && attributes.length > 1){ Result result = job.getResult(attributes[1]); if (result == null) - throw UWSExceptionFactory.incorrectJobResult(job.getJobId(), attributes[1]); + throw new UWSException(UWSException.NOT_FOUND, "No result identified with \"" + attributes[1] + "\" in the job \"" + job.getJobId() + "\"!"); else if (result.getHref() != null && !result.getHref().trim().isEmpty() && !result.getHref().equalsIgnoreCase(req.getRequestURL().toString())) redirect(result.getHref(), req, user, UWSAction.GET_JOB_PARAM, resp); else{ @@ -448,6 +639,7 @@ public abstract class UWSServlet extends HttpServlet implements UWS, UWSFactory input = getFileManager().getResultInput(result, job); UWSToolBox.write(input, result.getMimeType(), result.getSize(), resp); }catch(IOException ioe){ + getLogger().logUWS(LogLevel.ERROR, result, "GET_RESULT", "Can not read the content of the result \"" + result.getId() + "\" of the job \"" + job.getJobId() + "\"!", ioe); throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, ioe, "Can not read the content of the result " + result.getId() + " (job ID: " + job.getJobId() + ")."); }finally{ if (input != null) @@ -458,29 +650,63 @@ public abstract class UWSServlet extends HttpServlet implements UWS, UWSFactory else if (attributes[0].equalsIgnoreCase(UWSJob.PARAM_ERROR_SUMMARY) && attributes.length > 1 && attributes[1].equalsIgnoreCase("details")){ ErrorSummary error = job.getErrorSummary(); if (error == null) - throw UWSExceptionFactory.noErrorSummary(job.getJobId()); + throw new UWSException(UWSException.NOT_FOUND, "No error summary for the job \"" + job.getJobId() + "\"!"); else{ InputStream input = null; try{ input = getFileManager().getErrorInput(error, job); - UWSToolBox.write(input, "text/plain", getFileManager().getErrorSize(error, job), resp); + UWSToolBox.write(input, errorWriter.getErrorDetailsMIMEType(), getFileManager().getErrorSize(error, job), resp); }catch(IOException ioe){ + getLogger().logUWS(LogLevel.ERROR, error, "GET_ERROR", "Can not read the details of the error summary of the job \"" + job.getJobId() + "\"!", ioe); throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, ioe, "Can not read the error details (job ID: " + job.getJobId() + ")."); }finally{ if (input != null) input.close(); } } + }// REFERENCE FILE: Display the content of the uploaded file or redirect to the URL (if it is a URL): + else if (attributes[0].equalsIgnoreCase(UWSJob.PARAM_PARAMETERS) && attributes.length > 1 && job.getAdditionalParameterValue(attributes[1]) != null && job.getAdditionalParameterValue(attributes[1]) instanceof UploadFile){ + UploadFile upl = (UploadFile)job.getAdditionalParameterValue(attributes[1]); + if (upl.getLocation().matches("^http(s)?://")) + redirect(upl.getLocation(), req, user, getName(), resp); + else{ + InputStream input = null; + try{ + input = getFileManager().getUploadInput(upl); + UWSToolBox.write(input, upl.mimeType, upl.length, resp); + }catch(IOException ioe){ + getLogger().logUWS(LogLevel.ERROR, upl, "GET_PARAMETER", "Can not read the content of the uploaded file \"" + upl.paramName + "\" of the job \"" + job.getJobId() + "\"!", ioe); + throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, ioe, "Can not read the content of the uploaded file " + upl.paramName + " (job ID: " + job.getJobId() + ")."); + }finally{ + if (input != null) + input.close(); + } + } }// DEFAULT CASE: Display the serialization of the selected UWS object: else{ // Write the value/content of the selected attribute: UWSSerializer serializer = getSerializer(req.getHeader("Accept")); String uwsField = attributes[0]; - if (uwsField == null || uwsField.trim().isEmpty() || (attributes.length <= 1 && (uwsField.equalsIgnoreCase(UWSJob.PARAM_ERROR_SUMMARY) || uwsField.equalsIgnoreCase(UWSJob.PARAM_RESULTS) || uwsField.equalsIgnoreCase(UWSJob.PARAM_PARAMETERS)))) + boolean jobSerialization = false; + // Set the content type: + if (uwsField == null || uwsField.trim().isEmpty() || (attributes.length <= 1 && (uwsField.equalsIgnoreCase(UWSJob.PARAM_ERROR_SUMMARY) || uwsField.equalsIgnoreCase(UWSJob.PARAM_RESULTS) || uwsField.equalsIgnoreCase(UWSJob.PARAM_PARAMETERS)))){ resp.setContentType(serializer.getMimeType()); - else + jobSerialization = true; + }else resp.setContentType("text/plain"); - job.serialize(resp.getOutputStream(), attributes, serializer); + // Set the character encoding: + resp.setCharacterEncoding(UWSToolBox.DEFAULT_CHAR_ENCODING); + // Serialize the selected attribute: + try{ + job.serialize(resp.getOutputStream(), attributes, serializer); + }catch(Exception e){ + if (!(e instanceof UWSException)){ + String errorMsgPart = (jobSerialization ? "the job \"" + job.getJobId() + "\"" : "the parameter " + uwsField + " of the job \"" + job.getJobId() + "\""); + getLogger().logUWS(LogLevel.ERROR, requestUrl, "SERIALIZE", "Can not serialize " + errorMsgPart + "!", e); + throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, e, "Can not format properly " + errorMsgPart + "!"); + }else + throw (UWSException)e; + } } } @@ -502,19 +728,19 @@ public abstract class UWSServlet extends HttpServlet implements UWS, UWSFactory } public UWSJob getJob(UWSUrl requestUrl, JobOwner user) throws UWSException{ - // Get the jobs list: - JobList jobsList = getJobList(requestUrl.getJobListName()); - // Get the job ID: String jobId = requestUrl.getJobId(); - - if (jobId == null) - throw UWSExceptionFactory.missingJobID(); - - // Get the job: - UWSJob job = jobsList.getJob(jobId, user); - if (job == null) - throw UWSExceptionFactory.incorrectJobID(jobsList.getName(), jobId); + UWSJob job = null; + + if (jobId != null){ + // Get the jobs list: + JobList jobsList = getJobList(requestUrl.getJobListName()); + // Get the job: + job = jobsList.getJob(jobId, user); + if (job == null) + throw new UWSException(UWSException.NOT_FOUND, "Incorrect job ID! The job \"" + jobId + "\" does not exist in the jobs list \"" + jobsList.getName() + "\"."); + }else + throw new UWSException(UWSException.BAD_REQUEST, "Missing job ID!"); return job; } @@ -547,6 +773,11 @@ public abstract class UWSServlet extends HttpServlet implements UWS, UWSFactory return new UWSParameters(req, expectedAdditionalParams, inputParamControllers); } + @Override + public RequestParser createRequestParser(final UWSFileManager fileManager) throws UWSException{ + return new UWSRequestParser(fileManager); + } + /* ****************************** */ /* REDIRECTION & ERROR MANAGEMENT */ /* ****************************** */ @@ -563,9 +794,9 @@ public abstract class UWSServlet extends HttpServlet implements UWS, UWSFactory public void redirect(String url, HttpServletRequest request, JobOwner user, String uwsAction, HttpServletResponse response) throws ServletException, IOException{ response.setStatus(HttpServletResponse.SC_SEE_OTHER); response.setContentType(request.getContentType()); + response.setCharacterEncoding(UWSToolBox.DEFAULT_CHAR_ENCODING); response.setHeader("Location", url); response.flushBuffer(); - logger.httpRequest(request, user, uwsAction, HttpServletResponse.SC_SEE_OTHER, "[Redirection toward " + url + "]", null); } /** @@ -577,20 +808,58 @@ public abstract class UWSServlet extends HttpServlet implements UWS, UWSFactory * * @param error The error to send/display. * @param request The request which has caused the given error (not used by default). + * @param reqID ID of the request. + * @param user The user which executes the given request. + * @param uwsAction The UWS action corresponding to the given request. * @param response The response in which the error must be published. * * @throws IOException If there is an error when calling {@link #redirect(String, HttpServletRequest, JobOwner, String, HttpServletResponse)} or {@link HttpServletResponse#sendError(int, String)}. * @throws UWSException If there is an error when calling {@link #redirect(String, HttpServletRequest, JobOwner, String, HttpServletResponse)}. * * @see #redirect(String, HttpServletRequest, JobOwner, String, HttpServletResponse) - * @see #writeError(Throwable, HttpServletResponse, HttpServletRequest, JobOwner, String) + * @see #sendError(Throwable, HttpServletRequest, String, JobOwner, String, HttpServletResponse) */ - public void sendError(UWSException error, HttpServletRequest request, JobOwner user, String uwsAction, HttpServletResponse response) throws ServletException, IOException{ - if (error.getHttpErrorCode() == UWSException.SEE_OTHER) - redirect(error.getMessage(), request, user, uwsAction, response); - else{ - errorWriter.writeError(error, response, request, user, uwsAction); - } + public final void sendError(UWSException error, HttpServletRequest request, String reqID, JobOwner user, String uwsAction, HttpServletResponse response) throws ServletException{ + if (error.getHttpErrorCode() == UWSException.SEE_OTHER){ + // Log the redirection, if any: + logger.logHttp(LogLevel.INFO, response, reqID, user, "HTTP " + UWSException.SEE_OTHER + " [Redirection toward " + error.getMessage() + "] - Action \"" + uwsAction + "\" successfully executed.", null); + // Apply the redirection: + try{ + redirect(error.getMessage(), request, user, uwsAction, response); + }catch(IOException ioe){ + logger.logHttp(LogLevel.FATAL, request, reqID, "Can not redirect the response toward " + error.getMessage(), error); + throw new ServletException("Can not redirect the response! You should notify the administrator of the service (FATAL-" + reqID + "). However, while waiting a correction of this problem, you can manually go toward " + error.getMessage() + "."); + } + }else + sendError((Exception)error, request, reqID, user, uwsAction, response); + } + + /** + *

    + * Fills the response with the given error. + * The stack trace of the error is printed on the standard output and then the function + * {@link HttpServletResponse#sendError(int, String)} is called with the HTTP status code is {@link UWSException#INTERNAL_SERVER_ERROR} + * and the message of the given exception. + *

    + * + * + * @param error The error to send/display. + * @param request The request which has caused the given error (not used by default). + * @param reqID ID of the request. + * @param user The user which executes the given request. + * @param uwsAction The UWS action corresponding to the given request. + * @param response The response in which the error must be published. + * + * @throws IOException If there is an error when calling {@link HttpServletResponse#sendError(int, String)}. + * @throws UWSException + * + * @see ServiceErrorWriter#writeError(Throwable, HttpServletResponse, HttpServletRequest, String, JobOwner, String) + */ + public final void sendError(Throwable error, HttpServletRequest request, String reqID, JobOwner user, String uwsAction, HttpServletResponse response) throws ServletException{ + // Write the error in the response and return the appropriate HTTP status code: + errorWriter.writeError(error, response, request, reqID, user, uwsAction); + // Log the error: + logger.logHttp(LogLevel.ERROR, response, reqID, user, "Can not complete the UWS action \"" + uwsAction + "\", because: " + error.getMessage(), error); } /* ************** */ @@ -798,6 +1067,7 @@ public abstract class UWSServlet extends HttpServlet implements UWS, UWSFactory /** * @return The name. */ + @Override public final String getName(){ return name; } @@ -805,6 +1075,7 @@ public abstract class UWSServlet extends HttpServlet implements UWS, UWSFactory /** * @return The description. */ + @Override public final String getDescription(){ return description; } @@ -812,25 +1083,29 @@ public abstract class UWSServlet extends HttpServlet implements UWS, UWSFactory /* ******************** */ /* JOBS LIST MANAGEMENT */ /* ******************** */ + @Override public final Iterator iterator(){ return mapJobLists.values().iterator(); } + @Override public JobList getJobList(String name) throws UWSException{ if (name != null) name = name.trim(); if (name == null || name.length() == 0) - throw UWSExceptionFactory.missingJobListName(); + throw new UWSException(UWSException.BAD_REQUEST, "Missing job list name!"); else if (!mapJobLists.containsKey(name)) - throw UWSExceptionFactory.incorrectJobListName(name); + throw new UWSException(UWSException.NOT_FOUND, "Incorrect job list name ! The jobs list \"" + name + "\" does not exist."); else return mapJobLists.get(name); } + @Override public final int getNbJobList(){ return mapJobLists.size(); } + @Override public final boolean addJobList(JobList jl){ if (jl == null) return false; @@ -841,44 +1116,14 @@ public abstract class UWSServlet extends HttpServlet implements UWS, UWSFactory jl.setUWS(this); mapJobLists.put(jl.getName(), jl); }catch(IllegalStateException ise){ - logger.error("The jobs list \"" + jl.getName() + "\" can not be added into the UWS " + getName() + " !", ise); + logger.logUWS(LogLevel.ERROR, jl, "ADD_JOB_LIST", "The jobs list \"" + jl.getName() + "\" can not be added into the UWS " + getName() + ": it may already be associated with one!", ise); return false; } return true; } - /*public final JobList removeJobList(String name){ - JobList jl = mapJobLists.get(name); - if (jl != null){ - if (removeJobList(jl)) - return jl; - } - return null; - }*/ - - /* - * Removes the given jobs list from this UWS. - * - * @param jl The jobs list to remove. - * - * @return true if the jobs list has been successfully removed, false otherwise. - * - * @see JobList#removeAll() - * @see JobList#setUWS(AbstractUWS) - * - public boolean removeJobList(JobList jl){ - if (jl == null) - return false; - - jl = mapJobLists.remove(jl.getName()); - if (jl != null){ - jl.removeAll(); - jl.setUWS(null); - } - return jl != null; - }*/ - + @Override public final boolean destroyJobList(String name){ return destroyJobList(mapJobLists.get(name)); } @@ -891,7 +1136,7 @@ public abstract class UWSServlet extends HttpServlet implements UWS, UWSFactory * @return true if the given jobs list has been destroyed, false otherwise. * * @see JobList#clear() - * @see JobList#setUWS(AbstractUWS) + * @see JobList#setUWS(UWS) */ public boolean destroyJobList(JobList jl){ if (jl == null) @@ -903,7 +1148,7 @@ public abstract class UWSServlet extends HttpServlet implements UWS, UWSFactory jl.clear(); jl.setUWS(null); }catch(IllegalStateException ise){ - getLogger().warning("Impossible to erase completely the association between the jobs list \"" + jl.getName() + "\" and the UWS \"" + getName() + "\", because: " + ise.getMessage()); + logger.logUWS(LogLevel.WARNING, jl, "DESTROY_JOB_LIST", "Impossible to erase completely the association between the jobs list \"" + jl.getName() + "\" and the UWS \"" + getName() + "\"!", ise); } } return jl != null; @@ -946,6 +1191,7 @@ public abstract class UWSServlet extends HttpServlet implements UWS, UWSFactory * @see AcceptHeader#AcceptHeader(String) * @see AcceptHeader#getOrderedMimeTypes() */ + @Override public final UWSSerializer getSerializer(String mimeTypes) throws UWSException{ UWSSerializer choosenSerializer = null; @@ -963,7 +1209,7 @@ public abstract class UWSServlet extends HttpServlet implements UWS, UWSFactory if (choosenSerializer == null){ choosenSerializer = serializers.get(defaultSerializer); if (choosenSerializer == null) - throw UWSExceptionFactory.missingSerializer(mimeTypes + " (given MIME types) and " + defaultSerializer + " (default serializer MIME type)"); + throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, "Missing UWS serializer for the MIME types: " + mimeTypes + " (given MIME types) and " + defaultSerializer + " (default serializer MIME type)" + "!"); } return choosenSerializer; @@ -1017,6 +1263,7 @@ public abstract class UWSServlet extends HttpServlet implements UWS, UWSFactory * * @return The used UserIdentifier (MAY BE NULL). */ + @Override public final UserIdentifier getUserIdentifier(){ return userIdentifier; } @@ -1038,6 +1285,7 @@ public abstract class UWSServlet extends HttpServlet implements UWS, UWSFactory * * @return Its UWS URL interpreter. */ + @Override public final UWSUrl getUrlInterpreter(){ return urlInterpreter; } @@ -1045,7 +1293,7 @@ public abstract class UWSServlet extends HttpServlet implements UWS, UWSFactory /** * Sets the UWS URL interpreter to use in this UWS. * - * @param urlInterpreter Its new UWS URL interpreter (may be null. In this case, it will be created from the next request ; see {@link #executeRequest(HttpServletRequest, HttpServletResponse)}). + * @param urlInterpreter Its new UWS URL interpreter (may be null. In this case, it will be created from the next request ; see {@link #service(HttpServletRequest, HttpServletResponse)}). */ public final void setUrlInterpreter(UWSUrl urlInterpreter){ this.urlInterpreter = urlInterpreter; @@ -1061,6 +1309,7 @@ public abstract class UWSServlet extends HttpServlet implements UWS, UWSFactory * * @return Its backup manager. */ + @Override public final UWSBackupManager getBackupManager(){ return backupManager; } diff --git a/src/uws/service/UWSUrl.java b/src/uws/service/UWSUrl.java index 95073f7..5b0cea9 100644 --- a/src/uws/service/UWSUrl.java +++ b/src/uws/service/UWSUrl.java @@ -16,28 +16,25 @@ package uws.service; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.io.Serializable; - import java.net.MalformedURLException; import java.net.URL; - import java.util.Map; import javax.servlet.http.HttpServletRequest; -import uws.UWSException; import uws.UWSToolBox; - import uws.job.UWSJob; /** * This class helps managing with UWS URLs and URIs. * - * @author Grégory Mantelet (CDS) - * @version 05/2012 + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (09/2014) */ public class UWSUrl implements Serializable { private static final long serialVersionUID = 1L; @@ -91,16 +88,16 @@ public class UWSUrl implements Serializable { * * @param baseURI The baseURI to consider in all URL or request parsing. * - * @throws UWSException If the given baseURI is null or is an empty string. + * @throws NullPointerException If the given baseURI is null or is an empty string. */ - public UWSUrl(String baseURI) throws UWSException{ + public UWSUrl(String baseURI) throws NullPointerException{ if (baseURI == null) - throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, "The given base UWS URI is NULL !"); + throw new NullPointerException("The given base UWS URI is NULL!"); this.baseURI = normalizeURI(baseURI); if (baseURI.length() == 0) - throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, "The given base UWS URI is empty !"); + throw new NullPointerException("The given base UWS URI is empty!"); } /** @@ -108,19 +105,20 @@ public class UWSUrl implements Serializable { * * @param request The request to parse to get the baseURI. * - * @throws UWSException If the given request is null or if the extracted baseURI is null or is an empty string. + * @throws NullPointerException If the given request is null or if the extracted baseURI is null or is an empty string. * * @see #extractBaseURI(HttpServletRequest) */ - public UWSUrl(HttpServletRequest request) throws UWSException{ + public UWSUrl(HttpServletRequest request) throws NullPointerException{ + // Extract the base URI: String uri = extractBaseURI(request); if (uri == null) - throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, "The extracted base UWS URI is NULL !"); - - baseURI = normalizeURI(uri); + throw new NullPointerException("The extracted base UWS URI is NULL!"); + else + baseURI = normalizeURI(uri); - if (baseURI.length() == 0) - throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, "The extracted base UWS URI is empty !"); + // Load the rest of the request: + load(request); } /** @@ -509,9 +507,9 @@ public class UWSUrl implements Serializable { public final void setUwsURI(String uwsURI){ if (uwsURI == null || uwsURI.trim().length() == 0) this.uwsURI = null; - else{ + else this.uwsURI = uwsURI.trim(); - } + loadUwsURI(); updateRequestURL(); } diff --git a/src/uws/service/UserIdentifier.java b/src/uws/service/UserIdentifier.java index 5ec66dc..af08b47 100644 --- a/src/uws/service/UserIdentifier.java +++ b/src/uws/service/UserIdentifier.java @@ -22,15 +22,13 @@ package uws.service; import java.io.Serializable; import java.util.Map; -import uws.job.user.JobOwner; - -import uws.service.UWSUrl; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import uws.UWSException; +import uws.job.user.JobOwner; import uws.service.actions.UWSAction; import uws.service.backup.DefaultUWSBackupManager; -import uws.UWSException; - -import javax.servlet.http.HttpServletRequest; /** *

    Lets defining how identifying a user thanks to a HTTP request.

    @@ -67,7 +65,7 @@ public interface UserIdentifier extends Serializable { * * @param id ID of the user. * @param pseudo Pseudo of the user (may be NULL). - * @param otherdata Other data about the user (may be NULL or empty). + * @param otherData Other data about the user (may be NULL or empty). * * @return The corresponding user. * diff --git a/src/uws/service/actions/AddJob.java b/src/uws/service/actions/AddJob.java index a726501..3d00f3d 100644 --- a/src/uws/service/actions/AddJob.java +++ b/src/uws/service/actions/AddJob.java @@ -16,7 +16,8 @@ package uws.service.actions; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.io.IOException; @@ -26,14 +27,12 @@ import javax.servlet.http.HttpServletResponse; import uws.UWSException; import uws.UWSExceptionFactory; - import uws.job.JobList; import uws.job.UWSJob; - import uws.job.user.JobOwner; - import uws.service.UWSService; import uws.service.UWSUrl; +import uws.service.log.UWSLog.LogLevel; /** *

    The "Add Job" action of a UWS.

    @@ -43,8 +42,8 @@ import uws.service.UWSUrl; *

    This action creates a new job and adds it to the specified jobs list. * The response of this action is a redirection to the new job resource (that is to say: a redirection to the job summary of the new job).

    * - * @author Grégory Mantelet (CDS) - * @version 05/2012 + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (04/2015) */ public class AddJob extends UWSAction { private static final long serialVersionUID = 1L; @@ -74,7 +73,7 @@ public class AddJob extends UWSAction { *
  • the UWS URL does not make a reference to a job (so: no job ID),
  • *
  • the HTTP method is HTTP-POST.
  • * - * @see uws.service.actions.UWSAction#match(uws.service.UWSUrl, java.lang.String, javax.servlet.http.HttpServletRequest) + * @see uws.service.actions.UWSAction#match(UWSUrl, JobOwner, HttpServletRequest) */ @Override public boolean match(UWSUrl urlInterpreter, JobOwner user, HttpServletRequest request) throws UWSException{ @@ -87,11 +86,10 @@ public class AddJob extends UWSAction { * * @see #getJobsList(UWSUrl) * @see uws.service.UWSFactory#createJob(HttpServletRequest, JobOwner) - * @see UWSService#setExecutionManager(uws.job.manager.ExecutionManager) * @see JobList#addNewJob(UWSJob) * @see UWSService#redirect(String, HttpServletRequest, JobOwner, String, HttpServletResponse) * - * @see uws.service.actions.UWSAction#apply(uws.service.UWSUrl, java.lang.String, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) + * @see uws.service.actions.UWSAction#apply(UWSUrl, JobOwner, HttpServletRequest, HttpServletResponse) */ @Override public boolean apply(UWSUrl urlInterpreter, JobOwner user, HttpServletRequest request, HttpServletResponse response) throws UWSException, IOException{ @@ -100,10 +98,16 @@ public class AddJob extends UWSAction { // Forbids the job creation if the user has not the WRITE permission for the specified jobs list: if (user != null && !user.hasWritePermission(jobsList)) - throw UWSExceptionFactory.writePermissionDenied(user, true, jobsList.getName()); + throw new UWSException(UWSException.PERMISSION_DENIED, UWSExceptionFactory.writePermissionDenied(user, true, jobsList.getName())); // Create the job: - UWSJob newJob = uws.getFactory().createJob(request, user); + UWSJob newJob; + try{ + newJob = uws.getFactory().createJob(request, user); + }catch(UWSException ue){ + getLogger().logUWS(LogLevel.ERROR, urlInterpreter, "ADD_JOB", "Can not create a new job!", ue); + throw ue; + } // Add it to the jobs list: if (jobsList.addNewJob(newJob) != null){ @@ -113,7 +117,7 @@ public class AddJob extends UWSAction { return true; }else - throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, "Unable to add the new job to the jobs list. (ID of the new job = \"" + newJob.getJobId() + "\" ; ID already used = " + (jobsList.getJob(newJob.getJobId()) != null) + ")"); + throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, "Unable to add the new job to the jobs list for an unknown reason. (ID of the new job = \"" + newJob.getJobId() + "\" ; ID already used = " + (jobsList.getJob(newJob.getJobId()) != null) + ")"); } } diff --git a/src/uws/service/actions/DestroyJob.java b/src/uws/service/actions/DestroyJob.java index d6e5d33..f2629e1 100644 --- a/src/uws/service/actions/DestroyJob.java +++ b/src/uws/service/actions/DestroyJob.java @@ -16,7 +16,8 @@ package uws.service.actions; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.io.IOException; @@ -25,14 +26,13 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import uws.UWSException; - +import uws.UWSToolBox; import uws.job.JobList; import uws.job.UWSJob; - import uws.job.user.JobOwner; - import uws.service.UWSService; import uws.service.UWSUrl; +import uws.service.log.UWSLog.LogLevel; /** *

    The "Destroy Job" action of a UWS.

    @@ -42,8 +42,8 @@ import uws.service.UWSUrl; *

    This action destroys the job specified in the UWS URL. * The response of this action is a redirection to the jobs list.

    * - * @author Grégory Mantelet (CDS) - * @version 05/2012 + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (04/2015) */ public class DestroyJob extends UWSAction { private static final long serialVersionUID = 1L; @@ -75,11 +75,11 @@ public class DestroyJob extends UWSAction { *
  • ...or the HTTP method is HTTP-POST and there is the parameter {@link UWSJob#PARAM_ACTION PARAM_ACTION} (=ACTION) with the value {@link UWSJob#ACTION_DELETE ACTION_DELETE} (=DELETE).
  • * * - * @see uws.service.actions.UWSAction#match(uws.service.UWSUrl, java.lang.String, javax.servlet.http.HttpServletRequest) + * @see uws.service.actions.UWSAction#match(UWSUrl, JobOwner, HttpServletRequest) */ @Override public boolean match(UWSUrl urlInterpreter, JobOwner user, HttpServletRequest request) throws UWSException{ - return (urlInterpreter.hasJobList() && urlInterpreter.hasJob() && (request.getMethod().equalsIgnoreCase("delete") || (request.getMethod().equalsIgnoreCase("post") && request.getParameter(UWSJob.PARAM_ACTION) != null && request.getParameter(UWSJob.PARAM_ACTION).equalsIgnoreCase(UWSJob.ACTION_DELETE)))); + return urlInterpreter.hasJobList() && urlInterpreter.hasJob() && (request.getMethod().equalsIgnoreCase("delete") || (request.getMethod().equalsIgnoreCase("post") && UWSToolBox.hasParameter(UWSJob.PARAM_ACTION, UWSJob.ACTION_DELETE, request, false))); } /** @@ -92,7 +92,7 @@ public class DestroyJob extends UWSAction { * @see JobList#destroyJob(String,JobOwner) * @see UWSService#redirect(String, HttpServletRequest, JobOwner, String, HttpServletResponse) * - * @see uws.service.actions.UWSAction#apply(uws.service.UWSUrl, java.lang.String, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) + * @see uws.service.actions.UWSAction#apply(UWSUrl, JobOwner, HttpServletRequest, HttpServletResponse) */ @Override public boolean apply(UWSUrl urlInterpreter, JobOwner user, HttpServletRequest request, HttpServletResponse response) throws UWSException, IOException{ @@ -100,7 +100,13 @@ public class DestroyJob extends UWSAction { JobList jobsList = getJobsList(urlInterpreter); // Destroy the job: - boolean destroyed = jobsList.destroyJob(urlInterpreter.getJobId(), user); + boolean destroyed; + try{ + destroyed = jobsList.destroyJob(urlInterpreter.getJobId(), user); + }catch(UWSException ue){ + getLogger().logUWS(LogLevel.ERROR, urlInterpreter, "DESTROY_JOB", "Can not destroy the job \"" + urlInterpreter.getJobId() + "\"!", ue); + throw ue; + } // Make a redirection to the jobs list: uws.redirect(urlInterpreter.listJobs(jobsList.getName()).getRequestURL(), request, user, getName(), response); diff --git a/src/uws/service/actions/GetJobParam.java b/src/uws/service/actions/GetJobParam.java index 048ceff..be56b1a 100644 --- a/src/uws/service/actions/GetJobParam.java +++ b/src/uws/service/actions/GetJobParam.java @@ -16,31 +16,28 @@ package uws.service.actions; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.io.IOException; import java.io.InputStream; import javax.servlet.ServletOutputStream; - import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import uws.UWSException; -import uws.UWSExceptionFactory; import uws.UWSToolBox; - import uws.job.ErrorSummary; import uws.job.Result; import uws.job.UWSJob; - import uws.job.serializer.UWSSerializer; - import uws.job.user.JobOwner; - import uws.service.UWSService; import uws.service.UWSUrl; +import uws.service.log.UWSLog.LogLevel; +import uws.service.request.UploadFile; /** *

    The "Get Job Parameter" action of a UWS.

    @@ -52,8 +49,8 @@ import uws.service.UWSUrl; * whereas it is a complex type (i.e. results, parameters, ...) the value is the serialization of the job attribute itself. * The serializer is choosen in function of the HTTP Accept header.

    * - * @author Grégory Mantelet (CDS) - * @version 06/2012 + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (04/2015) */ public class GetJobParam extends UWSAction { private static final long serialVersionUID = 1L; @@ -85,7 +82,7 @@ public class GetJobParam extends UWSAction { *
  • the HTTP method is HTTP-GET.
  • * * - * @see uws.service.actions.UWSAction#match(uws.service.UWSUrl, java.lang.String, javax.servlet.http.HttpServletRequest) + * @see uws.service.actions.UWSAction#match(UWSUrl, JobOwner, HttpServletRequest) */ @Override public boolean match(UWSUrl urlInterpreter, JobOwner user, HttpServletRequest request) throws UWSException{ @@ -99,11 +96,11 @@ public class GetJobParam extends UWSAction { *

    Note: if the specified attribute is simple (i.e. jobID, runID, startTime, ...) it will not serialized ! The response will * merely be the job attribute value (so, the content type will be: text/plain).

    * - * @see #getJob(UWSUrl, String) + * @see #getJob(UWSUrl) * @see UWSService#getSerializer(String) * @see UWSJob#serialize(ServletOutputStream, UWSSerializer) * - * @see uws.service.actions.UWSAction#apply(uws.service.UWSUrl, java.lang.String, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) + * @see uws.service.actions.UWSAction#apply(UWSUrl, JobOwner, HttpServletRequest, HttpServletResponse) */ @Override public boolean apply(UWSUrl urlInterpreter, JobOwner user, HttpServletRequest request, HttpServletResponse response) throws UWSException, IOException{ @@ -116,7 +113,7 @@ public class GetJobParam extends UWSAction { if (attributes[0].equalsIgnoreCase(UWSJob.PARAM_RESULTS) && attributes.length > 1){ Result result = job.getResult(attributes[1]); if (result == null) - throw UWSExceptionFactory.incorrectJobResult(job.getJobId(), attributes[1]); + throw new UWSException(UWSException.NOT_FOUND, "No result identified with \"" + attributes[1] + "\" in the job \"" + job.getJobId() + "\"!"); else if (result.isRedirectionRequired()) uws.redirect(result.getHref(), request, user, getName(), response); else{ @@ -125,6 +122,7 @@ public class GetJobParam extends UWSAction { input = uws.getFileManager().getResultInput(result, job); UWSToolBox.write(input, result.getMimeType(), result.getSize(), response); }catch(IOException ioe){ + getLogger().logUWS(LogLevel.ERROR, result, "GET_RESULT", "Can not read the content of the result \"" + result.getId() + "\" of the job \"" + job.getJobId() + "\"!", ioe); throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, ioe, "Can not read the content of the result " + result.getId() + " (job ID: " + job.getJobId() + ")."); }finally{ if (input != null) @@ -135,30 +133,64 @@ public class GetJobParam extends UWSAction { else if (attributes[0].equalsIgnoreCase(UWSJob.PARAM_ERROR_SUMMARY) && attributes.length > 1 && attributes[1].equalsIgnoreCase("details")){ ErrorSummary error = job.getErrorSummary(); if (error == null) - throw UWSExceptionFactory.noErrorSummary(job.getJobId()); + throw new UWSException(UWSException.NOT_FOUND, "No error summary for the job \"" + job.getJobId() + "\"!"); else{ InputStream input = null; try{ input = uws.getFileManager().getErrorInput(error, job); - UWSToolBox.write(input, "text/plain", uws.getFileManager().getErrorSize(error, job), response); + UWSToolBox.write(input, getUWS().getErrorWriter().getErrorDetailsMIMEType(), uws.getFileManager().getErrorSize(error, job), response); }catch(IOException ioe){ + getLogger().logUWS(LogLevel.ERROR, error, "GET_ERROR", "Can not read the details of the error summary of the job \"" + job.getJobId() + "\"!", ioe); throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, ioe, "Can not read the error details (job ID: " + job.getJobId() + ")."); }finally{ if (input != null) input.close(); } } - } - // DEFAULT CASE: Display the serialization of the selected UWS object: + }// REFERENCE FILE: Display the content of the uploaded file or redirect to the URL (if it is a URL): + else if (attributes[0].equalsIgnoreCase(UWSJob.PARAM_PARAMETERS) && attributes.length > 1 && job.getAdditionalParameterValue(attributes[1]) != null && job.getAdditionalParameterValue(attributes[1]) instanceof UploadFile){ + UploadFile upl = (UploadFile)job.getAdditionalParameterValue(attributes[1]); + if (upl.getLocation().matches("^http(s)?://")) + uws.redirect(upl.getLocation(), request, user, getName(), response); + else{ + InputStream input = null; + try{ + input = uws.getFileManager().getUploadInput(upl); + UWSToolBox.write(input, upl.mimeType, upl.length, response); + }catch(IOException ioe){ + getLogger().logUWS(LogLevel.ERROR, upl, "GET_PARAMETER", "Can not read the content of the uploaded file \"" + upl.paramName + "\" of the job \"" + job.getJobId() + "\"!", ioe); + throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, ioe, "Can not read the content of the uploaded file " + upl.paramName + " (job ID: " + job.getJobId() + ")."); + }finally{ + if (input != null) + input.close(); + } + } + }// DEFAULT CASE: Display the serialization of the selected UWS object: else{ // Write the value/content of the selected attribute: UWSSerializer serializer = uws.getSerializer(request.getHeader("Accept")); String uwsField = attributes[0]; - if (uwsField == null || uwsField.trim().isEmpty() || (attributes.length <= 1 && (uwsField.equalsIgnoreCase(UWSJob.PARAM_ERROR_SUMMARY) || uwsField.equalsIgnoreCase(UWSJob.PARAM_RESULTS) || uwsField.equalsIgnoreCase(UWSJob.PARAM_PARAMETERS)))) + boolean jobSerialization = false; + // Set the content type: + if (uwsField == null || uwsField.trim().isEmpty() || (attributes.length <= 1 && (uwsField.equalsIgnoreCase(UWSJob.PARAM_ERROR_SUMMARY) || uwsField.equalsIgnoreCase(UWSJob.PARAM_RESULTS) || uwsField.equalsIgnoreCase(UWSJob.PARAM_PARAMETERS)))){ response.setContentType(serializer.getMimeType()); - else + jobSerialization = true; + }else response.setContentType("text/plain"); - job.serialize(response.getOutputStream(), attributes, serializer); + + // Set the character encoding: + response.setCharacterEncoding(UWSToolBox.DEFAULT_CHAR_ENCODING); + // Serialize the selected attribute: + try{ + job.serialize(response.getOutputStream(), attributes, serializer); + }catch(Exception e){ + if (!(e instanceof UWSException)){ + String errorMsgPart = (jobSerialization ? "the job \"" + job.getJobId() + "\"" : "the parameter " + uwsField + " of the job \"" + job.getJobId() + "\""); + getLogger().logUWS(LogLevel.ERROR, urlInterpreter, "SERIALIZE", "Can not serialize " + errorMsgPart + "!", e); + throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, e, "Can not format properly " + errorMsgPart + "!"); + }else + throw (UWSException)e; + } } return true; diff --git a/src/uws/service/actions/JobSummary.java b/src/uws/service/actions/JobSummary.java index adf9ed0..e35e1d8 100644 --- a/src/uws/service/actions/JobSummary.java +++ b/src/uws/service/actions/JobSummary.java @@ -16,26 +16,24 @@ package uws.service.actions; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.io.IOException; import javax.servlet.ServletOutputStream; - import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import uws.UWSException; - +import uws.UWSToolBox; import uws.job.UWSJob; - import uws.job.serializer.UWSSerializer; - import uws.job.user.JobOwner; - import uws.service.UWSService; import uws.service.UWSUrl; +import uws.service.log.UWSLog.LogLevel; /** *

    The "Get Job" action of a UWS.

    @@ -45,8 +43,8 @@ import uws.service.UWSUrl; *

    This action returns the summary of the job specified in the given UWS URL. * This summary is serialized by the {@link UWSSerializer} choosed in function of the HTTP Accept header.

    * - * @author Grégory Mantelet (CDS) - * @version 05/2012 + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (04/2015) */ public class JobSummary extends UWSAction { private static final long serialVersionUID = 1L; @@ -78,7 +76,7 @@ public class JobSummary extends UWSAction { *
  • the HTTP method is HTTP-GET.
  • * * - * @see uws.service.actions.UWSAction#match(uws.service.UWSUrl, java.lang.String, javax.servlet.http.HttpServletRequest) + * @see uws.service.actions.UWSAction#match(UWSUrl, JobOwner, HttpServletRequest) */ @Override public boolean match(UWSUrl urlInterpreter, JobOwner user, HttpServletRequest request) throws UWSException{ @@ -89,11 +87,11 @@ public class JobSummary extends UWSAction { * Gets the specified job (and throw an error if not found), * chooses the serializer and write the serialization of the job in the given response. * - * @see #getJob(UWSUrl, String) + * @see #getJob(UWSUrl) * @see UWSService#getSerializer(String) * @see UWSJob#serialize(ServletOutputStream, UWSSerializer) * - * @see uws.service.actions.UWSAction#apply(uws.service.UWSUrl, java.lang.String, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) + * @see uws.service.actions.UWSAction#apply(UWSUrl, JobOwner, HttpServletRequest, HttpServletResponse) */ @Override public boolean apply(UWSUrl urlInterpreter, JobOwner user, HttpServletRequest request, HttpServletResponse response) throws UWSException, IOException{ @@ -103,7 +101,16 @@ public class JobSummary extends UWSAction { // Write the job summary: UWSSerializer serializer = uws.getSerializer(request.getHeader("Accept")); response.setContentType(serializer.getMimeType()); - job.serialize(response.getOutputStream(), serializer, user); + response.setCharacterEncoding(UWSToolBox.DEFAULT_CHAR_ENCODING); + try{ + job.serialize(response.getOutputStream(), serializer, user); + }catch(Exception e){ + if (!(e instanceof UWSException)){ + getLogger().logUWS(LogLevel.ERROR, urlInterpreter, "SERIALIZE", "Can not serialize the job \"" + job.getJobId() + "\"!", e); + throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, e, "Can not format properly the job \"" + job.getJobId() + "\"!"); + }else + throw (UWSException)e; + } return true; } diff --git a/src/uws/service/actions/ListJobs.java b/src/uws/service/actions/ListJobs.java index dfeeb4f..2ce5c85 100644 --- a/src/uws/service/actions/ListJobs.java +++ b/src/uws/service/actions/ListJobs.java @@ -16,26 +16,24 @@ package uws.service.actions; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.io.IOException; import javax.servlet.ServletOutputStream; - import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import uws.UWSException; - +import uws.UWSToolBox; import uws.job.JobList; - import uws.job.serializer.UWSSerializer; - import uws.job.user.JobOwner; - import uws.service.UWSService; import uws.service.UWSUrl; +import uws.service.log.UWSLog.LogLevel; /** *

    The "List Jobs" action of a UWS.

    @@ -45,8 +43,8 @@ import uws.service.UWSUrl; *

    This action returns the list of jobs contained in the jobs list specified by the URL of the request. * This list is serialized by the {@link UWSSerializer} choosed in function of the HTTP Accept header.

    * - * @author Grégory Mantelet (CDS) - * @version 05/2012 + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (04/2015) */ public class ListJobs extends UWSAction { private static final long serialVersionUID = 1L; @@ -77,7 +75,7 @@ public class ListJobs extends UWSAction { *
  • the HTTP method is HTTP-GET.
  • * * - * @see uws.service.actions.UWSAction#match(uws.service.UWSUrl, java.lang.String, javax.servlet.http.HttpServletRequest) + * @see uws.service.actions.UWSAction#match(UWSUrl, JobOwner, HttpServletRequest) */ @Override public boolean match(UWSUrl urlInterpreter, JobOwner user, HttpServletRequest request) throws UWSException{ @@ -90,9 +88,9 @@ public class ListJobs extends UWSAction { * * @see #getJobsList(UWSUrl) * @see UWSService#getSerializer(String) - * @see JobList#serialize(ServletOutputStream, UWSSerializer, String) + * @see JobList#serialize(ServletOutputStream, UWSSerializer, JobOwner) * - * @see uws.service.actions.UWSAction#apply(uws.service.UWSUrl, java.lang.String, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) + * @see uws.service.actions.UWSAction#apply(UWSUrl, JobOwner, HttpServletRequest, HttpServletResponse) */ @Override public boolean apply(UWSUrl urlInterpreter, JobOwner user, HttpServletRequest request, HttpServletResponse response) throws UWSException, IOException{ @@ -102,7 +100,16 @@ public class ListJobs extends UWSAction { // Write the jobs list: UWSSerializer serializer = uws.getSerializer(request.getHeader("Accept")); response.setContentType(serializer.getMimeType()); - jobsList.serialize(response.getOutputStream(), serializer, user); + response.setCharacterEncoding(UWSToolBox.DEFAULT_CHAR_ENCODING); + try{ + jobsList.serialize(response.getOutputStream(), serializer, user); + }catch(Exception e){ + if (!(e instanceof UWSException)){ + getLogger().logUWS(LogLevel.ERROR, urlInterpreter, "SERIALIZE", "Can not serialize the jobs list \"" + jobsList.getName() + "\"!", e); + throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, e, "Can not format properly the jobs list \"" + jobsList.getName() + "\"!"); + }else + throw (UWSException)e; + } return true; } diff --git a/src/uws/service/actions/SetJobParam.java b/src/uws/service/actions/SetJobParam.java index 02026bb..51d848b 100644 --- a/src/uws/service/actions/SetJobParam.java +++ b/src/uws/service/actions/SetJobParam.java @@ -16,7 +16,8 @@ package uws.service.actions; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.io.IOException; @@ -25,14 +26,14 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import uws.UWSException; - +import uws.UWSToolBox; import uws.job.UWSJob; - import uws.job.parameters.UWSParameters; import uws.job.user.JobOwner; - +import uws.service.UWSFactory; import uws.service.UWSService; import uws.service.UWSUrl; +import uws.service.log.UWSLog.LogLevel; /** *

    The "Set Job Parameter" action of a UWS.

    @@ -42,8 +43,8 @@ import uws.service.UWSUrl; *

    This action sets the value of the specified job attribute. * The response of this action is a redirection to the job summary.

    * - * @author Grégory Mantelet (CDS) - * @version 05/2012 + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (04/2015) */ public class SetJobParam extends UWSAction { private static final long serialVersionUID = 1L; @@ -70,16 +71,16 @@ public class SetJobParam extends UWSAction { * Checks whether: *
      *
    • a job list name is specified in the given UWS URL (note: by default, the existence of the jobs list is not checked),
    • - *
    • a job ID is given in the UWS URL (note: by default, the existence of the job is not checked),
    • + *
    • a job ID is given in the UWS URL (note: by default, the existence of the job is not yet checked),
    • *
    • if the HTTP method is HTTP-POST: there is exactly one attribute and at least one parameter
    • *
    • if the HTTP method is HTTP-PUT: there are at least two attributes ({@link UWSJob#PARAM_PARAMETERS}/{parameter_name}) and there are at least two parameters
    • *
    * - * @see uws.service.actions.UWSAction#match(uws.service.UWSUrl, java.lang.String, javax.servlet.http.HttpServletRequest) + * @see uws.service.actions.UWSAction#match(UWSUrl, JobOwner, HttpServletRequest) */ @Override public boolean match(UWSUrl urlInterpreter, JobOwner user, HttpServletRequest request) throws UWSException{ - return (urlInterpreter.hasJobList() && urlInterpreter.hasJob() && ((request.getMethod().equalsIgnoreCase("post") && (!urlInterpreter.hasAttribute() || urlInterpreter.getAttributes().length == 1) && request.getParameterMap().size() > 0) || (request.getMethod().equalsIgnoreCase("put") && urlInterpreter.getAttributes().length >= 2 && urlInterpreter.getAttributes()[0].equalsIgnoreCase(UWSJob.PARAM_PARAMETERS) && request.getParameter(urlInterpreter.getAttributes()[1]) != null))); + return (urlInterpreter.hasJobList() && urlInterpreter.hasJob() && ((request.getMethod().equalsIgnoreCase("post") && (!urlInterpreter.hasAttribute() || urlInterpreter.getAttributes().length == 1)) || (request.getMethod().equalsIgnoreCase("put") && urlInterpreter.getAttributes().length >= 2 && urlInterpreter.getAttributes()[0].equalsIgnoreCase(UWSJob.PARAM_PARAMETERS) && UWSToolBox.hasParameter(urlInterpreter.getAttributes()[1], request, false)))); } /** @@ -87,19 +88,24 @@ public class SetJobParam extends UWSAction { * changes the value of the specified job attribute * and makes a redirection to the job summary.

    * - * @see #getJob(UWSUrl, String) - * @see UWSService#createUWSParameters(HttpServletRequest) - * @see UWSJob#addOrUpdateParameters(java.util.Map) + * @see #getJob(UWSUrl) + * @see UWSFactory#createUWSParameters(HttpServletRequest) + * @see UWSJob#addOrUpdateParameters(UWSParameters, JobOwner) * @see UWSService#redirect(String, HttpServletRequest, JobOwner, String, HttpServletResponse) - * - * @see uws.service.actions.UWSAction#apply(uws.service.UWSUrl, java.lang.String, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) + * @see uws.service.actions.UWSAction#apply(UWSUrl, JobOwner, HttpServletRequest, HttpServletResponse) */ @Override public boolean apply(UWSUrl urlInterpreter, JobOwner user, HttpServletRequest request, HttpServletResponse response) throws UWSException, IOException{ // Get the job: UWSJob job = getJob(urlInterpreter); - UWSParameters params = uws.getFactory().createUWSParameters(request); + UWSParameters params; + try{ + params = uws.getFactory().createUWSParameters(request); + }catch(UWSException ue){ + getLogger().logUWS(LogLevel.ERROR, request, "SET_PARAM", "Can not parse the sent UWS parameters!", ue); + throw ue; + } // Update the job parameters: boolean updated = job.addOrUpdateParameters(params, user); diff --git a/src/uws/service/actions/SetUWSParameter.java b/src/uws/service/actions/SetUWSParameter.java new file mode 100644 index 0000000..5364b3e --- /dev/null +++ b/src/uws/service/actions/SetUWSParameter.java @@ -0,0 +1,110 @@ +package uws.service.actions; + +/* + * This file is part of UWSLibrary. + * + * UWSLibrary is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * UWSLibrary is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with UWSLibrary. If not, see . + * + * Copyright 2014-2015 - Astronomisches Rechen Institut (ARI) + */ + +import java.io.IOException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import uws.UWSException; +import uws.UWSExceptionFactory; +import uws.UWSToolBox; +import uws.job.UWSJob; +import uws.job.parameters.UWSParameters; +import uws.job.user.JobOwner; +import uws.service.UWSService; +import uws.service.UWSUrl; + +/** + *

    The UWS action which lets set the phase (RUN or ABORT), the execution duration and the destruction time of a job + * with a POST or PUT request on {job-id}/{uws-param}.

    + * + *

    Note: The corresponding name is {@link UWSAction#SET_UWS_PARAMETER}.

    + * + * @author Grégory Mantelet (ARI) + * @version 4.1 (04/2015) + * @since 4.1 + */ +public class SetUWSParameter extends UWSAction { + private static final long serialVersionUID = 1L; + + public SetUWSParameter(final UWSService u){ + super(u); + } + + /** + * @see UWSAction#SET_UWS_PARAMETER + * @see uws.service.actions.UWSAction#getName() + */ + @Override + public String getName(){ + return SET_UWS_PARAMETER; + } + + @Override + public String getDescription(){ + return "Let change one of the standard UWS parameters of a job (e.g. phase, executionduration, destruction) (URL: {baseUWS_URL}/{jobListName}/{jobId}/{uws-param}, where {uws-param} = \"phase\" or \"executionduration\" or \"destruction\", Method: HTTP-POST or HTTP-PUT, Parameter: \"{uws-param}={param-value}\" in POST and \"{param-value\" in PUT (content-type:text/plain))"; + } + + /** + * Checks whether: + *
      + *
    • a job list name is specified in the given UWS URL (note: by default, the existence of the jobs list is not checked),
    • + *
    • a job ID is given in the UWS URL (note: by default, the existence of the job is not yet checked),
    • + *
    • the job attribute "phase", "runID", "executionduration" or "destruction" is used in the UWS URL, + *
    • the HTTP method is HTTP-POST or HTTP-PUT.
    • + *
    + * @see uws.service.actions.UWSAction#match(UWSUrl, JobOwner, HttpServletRequest) + */ + @Override + public boolean match(UWSUrl urlInterpreter, JobOwner user, HttpServletRequest request) throws UWSException{ + return (urlInterpreter.hasJobList() && urlInterpreter.hasJob() && urlInterpreter.getAttributes().length == 1 && urlInterpreter.getAttributes()[0].toLowerCase().matches(UWSParameters.UWS_RW_PARAMETERS_REGEXP) && (request.getMethod().equalsIgnoreCase("post") || request.getMethod().equalsIgnoreCase("put")) && UWSToolBox.hasParameter(urlInterpreter.getAttributes()[0], request, false)); + } + + /** + * Get the specified job (throw an error if not found), + * and update the specified UWS standard parameter. + * + * @see #getJob(UWSUrl) + * @see UWSJob#addOrUpdateParameter(String, Object) + * @see UWSService#redirect(String, HttpServletRequest, JobOwner, String, HttpServletResponse) + * + * @see uws.service.actions.UWSAction#apply(UWSUrl, JobOwner, HttpServletRequest, HttpServletResponse) + */ + @Override + public boolean apply(UWSUrl urlInterpreter, JobOwner user, HttpServletRequest request, HttpServletResponse response) throws UWSException, IOException{ + // Get the job: + UWSJob job = getJob(urlInterpreter); + + // Forbids the action if the user has not the WRITE permission for the specified job: + if (user != null && !user.hasWritePermission(job)) + throw new UWSException(UWSException.PERMISSION_DENIED, UWSExceptionFactory.writePermissionDenied(user, true, job.getJobId())); + + String name = urlInterpreter.getAttributes()[0]; + job.addOrUpdateParameter(name, UWSToolBox.getParameter(name, request, false), user); + + // Make a redirection to the job: + uws.redirect(urlInterpreter.jobSummary(urlInterpreter.getJobListName(), job.getJobId()).getRequestURL(), request, user, getName(), response); + + return true; + } + +} diff --git a/src/uws/service/actions/ShowHomePage.java b/src/uws/service/actions/ShowHomePage.java index c437497..d40c892 100644 --- a/src/uws/service/actions/ShowHomePage.java +++ b/src/uws/service/actions/ShowHomePage.java @@ -16,28 +16,26 @@ package uws.service.actions; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; - import java.net.URL; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import uws.UWSException; -import uws.UWSExceptionFactory; - +import uws.UWSToolBox; import uws.job.serializer.UWSSerializer; - import uws.job.user.JobOwner; - import uws.service.UWSService; import uws.service.UWSUrl; +import uws.service.log.UWSLog.LogLevel; /** *

    The "Show UWS Home Page" action of a UWS.

    @@ -46,8 +44,8 @@ import uws.service.UWSUrl; * *

    This action displays the UWS home page.

    * - * @author Grégory Mantelet (CDS) - * @version 05/2012 + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (04/2015) */ public class ShowHomePage extends UWSAction { private static final long serialVersionUID = 1L; @@ -85,7 +83,7 @@ public class ShowHomePage extends UWSAction { *
      *
    • Default home page ({@link UWSService#isDefaultHomePage()} returns true): * write the appropriate (considering the Accept header of the HTTP-Request) serialization of this UWS.
    • - *
    • Home redirection ({@link UWSService#isHomePageRedirection()} = true): call {@link UWSService#redirect(String, HttpServletRequest, HttpServletResponse)} with the {@link UWSService#getHomePage()} URL.
    • + *
    • Home redirection ({@link UWSService#isHomePageRedirection()} = true): call {@link UWSService#redirect(String, HttpServletRequest, JobOwner, String, HttpServletResponse)} with the {@link UWSService#getHomePage()} URL.
    • *
    • Otherwise (({@link UWSService#isHomePageRedirection()} = false)): read the content of the resource at the {@link UWSService#getHomePage()} URL and copy it in the given {@link HttpServletResponse}.
    • *
    * @@ -94,21 +92,35 @@ public class ShowHomePage extends UWSAction { * @throws IOException If there is an error while reading at a custom home page URL * or while writing in the given HttpServletResponse. * - * @see uws.service.actions.UWSAction#apply(uws.service.UWSUrl, java.lang.String, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) + * @see uws.service.actions.UWSAction#apply(UWSUrl, JobOwner, HttpServletRequest, HttpServletResponse) * @see UWSService#redirect(String, HttpServletRequest, JobOwner, String, HttpServletResponse) */ @Override public boolean apply(UWSUrl urlInterpreter, JobOwner user, HttpServletRequest request, HttpServletResponse response) throws UWSException, IOException{ + if (uws.isDefaultHomePage()){ UWSSerializer serializer = uws.getSerializer(request.getHeader("Accept")); response.setContentType(serializer.getMimeType()); - String serialization = serializer.getUWS(uws); + response.setCharacterEncoding(UWSToolBox.DEFAULT_CHAR_ENCODING); + // Get a short and simple serialization of this UWS: + String serialization; + try{ + serialization = serializer.getUWS(uws); + }catch(Exception e){ + if (!(e instanceof UWSException)){ + getLogger().logUWS(LogLevel.ERROR, urlInterpreter, "SERIALIZE", "Can't display the default home page, due to a serialization error!", e); + throw new UWSException(UWSException.NO_CONTENT, e, "No home page available for this UWS service!"); + }else + throw (UWSException)e; + } + // Write the simple UWS serialization in the given response: if (serialization != null){ PrintWriter output = response.getWriter(); output.print(serialization); output.flush(); }else - throw UWSExceptionFactory.incorrectSerialization(serialization, "the UWS " + uws.getName()); + throw new UWSException(UWSException.NO_CONTENT, "No home page available for this UWS service."); + }else{ if (uws.isHomePageRedirection()) uws.redirect(uws.getHomePage(), request, user, getName(), response); @@ -117,6 +129,7 @@ public class ShowHomePage extends UWSAction { BufferedReader reader = new BufferedReader(new InputStreamReader(homePageUrl.openStream())); response.setContentType("text/html"); + response.setCharacterEncoding(UWSToolBox.DEFAULT_CHAR_ENCODING); PrintWriter writer = response.getWriter(); try{ String line = null; diff --git a/src/uws/service/actions/UWSAction.java b/src/uws/service/actions/UWSAction.java index 3813363..5cd0344 100644 --- a/src/uws/service/actions/UWSAction.java +++ b/src/uws/service/actions/UWSAction.java @@ -16,7 +16,8 @@ package uws.service.actions; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.io.IOException; @@ -26,15 +27,12 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import uws.UWSException; -import uws.UWSExceptionFactory; - import uws.job.JobList; import uws.job.UWSJob; - import uws.job.user.JobOwner; - import uws.service.UWSService; import uws.service.UWSUrl; +import uws.service.log.UWSLog; /** *

    Action of a UWS (i.e. "List Jobs", "Get Job", etc...). An instance of a UWSAction can be added to a given UWS thanks to the method @@ -43,8 +41,8 @@ import uws.service.UWSUrl; *

    WARNING: The action of a UWS have, each one, a different name. So be careful about the name of your UWS action ! * By default the name of a UWS action is the full java name of the class !

    * - * @author Grégory Mantelet (CDS) - * @version 05/2012 + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (11/2014) * * @see UWSService */ @@ -55,6 +53,9 @@ public abstract class UWSAction implements Serializable { public final static String LIST_JOBS = "List Jobs"; /** Name of the UWS action {@link AddJob}. */ public final static String ADD_JOB = "Add Job"; + /** Name of the UWS action {@link SetUWSParameter}. + * @since 4.1 */ + public final static String SET_UWS_PARAMETER = "Set UWS Parameter"; /** Name of the UWS action {@link DestroyJob}. */ public final static String DESTROY_JOB = "Destroy Job"; /** Name of the UWS action {@link JobSummary}. */ @@ -93,6 +94,17 @@ public abstract class UWSAction implements Serializable { return uws; } + /** + * Get the logger associated with this UWS service. + * + * @return UWS logger. + * + * @since 4.1 + */ + public final UWSLog getLogger(){ + return uws.getLogger(); + } + /** *

    Gets the name of this UWS action. MUST BE UNIQUE !

    * @@ -139,9 +151,9 @@ public abstract class UWSAction implements Serializable { if (jlName != null){ jobsList = uws.getJobList(jlName); if (jobsList == null) - throw UWSExceptionFactory.incorrectJobListName(jlName); + throw new UWSException(UWSException.NOT_FOUND, "Incorrect job list name! The jobs list " + jlName + " does not exist."); }else - throw UWSExceptionFactory.missingJobListName(); + throw new UWSException(UWSException.BAD_REQUEST, "Missing job list name!"); return jobsList; } @@ -150,7 +162,7 @@ public abstract class UWSAction implements Serializable { *

    Extracts the job ID from the given UWS URL * and gets the corresponding job from the UWS.

    * - *

    Note: This function calls {@link #getJob(UWSUrl, String, boolean)} with userId=null and checkUser=false !

    + *

    Note: This function calls {@link #getJob(UWSUrl, JobOwner)} with userId=null and checkUser=false !

    * * @param urlInterpreter The UWS URL which contains the ID of the job to get. * @@ -160,7 +172,7 @@ public abstract class UWSAction implements Serializable { * or if there are no corresponding jobs list and/or job in the UWS * or if the specified user has not enough rights to get the specified job. * - * @see #getJob(UWSUrl, String, boolean) + * @see #getJob(UWSUrl, JobOwner) */ protected final UWSJob getJob(UWSUrl urlInterpreter) throws UWSException{ return getJob(urlInterpreter, (JobOwner)null); @@ -181,7 +193,7 @@ public abstract class UWSAction implements Serializable { * * @see UWSUrl#getJobId() * @see #getJobsList(UWSUrl) - * @see JobList#getJob(String,String) + * @see JobList#getJob(String, JobOwner) * * @since 3.1 */ @@ -193,9 +205,9 @@ public abstract class UWSAction implements Serializable { JobList jobsList = getJobsList(urlInterpreter); job = jobsList.getJob(jobId, user); if (job == null) - throw UWSExceptionFactory.incorrectJobID(jobsList.getName(), jobId); + throw new UWSException(UWSException.NOT_FOUND, "Incorrect job ID! The job \"" + jobId + "\" does not exist in the jobs list \"" + jobsList.getName() + "\"."); }else - throw UWSExceptionFactory.missingJobID(); + throw new UWSException(UWSException.BAD_REQUEST, "Missing job ID!"); return job; } @@ -203,7 +215,7 @@ public abstract class UWSAction implements Serializable { /** *

    Extracts the job ID from the given UWS URL and gets the corresponding job from the given jobs list.

    * - *

    Note: This function calls {@link #getJob(UWSUrl, JobList, String, boolean)} with userId=null and checkUser=false !

    + *

    Note: This function calls {@link #getJob(UWSUrl, JobList, JobOwner)} with userId=null and checkUser=false !

    * * @param urlInterpreter The UWS URL which contains the ID of the job to get. * @param jobsList The jobs list which is supposed to contain the job to get. @@ -213,7 +225,7 @@ public abstract class UWSAction implements Serializable { * @throws UWSException If no job ID can be found in the given UWS URL * or if there are no corresponding job in the UWS. * - * @see #getJob(UWSUrl, JobList, String, boolean) + * @see #getJob(UWSUrl, JobList, JobOwner) */ protected final UWSJob getJob(UWSUrl urlInterpreter, JobList jobsList) throws UWSException{ return getJob(urlInterpreter, jobsList, null); @@ -234,8 +246,7 @@ public abstract class UWSAction implements Serializable { * or if the specified user has not enough rights. * * @see UWSUrl#getJobId() - * @see JobList#getJob(String) - * @see JobList#getJob(String,String) + * @see JobList#getJob(String, JobOwner) * * @since 3.1 */ @@ -245,12 +256,12 @@ public abstract class UWSAction implements Serializable { if (jobId != null){ if (jobsList == null) - throw UWSExceptionFactory.missingJobListName(); + throw new UWSException(UWSException.BAD_REQUEST, "Missing job list name!"); job = jobsList.getJob(jobId, user); if (job == null) - throw UWSExceptionFactory.incorrectJobID(jobsList.getName(), jobId); + throw new UWSException(UWSException.NOT_FOUND, "Incorrect job ID! The job \"" + jobId + "\" does not exist in the jobs list \"" + jobsList.getName() + "\"."); }else - throw UWSExceptionFactory.missingJobID(); + throw new UWSException(UWSException.BAD_REQUEST, "Missing job ID!"); return job; } diff --git a/src/uws/service/backup/DefaultUWSBackupManager.java b/src/uws/service/backup/DefaultUWSBackupManager.java index 2a9db4b..22393f8 100644 --- a/src/uws/service/backup/DefaultUWSBackupManager.java +++ b/src/uws/service/backup/DefaultUWSBackupManager.java @@ -16,14 +16,14 @@ package uws.service.backup; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintWriter; - import java.text.ParseException; import java.util.ArrayList; import java.util.Date; @@ -41,25 +41,21 @@ import org.json.JSONTokener; import org.json.JSONWriter; import org.json.Json4Uws; +import uws.ISO8601Format; import uws.UWSException; -import uws.UWSExceptionFactory; import uws.UWSToolBox; - import uws.job.ErrorSummary; import uws.job.ErrorType; import uws.job.JobList; import uws.job.Result; import uws.job.UWSJob; - import uws.job.parameters.UWSParameters; -import uws.job.serializer.JSONSerializer; - import uws.job.user.JobOwner; - import uws.service.UWS; import uws.service.file.UWSFileManager; - import uws.service.log.UWSLog; +import uws.service.log.UWSLog.LogLevel; +import uws.service.request.UploadFile; /** *

    Default implementation of the interface {@link UWSBackupManager}.

    @@ -80,8 +76,8 @@ import uws.service.log.UWSLog; * *

    Another positive value will be considered as the frequency (in milliseconds) of the automatic backup (= {@link #saveAll()}).

    * - * @author Grégory Mantelet (CDS) - * @version 06/2012 + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (12/2014) */ public class DefaultUWSBackupManager implements UWSBackupManager { @@ -117,7 +113,7 @@ public class DefaultUWSBackupManager implements UWSBackupManager { * * @param uws The UWS to save/restore. * - * @see #DefaultBackupManager(UWS, long) + * @see #DefaultUWSBackupManager(UWS, long) */ public DefaultUWSBackupManager(final UWS uws){ this(uws, DEFAULT_FREQUENCY); @@ -160,7 +156,7 @@ public class DefaultUWSBackupManager implements UWSBackupManager { * * @throws UWSException If the user identification is disabled (that's to say, if the given UWS has no UserIdentifier) while the parameter byUser is true. * - * @see #DefaultBackupManager(UWS, boolean, long) + * @see #DefaultUWSBackupManager(UWS, boolean, long) */ public DefaultUWSBackupManager(final UWS uws, final boolean byUser) throws UWSException{ this(uws, byUser, byUser ? AT_USER_ACTION : DEFAULT_FREQUENCY); @@ -181,7 +177,7 @@ public class DefaultUWSBackupManager implements UWSBackupManager { this.backupFreq = frequency; if (byUser && uws.getUserIdentifier() == null) - throw new UWSException("Impossible to save/restore a UWS by user, if the user identification is disabled (no UserIdentifier is set to the UWS) !"); + throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, "Impossible to save/restore a UWS by user if the user identification is disabled (no UserIdentifier is set to the UWS)!"); if (backupFreq == AT_USER_ACTION && !byUser) backupFreq = MANUAL; @@ -206,6 +202,7 @@ public class DefaultUWSBackupManager implements UWSBackupManager { return enabled; } + @Override public final void setEnabled(boolean enabled){ this.enabled = enabled; if (backupFreq > 0){ @@ -310,6 +307,7 @@ public class DefaultUWSBackupManager implements UWSBackupManager { /* SAVE METHODS */ /* ************ */ + @Override public int[] saveAll(){ if (!enabled) return null; @@ -362,7 +360,7 @@ public class DefaultUWSBackupManager implements UWSBackupManager { out.value(getJSONUser(user)); nbSavedOwners++; }catch(JSONException je){ - getLogger().error("Unexpected JSON error while saving the user '" + user.getID() + "' !", je); + getLogger().logUWS(LogLevel.ERROR, user, "BACKUP", "Unexpected JSON error while saving the user '" + user.getID() + "'!", je); } } out.endArray(); @@ -378,9 +376,9 @@ public class DefaultUWSBackupManager implements UWSBackupManager { nbSavedJobs++; writer.flush(); }catch(UWSException ue){ - getLogger().error("Unexpected UWS error while saving the job '" + job.getJobId() + "' !", ue); + getLogger().logUWS(LogLevel.ERROR, job, "BACKUP", "Unexpected UWS error while saving the job '" + job.getJobId() + "'!", ue); }catch(JSONException je){ - getLogger().error("Unexpected JSON error while saving the job '" + job.getJobId() + "' !", je); + getLogger().logUWS(LogLevel.ERROR, job, "BACKUP", "Unexpected JSON error while saving the job '" + job.getJobId() + "'!", je); } } } @@ -390,9 +388,9 @@ public class DefaultUWSBackupManager implements UWSBackupManager { out.endObject(); }catch(JSONException je){ - getLogger().error("Unexpected JSON error while saving the whole UWS !", je); + getLogger().logUWS(LogLevel.ERROR, null, "BACKUP", "Unexpected JSON error while saving the whole UWS !", je); }catch(IOException ie){ - getLogger().error("Unexpected IO error while saving the whole UWS !", ie); + getLogger().logUWS(LogLevel.ERROR, null, "BACKUP", "Unexpected IO error while saving the whole UWS !", ie); }finally{ // Close the writer: if (writer != null) @@ -402,13 +400,14 @@ public class DefaultUWSBackupManager implements UWSBackupManager { // Build the report and log it: int[] report = new int[]{nbSavedJobs,nbJobs,nbSavedOwners,nbOwners}; - getLogger().uwsSaved(uws, report); + getLogger().logUWS(LogLevel.INFO, report, "BACKUPED", "UWS Service \"" + uws.getName() + "\" backuped!", null); lastBackup = new Date(); return report; } + @Override public int[] saveOwner(JobOwner user){ if (!enabled) return null; @@ -436,7 +435,7 @@ public class DefaultUWSBackupManager implements UWSBackupManager { out.object(); // Write the backup date: - out.key("date").value(UWSJob.dateFormat.format(new Date())); + out.key("date").value(ISO8601Format.format(new Date())); // Write the description of the user: out.key("user").value(getJSONUser(user)); @@ -453,9 +452,9 @@ public class DefaultUWSBackupManager implements UWSBackupManager { saveReport[0]++; writer.flush(); }catch(JSONException je){ - getLogger().error("Unexpected JSON error while saving the " + saveReport[1] + "-th job of the job list '" + jl.getName() + "' owned by the user '" + user.getID() + "' !", je); + getLogger().logUWS(LogLevel.ERROR, null, "BACKUP", "Unexpected JSON error while saving the " + saveReport[1] + "-th job of the job list '" + jl.getName() + "' owned by the user '" + user.getID() + "'!", je); }catch(UWSException ue){ - getLogger().error("Unexpected UWS error while saving the " + saveReport[1] + "-th job of the job list '" + jl.getName() + "' owned by the user '" + user.getID() + "' !", ue); + getLogger().logUWS(LogLevel.ERROR, null, "BACKUP", "Unexpected UWS error while saving the " + saveReport[1] + "-th job of the job list '" + jl.getName() + "' owned by the user '" + user.getID() + "'!", ue); } } } @@ -465,16 +464,16 @@ public class DefaultUWSBackupManager implements UWSBackupManager { out.endObject(); // Log the "save" report: - getLogger().ownerJobsSaved(user, saveReport); + getLogger().logUWS(LogLevel.INFO, saveReport, "BACKUPED", "UWS backuped!", null); lastBackup = new Date(); return saveReport; }catch(IOException ie){ - getLogger().error("Unexpected IO error while saving the jobs of user '" + user.getID() + "' !", ie); + getLogger().logUWS(LogLevel.ERROR, null, "BACKUP", "Unexpected IO error while saving the jobs of user '" + user.getID() + "'!", ie); }catch(JSONException je){ - getLogger().error("Unexpected JSON error while saving the jobs of user '" + user.getID() + "' !", je); + getLogger().logUWS(LogLevel.ERROR, null, "BACKUP", "Unexpected JSON error while saving the jobs of user '" + user.getID() + "'!", je); }finally{ // Close the writer: if (writer != null) @@ -525,7 +524,7 @@ public class DefaultUWSBackupManager implements UWSBackupManager { * *

    * note: - * the structure of the returned JSON object is decided by {@link JSONSerializer#getJson(UWSJob)}. + * the structure of the returned JSON object is decided by {@link Json4Uws#getJson(UWSJob)}. * Only one attribute is added: "jobListName". *

    * @@ -539,15 +538,70 @@ public class DefaultUWSBackupManager implements UWSBackupManager { */ protected JSONObject getJSONJob(final UWSJob job, final String jlName) throws UWSException, JSONException{ JSONObject jsonJob = Json4Uws.getJson(job); + + // Re-Build the parameters map, by separating the uploads and the "normal" parameters: + JSONArray uploads = new JSONArray(); + JSONObject params = new JSONObject(); + Object val; + for(String name : job.getAdditionalParameters()){ + // get the raw value: + val = job.getAdditionalParameterValue(name); + // if an array, build a JSON array of strings: + if (val != null && val.getClass().isArray()){ + JSONArray array = new JSONArray(); + for(Object o : (Object[])val){ + if (o != null) + array.put(o.toString()); + } + params.put(name, array); + }else if (val != null && val instanceof UploadFile) + uploads.put(getUploadJson((UploadFile)val)); + // otherwise, just put the value: + else if (val != null) + params.put(name, val); + } + + // Add the parameters and the uploads inside the JSON representation of the job: + jsonJob.put(UWSJob.PARAM_PARAMETERS, params); + jsonJob.put("uwsUploads", uploads); + + // Add the job owner: jsonJob.put(UWSJob.PARAM_OWNER, (job != null && job.getOwner() != null) ? job.getOwner().getID() : null); + + // Add the name of the job list owning the given job: jsonJob.put("jobListName", jlName); + return jsonJob; } + /** + * Get the JSON representation of the given {@link UploadFile}. + * + * @param upl The uploaded file to serialize in JSON. + * + * @return Its JSON representation. + * + * @throws JSONException If there is an error while building the JSON object. + * + * @since 4.1 + */ + protected JSONObject getUploadJson(final UploadFile upl) throws JSONException{ + if (upl == null) + return null; + JSONObject o = new JSONObject(); + o.put("paramName", upl.paramName); + o.put("fileName", upl.fileName); + o.put("location", upl.getLocation()); + o.put("mime", upl.mimeType); + o.put("lenght", upl.length); + return o; + } + /* ******************* */ /* RESTORATION METHODS */ /* ******************* */ + @Override public int[] restoreAll(){ // Removes all current jobs from the UWS before restoring it from files: for(JobList jl : uws) @@ -564,7 +618,7 @@ public class DefaultUWSBackupManager implements UWSBackupManager { // Get the list of the input streams (on all the backup files to read): if (byUser){ if (!userIdentificationEnabled){ - getLogger().error("[restoration] Impossible to restore a UWS by user if the user identification is disabled (that's to say, the UWS has no UserIdentifier) !"); + getLogger().logUWS(LogLevel.ERROR, null, "RESTORATION", "Impossible to restore a UWS by user if the user identification is disabled (that's to say, the UWS has no UserIdentifier)!", null); return null; }else itInput = fileManager.getAllUserBackupInputs(); @@ -572,7 +626,7 @@ public class DefaultUWSBackupManager implements UWSBackupManager { try{ itInput = new SingleInputIterator(fileManager.getBackupInput()); }catch(IOException ioe){ - getLogger().error("[restoration] Restoration of the UWS " + uws.getName() + " failed because an unexpected IO error has occured.", ioe); + getLogger().logUWS(LogLevel.ERROR, null, "RESTORATION", "Restoration of the UWS " + uws.getName() + " failed because an unexpected IO error has occured.", ioe); return null; } } @@ -588,7 +642,7 @@ public class DefaultUWSBackupManager implements UWSBackupManager { HashMap users = new HashMap(); String key; - JSONObject object; + JSONObject object = null; try{ // Reads progressively the general structure (which is theoretically a JSON object): @@ -623,7 +677,7 @@ public class DefaultUWSBackupManager implements UWSBackupManager { } } }catch(UWSException ue){ - getLogger().error("[restoration] A job owner can not be restored !", ue); + getLogger().logUWS(LogLevel.ERROR, object, "RESTORATION", "A job owner can not be restored!", ue); //break; // Because, the key "user" is found ONLY in the backup file of a user. If the user can not be restored, its jobs won't be ! } @@ -649,7 +703,7 @@ public class DefaultUWSBackupManager implements UWSBackupManager { } } }catch(UWSException ue){ - getLogger().error("[restoration] The " + nbUsers + "-th user can not be restored !", ue); + getLogger().logUWS(LogLevel.ERROR, object, "RESTORATION", "The " + nbUsers + "-th user can not be restored!", ue); } } @@ -670,26 +724,26 @@ public class DefaultUWSBackupManager implements UWSBackupManager { if (restoreJob(object, users)) nbRestoredJobs++; }catch(UWSException ue){ - getLogger().error("[restoration] The " + nbJobs + "-th job can not be restored !", ue); + getLogger().logUWS(LogLevel.ERROR, object, "RESTORATION", "The " + nbJobs + "-th job can not be restored!", ue); } } }// any other key is ignore but with a warning message: else - getLogger().warning("[restoration] Key '" + key + "' ignored because unknown ! The UWS may be not completely restored !"); + getLogger().logUWS(LogLevel.WARNING, null, "RESTORATION", "Key '" + key + "' ignored because unknown! The UWS may be not completely restored.", null); } }catch(JSONException je){ - getLogger().error("[restoration] Incorrect JSON format for a UWS backup file !", je); + getLogger().logUWS(LogLevel.ERROR, null, "RESTORATION", "Incorrect JSON format for a UWS backup file!", je); return null; }catch(Exception e){ - getLogger().error("[restoration] Unexpected error while restoring the UWS !", e); + getLogger().logUWS(LogLevel.ERROR, null, "RESTORATION", "Unexpected error while restoring the UWS!", e); return null; }finally{ // Close the reader: try{ inputStream.close(); }catch(IOException ioe){ - getLogger().error("[restoration] Can not close the input stream opened on a user backup file !", ioe); + getLogger().logUWS(LogLevel.ERROR, null, "RESTORATION", "Can not close the input stream opened on a user backup file!", ioe); } // Set the last restoration date: lastRestoration = new Date(); @@ -697,11 +751,11 @@ public class DefaultUWSBackupManager implements UWSBackupManager { } if (!userIdentificationEnabled && nbUsers > 0) - getLogger().warning("[restoration] " + nbUsers + " job owners have not been restored because the user identification is disabled in this UWS ! => Jobs of these users have not been restored !"); + getLogger().logUWS(LogLevel.WARNING, null, "RESTORATION", nbUsers + " job owners have not been restored because the user identification is disabled in this UWS! => Jobs of these users have not been restored.", null); // Build the restoration report and log it: int[] report = new int[]{nbRestoredJobs,nbJobs,nbRestoredUsers,nbUsers}; - getLogger().uwsRestored(uws, report); + getLogger().logUWS(LogLevel.INFO, report, "RESTORED", "UWS restored!", null); return report; } @@ -734,13 +788,13 @@ public class DefaultUWSBackupManager implements UWSBackupManager { else userData.put(key, json.getString(key)); }catch(JSONException je){ - getLogger().error("[restoration] Incorrect JSON format for the serialization of the user " + ID + " !", je); + getLogger().logUWS(LogLevel.WARNING, null, "RESTORATION", "Incorrect JSON format for the serialization of the user \"" + ID + "\"! The restoration of this job may be incomplete.", je); } } // Check that the ID exists: if (ID == null || ID.trim().isEmpty()) - throw UWSExceptionFactory.restoreUserImpossible("Missing user ID !"); + throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, null, "Impossible to restore a user from the backup file(s): no ID has been found!"); return uws.getUserIdentifier().restoreUser(ID, pseudo, userData); } @@ -768,6 +822,7 @@ public class DefaultUWSBackupManager implements UWSBackupManager { //Map params = null; ArrayList results = null; ErrorSummary error = null; + JSONArray uploads = null; String[] keys = JSONObject.getNames(json); for(String key : keys){ @@ -806,35 +861,39 @@ public class DefaultUWSBackupManager implements UWSBackupManager { else if (key.equalsIgnoreCase(UWSJob.PARAM_DESTRUCTION_TIME)){ try{ tmp = json.getString(key); - inputParams.put(UWSJob.PARAM_DESTRUCTION_TIME, UWSJob.dateFormat.parse(tmp)); + inputParams.put(UWSJob.PARAM_DESTRUCTION_TIME, ISO8601Format.parseToDate(tmp)); }catch(ParseException pe){ - getLogger().error("[restoration] Incorrect date format for the '" + key + "' parameter !", pe); + getLogger().logUWS(LogLevel.ERROR, json, "RESTORATION", "Incorrect date format for the '" + key + "' parameter!", pe); } }// key=START_TIME: else if (key.equalsIgnoreCase(UWSJob.PARAM_START_TIME)){ tmp = json.getString(key); try{ - Date d = UWSJob.dateFormat.parse(tmp); + Date d = ISO8601Format.parseToDate(tmp); startTime = d.getTime(); }catch(ParseException pe){ - getLogger().error("[restoration] Incorrect date format for the '" + key + "' parameter !", pe); + getLogger().logUWS(LogLevel.ERROR, json, "RESTORATION", "Incorrect date format for the '" + key + "' parameter!", pe); } }// key=END_TIME: else if (key.equalsIgnoreCase(UWSJob.PARAM_END_TIME)){ tmp = json.getString(key); try{ - Date d = UWSJob.dateFormat.parse(tmp); + Date d = ISO8601Format.parseToDate(tmp); endTime = d.getTime(); }catch(ParseException pe){ - getLogger().error("[restoration] Incorrect date format for the '" + key + "' parameter !", pe); + getLogger().logUWS(LogLevel.ERROR, json, "RESTORATION", "Incorrect date format for the '" + key + "' parameter!", pe); } }// key=PARAMETERS: else if (key.equalsIgnoreCase(UWSJob.PARAM_PARAMETERS)) inputParams.put(UWSJob.PARAM_PARAMETERS, getParameters(json.getJSONObject(key))); + // key=uwsUploads: + else if (key.equalsIgnoreCase("uwsUploads")) + uploads = json.getJSONArray(key); + // key=RESULTS: else if (key.equalsIgnoreCase(UWSJob.PARAM_RESULTS)) results = getResults(json.getJSONArray(key)); @@ -845,24 +904,40 @@ public class DefaultUWSBackupManager implements UWSBackupManager { }// Ignore any other key but with a warning message: else - getLogger().warning("[restoration] The job attribute '" + key + "' has been ignored because unknown ! A job may be not completely restored !"); + getLogger().logUWS(LogLevel.WARNING, json, "RESTORATION", "The job attribute '" + key + "' has been ignored because unknown! A job may be not completely restored!", null); }catch(JSONException je){ - getLogger().error("[restoration] Incorrect JSON format for a job serialization (attribute: \"" + key + "\") !", je); + getLogger().logUWS(LogLevel.ERROR, json, "RESTORATION", "Incorrect JSON format for a job serialization (attribute: \"" + key + "\")!", je); + } + } + + // Re-Build all the uploaded files' pointers for this job: + if (uploads != null){ + @SuppressWarnings("unchecked") + Map params = (Map)(inputParams.get(UWSJob.PARAM_PARAMETERS)); + UploadFile upl; + try{ + for(int i = 0; i < uploads.length(); i++){ + upl = getUploadFile(uploads.getJSONObject(i));; + if (upl != null) + params.put(upl.paramName, upl); + } + }catch(JSONException je){ + getLogger().logUWS(LogLevel.ERROR, json, "RESTORATION", "Incorrect JSON format for the serialization of the job \"" + jobId + "\" (attribute: \"uwsUploads\")!", je); } } // The job list name is REQUIRED: if (jobListName == null || jobListName.isEmpty()) - getLogger().error("[restoration] Missing job list name ! => Can not restore the job " + jobId + " !"); + getLogger().logUWS(LogLevel.ERROR, json, "RESTORATION", "Missing job list name! => Can not restore the job " + jobId + "!", null); // The job list name MUST correspond to an existing job list: else if (uws.getJobList(jobListName) == null) - getLogger().error("[restoration] No job list named " + jobListName + " ! => Can not restore the job " + jobId + " !"); + getLogger().logUWS(LogLevel.ERROR, json, "RESTORATION", "No job list named " + jobListName + "! => Can not restore the job " + jobId + "!", null); // The job ID is REQUIRED: else if (jobId == null || jobId.isEmpty()) - getLogger().error("[restoration] Missing job ID ! => Can not restore a job !"); + getLogger().logUWS(LogLevel.ERROR, json, "RESTORATION", "Missing job ID! => Can not restore a job!", null); // Otherwise: the job can be created and restored: else{ @@ -871,7 +946,7 @@ public class DefaultUWSBackupManager implements UWSBackupManager { // If the specified user is unknown, display a warning and create the job without owner: if (ownerID != null && !ownerID.isEmpty() && owner == null){ - getLogger().error("[restoration] Unknown job owner: " + ownerID + " ! => Can not restore the job " + jobId + " !"); + getLogger().logUWS(LogLevel.ERROR, json, "RESTORATION", "Unknown job owner: " + ownerID + "! => Can not restore the job " + jobId + "!", null); return false; } @@ -880,7 +955,7 @@ public class DefaultUWSBackupManager implements UWSBackupManager { try{ uwsParams = uws.getFactory().createUWSParameters(inputParams); }catch(UWSException ue){ - getLogger().error("[restoration] Error with at least one of the UWS parameters to restore !", ue); + getLogger().logUWS(LogLevel.ERROR, json, "RESTORATION", "Error with at least one of the UWS parameters to restore!", ue); return false; } @@ -923,10 +998,8 @@ public class DefaultUWSBackupManager implements UWSBackupManager { * * @return The corresponding list of parameters * or null if the given object is empty. - * - * @throws UWSException */ - protected Map getParameters(final JSONObject obj) throws UWSException{ + protected Map getParameters(final JSONObject obj){ if (obj == null || obj.length() == 0) return null; @@ -936,12 +1009,37 @@ public class DefaultUWSBackupManager implements UWSBackupManager { try{ params.put(n, obj.get(n)); }catch(JSONException je){ - getLogger().error("Incorrect JSON format for the serialization of the parameter '" + n + "' !", je); + getLogger().logUWS(LogLevel.ERROR, obj, "RESTORATION", "Incorrect JSON format for the serialization of the parameter '" + n + "'!", je); } } return params; } + /** + * Build the upload file corresponding to the given JSON object. + * + * @param obj The JSON representation of the {@link UploadFile} to get. + * + * @return The corresponding {@link UploadFile}. + * + * @since 4.1 + */ + protected UploadFile getUploadFile(final JSONObject obj){ + try{ + UploadFile upl = new UploadFile(obj.getString("paramName"), (obj.has("fileName") ? obj.getString("fileName") : null), obj.getString("location"), uws.getFileManager()); + if (obj.has("mime")) + upl.mimeType = obj.getString("mime"); + try{ + if (obj.has("length")) + upl.length = Long.parseLong(obj.getString("length")); + }catch(NumberFormatException ex){} + return upl; + }catch(JSONException je){ + getLogger().logUWS(LogLevel.ERROR, obj, "RESTORATION", "Incorrect JSON format for the serialization of an uploaded file!", je); + return null; + } + } + /** * Builds the list of results corresponding to the given JSON array. * @@ -952,7 +1050,7 @@ public class DefaultUWSBackupManager implements UWSBackupManager { * * @throws UWSException If there is an error while restoring one of the result. * - * @see {@link #getResult(JSONObject)} + * @see #getResult(JSONObject) */ protected ArrayList getResults(final JSONArray array) throws UWSException{ if (array == null || array.length() == 0) @@ -965,7 +1063,7 @@ public class DefaultUWSBackupManager implements UWSBackupManager { if (r != null) results.add(r); }catch(JSONException je){ - getLogger().error("Incorrect JSON format for the serialization of the " + (i + 1) + "-th result !", je); + getLogger().logUWS(LogLevel.ERROR, array, "RESTORATION", "Incorrect JSON format for the serialization of the " + (i + 1) + "-th result!", je); } } @@ -1004,11 +1102,11 @@ public class DefaultUWSBackupManager implements UWSBackupManager { else if (n.equalsIgnoreCase("size")) size = obj.getLong(n); else - getLogger().warning("[restoration] The result parameter '" + n + "' has been ignored because unknown ! A result may be not completely restored !"); + getLogger().logUWS(LogLevel.WARNING, obj, "RESTORATION", "The result parameter '" + n + "' has been ignored because unknown! A result may be not completely restored!", null); } if (id == null){ - getLogger().error("[restoration] Missing result ID ! => A result can not be restored !"); + getLogger().logUWS(LogLevel.ERROR, obj, "RESTORATION", "Missing result ID! => A result can not be restored!", null); return null; }else{ Result r = new Result(id, type, href, redirection); @@ -1044,13 +1142,13 @@ public class DefaultUWSBackupManager implements UWSBackupManager { else if (n.equalsIgnoreCase("message")) message = obj.getString(n); else - getLogger().warning("[restoration] The error attribute '" + n + "' has been ignored because unknown ! => An error summary may be not completely restored !"); + getLogger().logUWS(LogLevel.WARNING, obj, "RESTORATION", "The error attribute '" + n + "' has been ignored because unknown! => An error summary may be not completely restored!", null); }catch(JSONException je){ - getLogger().error("Incorrect JSON format for an error serialization !", je); + getLogger().logUWS(LogLevel.ERROR, obj, "RESTORATION", "Incorrect JSON format for an error serialization!", je); } } if (message != null) - return new ErrorSummary(message, ErrorType.valueOf(type), details); + return new ErrorSummary(message, ErrorType.valueOf(type.toUpperCase()), details); else return null; } @@ -1199,7 +1297,7 @@ public class DefaultUWSBackupManager implements UWSBackupManager { readNext(); return nextKey; }catch(JSONException je){ - logger.error("Incorrect JSON format in an object !", je); + logger.logUWS(LogLevel.ERROR, null, "RESTORATION", "Incorrect JSON format in an object!", je); endReached = true; return null; } @@ -1296,7 +1394,7 @@ public class DefaultUWSBackupManager implements UWSBackupManager { try{ readNext(); }catch(JSONException je){ - logger.error("Incorrect JSON format in an Array !", je); + logger.logUWS(LogLevel.ERROR, null, "RESTORATION", "Incorrect JSON format in an Array!", je); endReached = true; nextObj = null; } diff --git a/src/uws/service/backup/UWSBackupManager.java b/src/uws/service/backup/UWSBackupManager.java index 52ae168..6e50956 100644 --- a/src/uws/service/backup/UWSBackupManager.java +++ b/src/uws/service/backup/UWSBackupManager.java @@ -32,7 +32,7 @@ public interface UWSBackupManager { /** * Enables/Disables the backup of the associated UWS. * - * @param enableBackup true to enable the backup, false otherwise. + * @param enabled true to enable the backup, false otherwise. */ public void setEnabled(final boolean enabled); diff --git a/src/uws/service/error/AbstractServiceErrorWriter.java b/src/uws/service/error/AbstractServiceErrorWriter.java deleted file mode 100644 index a71b45c..0000000 --- a/src/uws/service/error/AbstractServiceErrorWriter.java +++ /dev/null @@ -1,291 +0,0 @@ -package uws.service.error; - -import java.io.IOException; -import java.io.PrintWriter; -import java.util.ArrayList; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.json.JSONException; -import org.json.JSONWriter; - -import uws.AcceptHeader; -import uws.job.ErrorType; -import uws.job.serializer.UWSSerializer; -import uws.job.user.JobOwner; -import uws.service.log.UWSLog; - -/** - *

    Abstract implementation of the {@link ServiceErrorWriter} interface.

    - * - *

    - * The only abstract method is the function {@link #getLogger()}. It MUST return a NON-NULL logger. - * The other functions ({@link #writeError(Throwable, HttpServletResponse, HttpServletRequest, JobOwner, String)} - * and {@link #writeError(String, ErrorType, int, HttpServletResponse, HttpServletRequest, JobOwner, String)}) have - * already a default implementation but may be overridden if needed. Both of them call the function - * {@link #formatError(Throwable, boolean, ErrorType, int, String, JobOwner, HttpServletResponse)} - * to format and write the error in the given {@link HttpServletResponse} in the HTML format with - * the appropriate HTTP error code. The (full) stack trace of the error may be printed if asked. - *

    - * - *

    2 formats are managed by this implementation: HTML (default) and JSON. That means the writer will format and - * write a given error in the best appropriate format. This format is chosen thanks to the "Accept" header of the HTTP request. - * If no request is provided or if there is no known format, the HTML format is chosen by default.

    - * - * @author Grégory Mantelet (CDS) - * @version 06/2012 - */ -public abstract class AbstractServiceErrorWriter implements ServiceErrorWriter { - - protected final String[] managedFormats = new String[]{"application/json","json","text/json","text/html","html"}; - - /** - * Logger to use to display the given errors in the appropriate log files. - * @return A NON-NULL and VALID logger. - */ - protected abstract UWSLog getLogger(); - - @Override - public void writeError(Throwable t, HttpServletResponse response, HttpServletRequest request, JobOwner user, String action) throws IOException{ - if (t != null && response != null){ - formatError(t, true, ErrorType.FATAL, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, action, user, response, (request != null) ? request.getHeader("Accept") : null); - getLogger().error(t); - String errorMsg = t.getMessage(); - if (errorMsg == null || errorMsg.trim().isEmpty()) - errorMsg = t.getClass().getName() + " (no error message)"; - getLogger().httpRequest(request, user, action, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, errorMsg, t); - } - } - - @Override - public void writeError(String message, ErrorType type, int httpErrorCode, HttpServletResponse response, HttpServletRequest request, JobOwner user, String action) throws IOException{ - if (message != null && response != null){ - formatError(new Exception(message), false, type, httpErrorCode, action, user, response, (request != null) ? request.getHeader("Accept") : null); - getLogger().httpRequest(request, user, action, httpErrorCode, message, null); - } - } - - /** - * Parses the header "Accept", splits it in a list of MIME type and compare each one to each managed formats ({@link #managedFormats}). - * If there is a match (not case sensitive), return the corresponding managed format immediately. - * - * @param acceptHeader The header item named "Accept" (which lists all expected response formats). - * @return The first format common to the "Accept" header and the managed formats of this writer. - */ - protected final String chooseFormat(final String acceptHeader){ - if (acceptHeader != null && !acceptHeader.trim().isEmpty()){ - // Parse the given MIME types list: - AcceptHeader accept = new AcceptHeader(acceptHeader); - ArrayList lstMimeTypes = accept.getOrderedMimeTypes(); - for(String acceptedFormat : lstMimeTypes){ - for(String f : managedFormats){ - if (acceptedFormat.equalsIgnoreCase(f)) - return f; - } - } - } - return null; - } - - /** - *

    Formats and writes the given error in the HTTP servlet response.

    - *

    The format is chosen thanks to the Accept header of the HTTP request. - * If unknown, the HTML output is chosen.

    - * - * @param t Exception to format and to write. - * @param printStackTrace true to print the (full) stack trace, false otherwise. - * @param type Type of the error: FATAL or TRANSIENT. - * @param httpErrorCode HTTP error code (i.e. 404, 500). - * @param action Action which generates the error note: displayed only if not NULL and not empty. - * @param user User which is at the origin of the request/action which generates the error. - * @param response Response in which the error must be written. - * @param acceptHeader Value of the header named "Accept" (which lists all allowed response format). - * - * @throws IOException If there is an error while writing the given exception. - * - * @see #formatHTMLError(Throwable, boolean, ErrorType, int, String, JobOwner, HttpServletResponse) - * @see #formatJSONError(Throwable, boolean, ErrorType, int, String, JobOwner, HttpServletResponse) - */ - protected void formatError(final Throwable t, final boolean printStackTrace, final ErrorType type, final int httpErrorCode, final String action, final JobOwner user, final HttpServletResponse response, final String acceptHeader) throws IOException{ - // Reset the whole response to ensure the output stream is free: - if (response.isCommitted()) - return; - response.reset(); - - String format = chooseFormat(acceptHeader); - if (format != null && (format.equalsIgnoreCase("application/json") || format.equalsIgnoreCase("text/json") || format.equalsIgnoreCase("json"))) - formatJSONError(t, printStackTrace, type, httpErrorCode, action, user, response); - else - formatHTMLError(t, printStackTrace, type, httpErrorCode, action, user, response); - } - - /** - *

    Formats and writes the given error in the HTTP servlet response.

    - *

    A full HTML response is printed with: the HTTP error code, the error type, the name of the exception, the message and the full stack trace.

    - * - * @param t Exception to format and to write. - * @param printStackTrace true to print the (full) stack trace, false otherwise. - * @param type Type of the error: FATAL or TRANSIENT. - * @param httpErrorCode HTTP error code (i.e. 404, 500). - * @param action Action which generates the error note: displayed only if not NULL and not empty. - * @param user User which is at the origin of the request/action which generates the error. - * @param response Response in which the error must be written. - * - * @throws IOException If there is an error while writing the given exception. - */ - protected void formatHTMLError(final Throwable t, final boolean printStackTrace, final ErrorType type, final int httpErrorCode, final String action, final JobOwner user, final HttpServletResponse response) throws IOException{ - // Set the HTTP status code and the content type of the response: - response.setStatus(httpErrorCode); - response.setContentType(UWSSerializer.MIME_TYPE_HTML); - - PrintWriter out = response.getWriter(); - - // Header: - out.println("\n\t"); - out.println("\t\t"); - out.println("\t\t"); - out.println("\t\tSERVICE ERROR"); - out.println("\t\n\t"); - - // Title: - String errorColor = (type == ErrorType.FATAL) ? "red" : "orange"; - out.println("\t\t

    SERVICE ERROR - " + httpErrorCode + "

    "); - - // Description part: - out.println("\t\t

    Description

    "); - out.println("\t\t
      "); - out.println("\t\t\t
    • Type: " + type + "
    • "); - if (action != null && !action.trim().isEmpty()) - out.println("\t\t\t
    • Action: " + action + "
    • "); - String context = null; - String msg = t.getMessage(); - if (msg != null && !msg.trim().isEmpty()){ - int start = msg.indexOf("["), end = msg.indexOf("]"); - if (start >= 0 && start < end){ - context = msg.substring(start + 1, end); - msg = msg.substring(end + 1); - } - }else - msg = ""; - if (context != null) - out.println("\t\t\t
    • Context: " + context + "
    • "); - if (printStackTrace) - out.println("\t\t\t
    • Exception: " + t.getClass().getName() + "
    • "); - out.println("\t\t\t
    • Message:

      " + msg + "

    • "); - out.println("\t\t
    "); - - // Stack trace part: - if (printStackTrace){ - out.println("\t\t

    Stack trace

    "); - Throwable cause = t; - do{ - out.println("\t\t"); - out.println("\t\t\t"); - StackTraceElement[] trace = cause.getStackTrace(); - for(int i = 0; i < trace.length; i++) - out.println("\t\t\t"); - out.println("\t\t
    ClassMethodLine
    " + trace[i].getClassName() + "" + trace[i].getMethodName() + "" + trace[i].getLineNumber() + "
    "); - - // Print the stack trace of the "next" error: - cause = cause.getCause(); - if (cause != null){ - out.println("\t\t

    Caused by " + cause.getClass().getName() + ":

    "); - out.println("\t\t

    " + cause.getMessage() + "

    "); - } - }while(cause != null); - } - - out.println("\t\n"); - out.close(); - } - - /** - *

    Formats and writes the given error in the HTTP servlet response.

    - *

    A JSON response is printed with: the HTTP error code, the error type, the name of the exception, the message and the list of all causes' message.

    - * - * @param t Exception to format and to write. - * @param printStackTrace true to print the (full) stack trace, false otherwise. - * @param type Type of the error: FATAL or TRANSIENT. - * @param httpErrorCode HTTP error code (i.e. 404, 500). - * @param action Action which generates the error note: displayed only if not NULL and not empty. - * @param user User which is at the origin of the request/action which generates the error. - * @param response Response in which the error must be written. - * - * @throws IOException If there is an error while writing the given exception. - */ - protected void formatJSONError(final Throwable t, final boolean printStackTrace, final ErrorType type, final int httpErrorCode, final String action, final JobOwner user, final HttpServletResponse response) throws IOException{ - // Set the HTTP status code and the content type of the response: - response.setStatus(httpErrorCode); - response.setContentType(UWSSerializer.MIME_TYPE_JSON); - - PrintWriter out = response.getWriter(); - try{ - JSONWriter json = new JSONWriter(out); - - json.object(); - json.key("errorcode").value(httpErrorCode); - json.key("errortype").value(type.toString()); - json.key("action").value(action); - - String context = null; - String msg = t.getMessage(); - if (msg != null && !msg.trim().isEmpty()){ - int start = msg.indexOf("["), end = msg.indexOf("]"); - if (start >= 0 && start < end){ - context = msg.substring(start + 1, end); - msg = msg.substring(end + 1); - } - }else - msg = ""; - if (context != null) - json.key("context").value(context); - if (printStackTrace) - json.key("exception").value(t.getClass().getName()); - json.key("message").value(msg); - - // Stack trace part: - if (printStackTrace){ - json.key("cause").array(); - Throwable cause = t; - do{ - json.object(); - json.key("exception").value(cause.getClass().getName()); - json.key("stacktrace").array(); - StackTraceElement[] trace = cause.getStackTrace(); - for(int i = 0; i < trace.length; i++){ - json.object(); - json.key("class").value(trace[i].getClassName()); - json.key("method").value(trace[i].getMethodName()); - json.key("line").value(trace[i].getLineNumber()); - json.endObject(); - } - json.endArray().endObject(); - - // Print the stack trace of the "next" error: - cause = cause.getCause(); - }while(cause != null); - json.endArray(); - } - - json.endObject(); - }catch(JSONException je){ - getLogger().error("Impossible to format/write an error in JSON !", je); - throw new IOException("Error while formatting the error in JSON !", je); - }finally{ - out.flush(); - out.close(); - } - } - -} diff --git a/src/uws/service/error/DefaultUWSErrorWriter.java b/src/uws/service/error/DefaultUWSErrorWriter.java index 82529f6..71ec87e 100644 --- a/src/uws/service/error/DefaultUWSErrorWriter.java +++ b/src/uws/service/error/DefaultUWSErrorWriter.java @@ -1,58 +1,341 @@ package uws.service.error; +/* + * This file is part of UWSLibrary. + * + * UWSLibrary is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * UWSLibrary is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with UWSLibrary. If not, see . + * + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) + */ + +import java.io.BufferedWriter; import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.util.ArrayList; + import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.json.JSONException; +import org.json.JSONWriter; + +import tap.TAPException; +import uws.AcceptHeader; import uws.UWSException; import uws.UWSToolBox; +import uws.job.ErrorSummary; +import uws.job.ErrorType; +import uws.job.UWSJob; +import uws.job.serializer.UWSSerializer; import uws.job.user.JobOwner; -import uws.service.UWS; import uws.service.log.UWSLog; +import uws.service.log.UWSLog.LogLevel; /** - *

    Default implementation of {@link ServiceErrorWriter} for a UWS service.

    - * - *

    All errors are written using the function {@link #formatError(Throwable, boolean, uws.job.ErrorType, int, String, JobOwner, HttpServletResponse)} - * of the abstract implementation of the error writer: {@link AbstractServiceErrorWriter}.

    + *

    Default implementation of a {@link ServiceErrorWriter} interface for a UWS service.

    * - *

    A {@link UWSException} may precise the HTTP error code to apply. That's why, {@link #writeError(Throwable, HttpServletResponse, HttpServletRequest, JobOwner, String)} - * has been overridden: to get this error code and submit it to the {@link #formatError(Throwable, boolean, uws.job.ErrorType, int, String, JobOwner, HttpServletResponse)} - * function. Besides, the stack trace of {@link UWSException}s is not printed (except if the message is NULL or empty). - * And this error will be logged only if its error code is {@link UWSException#INTERNAL_SERVER_ERROR}.

    + *

    + * All errors are written using the function {@link #formatError(String, ErrorType, int, String, String, JobOwner, HttpServletResponse, String)} + * in order to format the error in the most appropriate format. 2 formats are managed by default by this implementation: HTML (default) and JSON. + * This format is chosen thanks to the "Accept" header of the HTTP request. If no request is provided or if there is no known format, + * the HTML format is chosen by default. + *

    * - *

    2 formats are managed by this implementation: HTML (default) and JSON. That means the writer will format and - * write a given error in the best appropriate format. This format is chosen thanks to the "Accept" header of the HTTP request. - * If no request is provided or if there is no known format, the HTML format is chosen by default.

    + *

    + * {@link UWSException}s may precise the HTTP error code to apply, + * which will be used to set the HTTP status of the response. If it is a different kind of exception, + * the HTTP status 500 (INTERNAL SERVER ERROR) will be used. + *

    * - * @author Grégory Mantelet (CDS) - * @version 06/2012 + *

    + * Besides, all exceptions except {@link UWSException} and {@link TAPException} will be logged as FATAL in the TAP context + * (with no event and no object). Thus the full stack trace is available to the administrator so that the error can + * be understood as easily and quickly as possible. + * The stack trace is no longer displayed to the user. + *

    * - * @see AbstractServiceErrorWriter + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (04/2015) */ -public class DefaultUWSErrorWriter extends AbstractServiceErrorWriter { +public class DefaultUWSErrorWriter implements ServiceErrorWriter { - protected final UWS uws; + /** List of all managed output formats. */ + protected final String[] managedFormats = new String[]{"application/json","json","text/json","text/html","html"}; - public DefaultUWSErrorWriter(final UWS uws){ - this.uws = uws; - } + /** Logger to use when grave error must be logged or if a JSON error occurs. */ + protected final UWSLog logger; - @Override - protected final UWSLog getLogger(){ - return (uws != null && uws.getLogger() != null) ? uws.getLogger() : UWSToolBox.getDefaultLogger(); + /** + * Build an error writer which will log any error in response of an HTTP request. + * + * @param logger Object to use to log errors. + */ + public DefaultUWSErrorWriter(final UWSLog logger){ + if (logger == null) + throw new NullPointerException("Missing logger! Can not write a default error writer without."); + + this.logger = logger; } @Override - public void writeError(Throwable t, HttpServletResponse response, HttpServletRequest request, JobOwner user, String action) throws IOException{ + public boolean writeError(Throwable t, HttpServletResponse response, HttpServletRequest request, String reqID, JobOwner user, String action){ + if (t == null || response == null) + return true; + + boolean written = false; + // If expected error, just write it: if (t instanceof UWSException){ UWSException ue = (UWSException)t; - formatError(ue, (ue.getMessage() == null || ue.getMessage().trim().isEmpty()), ue.getUWSErrorType(), ue.getHttpErrorCode(), action, user, response, request.getHeader("Accept")); - if (ue.getHttpErrorCode() == UWSException.INTERNAL_SERVER_ERROR) - getLogger().error(ue); - getLogger().httpRequest(request, user, action, ue.getHttpErrorCode(), ue.getMessage(), ue); - }else - super.writeError(t, response, request, user, action); + written = writeError(ue.getMessage(), ue.getUWSErrorType(), ue.getHttpErrorCode(), response, request, reqID, user, action); + } + // Otherwise, log it and write a message to the user: + else{ + // log the error as GRAVE/FATAL (because unexpected/unmanaged): + logger.logUWS(LogLevel.FATAL, null, null, "[REQUEST N°" + reqID + "] " + t.getMessage(), t); + // write a message to the user: + written = writeError("INTERNAL SERVER ERROR! Sorry, this error is unexpected and no explanation can be provided for the moment. Details about this error have been reported in the service log files ; you should try again your request later or notify the administrator(s) by yourself (with the following 'Request ID').", ErrorType.FATAL, UWSException.INTERNAL_SERVER_ERROR, response, request, reqID, user, action); + } + return written; + } + + @Override + public boolean writeError(String message, ErrorType type, int httpErrorCode, HttpServletResponse response, HttpServletRequest request, String reqID, JobOwner user, String action){ + if (message == null || response == null) + return true; + + try{ + // Just format and write the error message: + formatError(message, type, httpErrorCode, reqID, action, user, response, (request != null) ? request.getHeader("Accept") : null); + return true; + }catch(IllegalStateException ise){ + return false; + }catch(IOException ioe){ + return false; + } + } + + @Override + public void writeError(Throwable t, ErrorSummary error, UWSJob job, OutputStream output) throws IOException{ + UWSToolBox.writeErrorFile((t instanceof Exception) ? (Exception)t : new UWSException(t), error, job, output); + } + + @Override + public String getErrorDetailsMIMEType(){ + return "text/plain"; + } + + /** + * Parses the header "Accept", splits it in a list of MIME type and compare each one to each managed formats ({@link #managedFormats}). + * If there is a match (not case sensitive), return the corresponding managed format immediately. + * + * @param acceptHeader The header item named "Accept" (which lists all expected response formats). + * @return The first format common to the "Accept" header and the managed formats of this writer. + */ + protected final String chooseFormat(final String acceptHeader){ + if (acceptHeader != null && !acceptHeader.trim().isEmpty()){ + // Parse the given MIME types list: + AcceptHeader accept = new AcceptHeader(acceptHeader); + ArrayList lstMimeTypes = accept.getOrderedMimeTypes(); + for(String acceptedFormat : lstMimeTypes){ + for(String f : managedFormats){ + if (acceptedFormat.equalsIgnoreCase(f)) + return f; + } + } + } + return null; + } + + /** + *

    Formats and writes the given error in the HTTP servlet response.

    + *

    The format is chosen thanks to the Accept header of the HTTP request. + * If unknown, the HTML output is chosen.

    + * + * @param message Error message to write. + * @param type Type of the error: FATAL or TRANSIENT. + * @param httpErrorCode HTTP error code (i.e. 404, 500). + * @param reqID ID of the request at the origin of the specified error. + * @param action Action which generates the error note: displayed only if not NULL and not empty. + * @param user User which is at the origin of the request/action which generates the error. + * @param response Response in which the error must be written. + * @param acceptHeader Value of the header named "Accept" (which lists all allowed response format). + * + * @throws IOException If there is an error while writing the given exception. + * + * @see #formatHTMLError(String, ErrorType, int, String, String, JobOwner, HttpServletResponse) + * @see #formatJSONError(String, ErrorType, int, String, String, JobOwner, HttpServletResponse) + */ + protected void formatError(final String message, final ErrorType type, final int httpErrorCode, final String reqID, final String action, final JobOwner user, final HttpServletResponse response, final String acceptHeader) throws IOException{ + String format = chooseFormat(acceptHeader); + if (format != null && (format.equalsIgnoreCase("application/json") || format.equalsIgnoreCase("text/json") || format.equalsIgnoreCase("json"))) + formatJSONError(message, type, httpErrorCode, reqID, action, user, response); + else + formatHTMLError(message, type, httpErrorCode, reqID, action, user, response); + } + + /** + *

    Formats and writes the given error in the HTTP servlet response.

    + *

    A full HTML response is printed with: the HTTP error code, the error type, the name of the exception, the message and the full stack trace.

    + * + * @param message Error message to write. + * @param type Type of the error: FATAL or TRANSIENT. + * @param httpErrorCode HTTP error code (i.e. 404, 500). + * @param reqID ID of the request at the origin of the specified error. + * @param action Action which generates the error note: displayed only if not NULL and not empty. + * @param user User which is at the origin of the request/action which generates the error. + * @param response Response in which the error must be written. + * + * @throws IOException If there is an error while writing the given exception. + */ + protected void formatHTMLError(final String message, final ErrorType type, final int httpErrorCode, final String reqID, final String action, final JobOwner user, final HttpServletResponse response) throws IOException{ + try{ + // Erase anything written previously in the HTTP response: + response.reset(); + + // Set the HTTP status: + response.setStatus(httpErrorCode); + + // Set the MIME type of the answer (XML for a VOTable document): + response.setContentType(UWSSerializer.MIME_TYPE_HTML); + + // Set the character encoding: + response.setCharacterEncoding(UWSToolBox.DEFAULT_CHAR_ENCODING); + + }catch(IllegalStateException ise){ + /* If it is not possible any more to reset the response header and body, + * the error is anyway written in order to corrupt the HTTP response. + * Thus, it will be obvious that an error occurred and the result is + * incomplete and/or wrong.*/ + } + + PrintWriter out; + try{ + out = response.getWriter(); + }catch(IllegalStateException ise){ + /* This exception may occur just because either the writer or + * the output-stream can be used (because already got before). + * So, we just have to get the output-stream if getting the writer + * throws an error.*/ + out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(response.getOutputStream()))); + } + + // Header: + out.println("\n\t"); + out.println("\t\t"); + out.println("\t\t"); + out.println("\t\tSERVICE ERROR"); + out.println("\t\n\t"); + + // Title: + String errorColor = (type == ErrorType.FATAL) ? "red" : "orange"; + out.println("\t\t

    SERVICE ERROR - " + httpErrorCode + "

    "); + + // Description part: + out.println("\t\t

    Description

    "); + out.println("\t\t
      "); + out.println("\t\t\t
    • Type: " + type + "
    • "); + if (reqID != null) + out.println("\t\t\t
    • Request ID: " + reqID + "
    • "); + if (action != null) + out.println("\t\t\t
    • Action: " + action + "
    • "); + out.println("\t\t\t
    • Message:

      " + message + "

    • "); + out.println("\t\t
    "); + + out.println("\t\n"); + + out.flush(); + } + + /** + *

    Formats and writes the given error in the HTTP servlet response.

    + *

    A JSON response is printed with: the HTTP error code, the error type, the name of the exception, the message and the list of all causes' message.

    + * + * @param message Error message to write. + * @param type Type of the error: FATAL or TRANSIENT. + * @param httpErrorCode HTTP error code (i.e. 404, 500). + * @param reqID ID of the request at the origin of the specified error. + * @param action Action which generates the error note: displayed only if not NULL and not empty. + * @param user User which is at the origin of the request/action which generates the error. + * @param response Response in which the error must be written. + * + * @throws IOException If there is an error while writing the given exception. + */ + protected void formatJSONError(final String message, final ErrorType type, final int httpErrorCode, final String reqID, final String action, final JobOwner user, final HttpServletResponse response) throws IOException{ + try{ + // Erase anything written previously in the HTTP response: + response.reset(); + + // Set the HTTP status: + response.setStatus(httpErrorCode); + + // Set the MIME type of the answer (JSON): + response.setContentType(UWSSerializer.MIME_TYPE_JSON); + + // Set the character encoding: + response.setCharacterEncoding(UWSToolBox.DEFAULT_CHAR_ENCODING); + + }catch(IllegalStateException ise){ + /* If it is not possible any more to reset the response header and body, + * the error is anyway written in order to corrupt the HTTP response. + * Thus, it will be obvious that an error occurred and the result is + * incomplete and/or wrong.*/ + } + + PrintWriter out; + try{ + out = response.getWriter(); + }catch(IllegalStateException ise){ + /* This exception may occur just because either the writer or + * the output-stream can be used (because already got before). + * So, we just have to get the output-stream if getting the writer + * throws an error.*/ + out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(response.getOutputStream()))); + } + + try{ + JSONWriter json = new JSONWriter(out); + + json.object(); + json.key("errorcode").value(httpErrorCode); + json.key("errortype").value(type.toString()); + if (reqID != null) + json.key("requestid").value(reqID); + if (action != null) + json.key("action").value(action); + json.key("message").value(message); + + json.endObject(); + + out.flush(); + + }catch(JSONException je){ + logger.logUWS(LogLevel.ERROR, null, "FORMAT_ERROR", "Impossible to format/write an error in JSON!", je); + throw new IOException("Error while formatting the error in JSON!", je); + } } } diff --git a/src/uws/service/error/ServiceErrorWriter.java b/src/uws/service/error/ServiceErrorWriter.java index 9c19a50..04c2936 100644 --- a/src/uws/service/error/ServiceErrorWriter.java +++ b/src/uws/service/error/ServiceErrorWriter.java @@ -1,47 +1,137 @@ package uws.service.error; +/* + * This file is part of UWSLibrary. + * + * UWSLibrary is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * UWSLibrary is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with UWSLibrary. If not, see . + * + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) + */ + import java.io.IOException; +import java.io.OutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import uws.job.ErrorSummary; import uws.job.ErrorType; +import uws.job.UWSJob; import uws.job.user.JobOwner; /** - * Let's writing/formatting any Exception/Throwable in a {@link HttpServletResponse}. + * Let's writing/formatting any Exception/Throwable in an {@link HttpServletResponse} or in an error summary. * - * @author Grégory Mantelet (CDS) - * @version 06/2012 + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (04/2015) */ public interface ServiceErrorWriter { /** - * Writes the given exception in the given response. + *

    Write the given exception in the given response.

    + * + *

    Note: + * If this function is called without at least an exception and an HTTP response, nothing should be done. + * No error may be thrown. + *

    * - * @param t Exception to write/format. - * @param response Response in which the given exception must be written. - * @param request Request at the origin of the error (MAY BE NULL). - * @param user User which sends the given request (which generates the error) (MAY BE NULL). - * @param action Type/Name of the action which generates the error (MAY BE NULL). + *

    IMPORTANT: + * If any {@link IOException} occurs while writing the error in the given {@link HttpServletResponse} output stream, + * this function should stop and return false. In such case, the error which was supposed to be written + * may be logged. + *

    * - * @throws IOException If there is an error while writing the response. + * @param t Exception to write/format. + * @param response Response in which the given exception must be written. + * @param request Request at the origin of the error (MAY BE NULL). + * @param reqID ID of the request (which let the user and the administrator identify the failed request). (MAY BE NULL if the request is not provided) + * @param user User which sends the given request (which generates the error) (MAY BE NULL). + * @param action Type/Name of the action which generates the error (MAY BE NULL). + * + * @return true if the given error message has been successfully written in the given {@link HttpServletResponse}, + * false otherwise. */ - public void writeError(final Throwable t, final HttpServletResponse response, final HttpServletRequest request, final JobOwner user, final String action) throws IOException; + public boolean writeError(final Throwable t, final HttpServletResponse response, final HttpServletRequest request, final String reqID, final JobOwner user, final String action); /** - * Writes the described error in the given response. + *

    Write the described error in the given response.

    + * + *

    Note: + * If this function is called without at least a message and an HTTP response, nothing should be done. + * No error may be thrown. + *

    + * + *

    IMPORTANT: + * If any {@link IOException} occurs while writing the error in the given {@link HttpServletResponse} output stream, + * this function should stop and return false. In such case, the error which was supposed to be written + * may be logged. + *

    * * @param message Message to display. * @param type Type of the error: FATAL or TRANSIENT. * @param httpErrorCode HTTP error code (i.e. 404, 500). * @param response Response in which the described error must be written. * @param request Request which causes this error. + * @param reqID ID of the request (which let the user and the administrator identify the failed request). * @param user User which sends the HTTP request. * @param action Action corresponding to the given request. * - * @throws IOException If there is an error while writing the response. + * @return true if the given error message has been successfully written in the given {@link HttpServletResponse}, + * false otherwise. + */ + public boolean writeError(final String message, final ErrorType type, final int httpErrorCode, final HttpServletResponse response, final HttpServletRequest request, final String reqID, final JobOwner user, final String action); + + /** + *

    Write the given error in the given output stream.

    + * + *

    + * This function is used only for the error summary of a job (that's to say to report in the + * ../error/details parameter any error which occurs while executing a job). + *

    + * + *

    Important note: + * The error details written in the given output MUST always have the same MIME type. + * This latter MUST be returned by {@link #getErrorDetailsMIMEType()}. + *

    + * + * @param t Error to write. If error is not null, it will be displayed instead of the message of this throwable. + * @param error Summary of the error. It may particularly contain a message different from the one of the given exception. In this case, it will displayed instead of the exception's message. + * @param job The job which fails. + * @param output Stream in which the error must be written. + * + * @throws IOException If there an error while writing the error in the given stream. + * + * @see #getErrorDetailsMIMEType() + * + * @since 4.1 + */ + public void writeError(final Throwable t, final ErrorSummary error, final UWSJob job, final OutputStream output) throws IOException; + + /** + *

    Get the MIME type of the error details written by {@link #writeError(Throwable, ErrorSummary, UWSJob, OutputStream)} in the error summary.

    + * + *

    Important note: + * If NULL is returned, the MIME type will be considered as text/plain. + *

    + * + * @return MIME type of the error details document. If NULL, it will be considered as text/plain. + * + * @see #writeError(Throwable, ErrorSummary, UWSJob, OutputStream) + * + * @since 4.1 */ - public void writeError(final String message, final ErrorType type, final int httpErrorCode, final HttpServletResponse response, final HttpServletRequest request, final JobOwner user, final String action) throws IOException; + public String getErrorDetailsMIMEType(); } diff --git a/src/uws/service/file/EventFrequency.java b/src/uws/service/file/EventFrequency.java new file mode 100644 index 0000000..a2c11f9 --- /dev/null +++ b/src/uws/service/file/EventFrequency.java @@ -0,0 +1,498 @@ +package uws.service.file; + +/* + * This file is part of UWSLibrary. + * + * UWSLibrary is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * UWSLibrary is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with UWSLibrary. If not, see . + * + * Copyright 2014-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) + */ + +import java.text.DateFormat; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.Scanner; +import java.util.regex.MatchResult; + +/** + *

    Let interpret and computing a frequency.

    + * + *

    Frequency syntax

    + * + *

    The frequency is expressed as a string at initialization of this object. This string must respect the following syntax:

    + *
      + *
    • 'D' hh mm : daily schedule at hh:mm
    • + *
    • 'W' dd hh mm : weekly schedule at the given day of the week (1:sunday, 2:monday, ..., 7:saturday) at hh:mm
    • + *
    • 'M' dd hh mm : monthly schedule at the given day of the month at hh:mm
    • + *
    • 'h' mm : hourly schedule at the given minute
    • + *
    • 'm' : scheduled every minute (for completness :-))
    • + *
    + *

    Where: hh = integer between 0 and 23, mm = integer between 0 and 59, dd (for 'W') = integer between 1 and 7 (1:sunday, 2:monday, ..., 7:saturday), + * dd (for 'M') = integer between 1 and 31.

    + * + *

    Warning: + * The frequency type is case sensitive! Then you should particularly pay attention at the case + * when using the frequency types 'M' (monthly) and 'm' (every minute). + *

    + * + *

    + * Parsing errors are not thrown but "resolved" silently. The "solution" depends of the error. + * 2 cases of errors are considered: + *

    + *
      + *
    • Frequency type mismatch: It happens when the first character is not one of the expected (D, W, M, h, m). + * That means: bad case (i.e. 'd' rather than 'D'), another character. + * In this case, the frequency will be: daily at 00:00.
    • + * + *
    • Parameter(s) missing or incorrect: With the "daily" frequency ('D'), at least 2 parameters must be provided ; + * 3 for "weekly" ('W') and "monthly" ('M') ; only 1 for "hourly" ('h') ; none for "every minute" ('m'). + * This number of parameters is a minimum: only the n first parameters will be considered while + * the others will be ignored. + * If this minimum number of parameters is not respected or if a parameter value is incorrect, + * all parameters will be set to their default value + * (which is 0 for all parameter except dd for which it is 1).
    • + *
    + * + *

    Examples:

    + *
      + *
    • "" or NULL = every day at 00:00
    • + *
    • "D 06 30" or "D 6 30" = every day at 06:30
    • + *
    • "D 24 30" = every day at 00:00, because hh must respect the rule: 0 ≤ hh ≤ 23
    • + *
    • "d 06 30" or "T 06 30" = every day at 00:00, because the frequency type "d" (lower case of "D") or "T" do not exist
    • + *
    • "W 2 6 30" = every week on Tuesday at 06:30
    • + *
    • "W 8 06 30" = every week on Sunday at 00:00, because with 'W' dd must respect the rule: 1 ≤ dd ≤ 7
    • + *
    • "M 2 6 30" = every month on the 2nd at 06:30
    • + *
    • "M 32 6 30" = every month on the 1st at 00:00, because with 'M' dd must respect the rule: 1 ≤ dd ≤ 31
    • + *
    • "M 5 6 30 12" = every month on the 5th at 06:30, because at least 3 parameters are expected and so considered: "12" and eventual other parameters are ignored
    • + *
    + * + *

    Computing next event date

    + * + *

    + * When this class is initialized with a frequency, it is able to compute the date of the event following a given date. + * The functions {@link #nextEvent()} and {@link #nextEvent(Date)} will compute this next event date + * from, respectively, now (current date/time) and the given date (the date of the last event). Both are computing the date of the next + * event by "adding" the frequency to the given date. And finally, the computed date is stored and returned. + *

    + * + *

    Then, you have 2 possibilities to trigger the desired event:

    + *
      + *
    • By calling {@link #isTimeElapsed()}, you can test whether at the current moment the date of the next event has been reached or not. + * In function of the value returned by this function you will be then able to process the desired action or not.
    • + *
    • By creating a Timer with the next date event. Thus, the desired action will be automatically triggered at the exact moment.
    • + *

      + * + * + * @author Marc Wenger (CDS) + * @author Grégory Mantelet (ARI) + * @version 4.1 (02/2015) + * @since 4.1 + */ +public final class EventFrequency { + + /** String format of a hour or a minute number. */ + private static final NumberFormat NN = new DecimalFormat("00"); + + /** Date-Time format to use in order to identify a frequent event. */ + private static final DateFormat EVENT_ID_FORMAT = new SimpleDateFormat("yyyyMMdd_HHmm"); + + /** Ordered list of all week days (there, the first week day is Sunday). */ + private static final String[] WEEK_DAYS = {"Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"}; + + /** Ordinal day number suffix (1st, 2nd, 3rd and th for the others). */ + private static final String[] DAY_SUFFIX = {"st","nd","rd","th"}; + + /** Frequency type (D, W, M, h, m). Default value: 'D' */ + private char dwm = 'D'; + + /** "day" (dd) parameter of the frequency. */ + private int day = 0; + /** "hour" (hh) parameter of the frequency. */ + private int hour = 0; + /** "minute" (mm) parameter of the frequency. */ + private int min = 0; + + /** ID of the next event. By default, it is built using the date of the last event with the format {@link #EVENT_ID_FORMAT}. */ + private String eventID = ""; + + /** Date (in millisecond) of the next event. */ + private long nextEvent = -1; + + /** + *

      Create a new event frequency.

      + * + *

      The frequency string must respect the following syntax:

      + *
        + *
      • 'D' hh mm : daily schedule at hh:mm
      • + *
      • 'W' dd hh mm : weekly schedule at the given day of the week (1:sunday, 2:monday, ..., 7:saturday) at hh:mm
      • + *
      • 'M' dd hh mm : monthly schedule at the given day of the month at hh:mm
      • + *
      • 'h' mm : hourly schedule at the given minute
      • + *
      • 'm' : scheduled every minute (for completness :-))
      • + *
      + *

      Where: hh = integer between 0 and 23, mm = integer between 0 and 59, dd (for 'W') = integer between 1 and 7 (1:sunday, 2:monday, ..., 7:saturday), + * dd (for 'M') = integer between 1 and 31.

      + * + *

      Warning: + * The frequency type is case sensitive! Then you should particularly pay attention at the case + * when using the frequency types 'M' (monthly) and 'm' (every minute). + *

      + * + *

      + * Parsing errors are not thrown but "resolved" silently. The "solution" depends of the error. + * 2 cases of errors are considered: + *

      + *
        + *
      • Frequency type mismatch: It happens when the first character is not one of the expected (D, W, M, h, m). + * That means: bad case (i.e. 'd' rather than 'D'), another character. + * In this case, the frequency will be: daily at 00:00.
      • + * + *
      • Parameter(s) missing or incorrect: With the "daily" frequency ('D'), at least 2 parameters must be provided ; + * 3 for "weekly" ('W') and "monthly" ('M') ; only 1 for "hourly" ('h') ; none for "every minute" ('m'). + * This number of parameters is a minimum: only the n first parameters will be considered while + * the others will be ignored. + * If this minimum number of parameters is not respected or if a parameter value is incorrect, + * all parameters will be set to their default value + * (which is 0 for all parameter except dd for which it is 1).
      • + *
      + * + *

      Examples:

      + *
        + *
      • "" or NULL = every day at 00:00
      • + *
      • "D 06 30" or "D 6 30" = every day at 06:30
      • + *
      • "D 24 30" = every day at 00:00, because hh must respect the rule: 0 ≤ hh ≤ 23
      • + *
      • "d 06 30" or "T 06 30" = every day at 00:00, because the frequency type "d" (lower case of "D") or "T" do not exist
      • + *
      • "W 2 6 30" = every week on Tuesday at 06:30
      • + *
      • "W 8 06 30" = every week on Sunday at 00:00, because with 'W' dd must respect the rule: 1 ≤ dd ≤ 7
      • + *
      • "M 2 6 30" = every month on the 2nd at 06:30
      • + *
      • "M 32 6 30" = every month on the 1st at 00:00, because with 'M' dd must respect the rule: 1 ≤ dd ≤ 31
      • + *
      • "M 5 6 30 12" = every month on the 5th at 06:30, because at least 3 parameters are expected and so considered: "12" and eventual other parameters are ignored
      • + *
      + * + * @param interval A string defining the event frequency (see above for the string format). + */ + public EventFrequency(String interval){ + String str; + + // Determine the separation between the frequency type character (D, W, M, h, m) and the parameters + // and normalize the given interval: + int p = -1; + if (interval == null) + interval = ""; + else{ + interval = interval.replaceAll("[ \t]+", " ").trim(); + p = interval.indexOf(' '); + } + + // Parse the given interval ONLY IF a frequency type is provided (even if there is no parameter): + if (p == 1 || interval.length() == 1){ + MatchResult result; + Scanner scan = null; + + // Extract and identify the frequency type: + dwm = interval.charAt(0); + str = interval.substring(p + 1); + scan = new Scanner(str); + + // Extract the parameters in function of the frequency type: + switch(dwm){ + // CASE: DAILY + case 'D': + scan.findInLine("(\\d{1,2}) (\\d{1,2})"); + try{ + result = scan.match(); + hour = parseHour(result.group(1)); + min = parseMinute(result.group(2)); + }catch(IllegalStateException ise){ + day = hour = min = 0; + } + break; + + // CASE: WEEKLY AND MONTHLY + case 'W': + case 'M': + scan.findInLine("(\\d{1,2}) (\\d{1,2}) (\\d{1,2})"); + try{ + result = scan.match(); + day = (dwm == 'W') ? parseDayOfWeek(result.group(1)) : parseDayOfMonth(result.group(1)); + hour = parseHour(result.group(2)); + min = parseMinute(result.group(3)); + }catch(IllegalStateException ise){ + day = (dwm == 'W') ? 0 : 1; + hour = min = 0; + } + break; + + // CASE: HOURLY + case 'h': + scan.findInLine("(\\d{1,2})"); + try{ + result = scan.match(); + min = parseMinute(result.group(1)); + }catch(IllegalStateException ise){ + min = 0; + } + break; + + // CASE: EVERY MINUTE + case 'm': + // no other data needed + break; + + // CASE: UNKNOWN FREQUENCY TYPE + default: + dwm = 'D'; + day = hour = min = 0; + } + if (scan != null) + scan.close(); + } + } + + /** + * Parse a string representing the day of the week (as a number). + * + * @param dayNbStr String containing an integer representing a week day. + * + * @return The identified week day. (integer between 0 and 6 (included)) + * + * @throws IllegalStateException If the given string does not contain an integer or is not between 1 and 7 (included). + */ + private int parseDayOfWeek(final String dayNbStr) throws IllegalStateException{ + try{ + int d = Integer.parseInt(dayNbStr); + if (d >= 1 && d <= WEEK_DAYS.length) + return d - 1; + }catch(Exception e){} + throw new IllegalStateException("Incorrect day of week (" + dayNbStr + ") ; it should be between 1 and 7 (both included)!"); + } + + /** + * Parse a string representing the day of the month. + * + * @param dayStr String containing an integer representing a month day. + * + * @return The identified month day. (integer between 1 and 31 (included)) + * + * @throws IllegalStateException If the given string does not contain an integer or is not between 1 and 31 (included). + */ + private int parseDayOfMonth(final String dayStr) throws IllegalStateException{ + try{ + int d = Integer.parseInt(dayStr); + if (d >= 1 && d <= 31) + return d; + }catch(Exception e){} + throw new IllegalStateException("Incorrect day of month (" + dayStr + ") ; it should be between 1 and 31 (both included)!"); + } + + /** + * Parse a string representing the hour part of a time (hh:mm). + * + * @param hourStr String containing an integer representing an hour. + * + * @return The identified hour. (integer between 0 and 23 (included)) + * + * @throws IllegalStateException If the given string does not contain an integer or is not between 0 and 23 (included). + */ + private int parseHour(final String hourStr) throws IllegalStateException{ + try{ + int h = Integer.parseInt(hourStr); + if (h >= 0 && h <= 23) + return h; + }catch(Exception e){} + throw new IllegalStateException("Incorrect hour number(" + hourStr + ") ; it should be between 0 and 23 (both included)!"); + } + + /** + * Parse a string representing the minute part of a time (hh:mm). + * + * @param minStr String containing an integer representing a minute. + * + * @return The identified minute. (integer between 0 and 59 (included)) + * + * @throws IllegalStateException If the given string does not contain an integer or is not between 0 and 59 (included). + */ + private int parseMinute(final String minStr) throws IllegalStateException{ + try{ + int m = Integer.parseInt(minStr); + if (m >= 0 && m <= 59) + return m; + }catch(Exception e){} + throw new IllegalStateException("Incorrect minute number (" + minStr + ") ; it should be between 0 and 59 (both included)!"); + } + + /** + * Tell whether the interval between the last event and now is greater or equals to the frequency represented by this object. + * + * @return true if the next event date has been reached, false otherwise. + */ + public boolean isTimeElapsed(){ + return (nextEvent <= 0) || (System.currentTimeMillis() >= nextEvent); + } + + /** + * Get the date of the next event. + * + * @return Date of the next event, or NULL if no date has yet been computed. + */ + public Date getNextEvent(){ + return (nextEvent <= 0) ? null : new Date(nextEvent); + } + + /** + *

      Get a string which identity the period between the last event and the next one (whose the date has been computed by this object).

      + * + *

      This ID is built by formatting in string the given date of the last event.

      + * + * @return ID of the period before the next event. + */ + public String getEventID(){ + return eventID; + } + + /** + *

      Compute the date of the event, by adding the interval represented by this object to the current date/time.

      + * + *

      + * The role of this function is to compute the next event date, not to get it. After computation, you can get this date + * thanks to {@link #getNextEvent()}. Furthermore, using {@link #isTimeElapsed()} after having called this function will + * let you test whether the next event should (have) occur(red). + *

      + * + *

      Note: + * This function computes the next event date by taking the current date as the date of the last event. However, + * if the last event occurred at a different date, you should use {@link #nextEvent(Date)}. + *

      + * + * @return Date at which the next event should occur. (basically, it is: NOW + frequency) + */ + public Date nextEvent(){ + return nextEvent(new Date()); + } + + /** + *

      Compute the date of the event, by adding the interval represented by this object to the given date/time.

      + * + *

      + * The role of this function is to compute the next event date, not to get it. After computation, you can get this date + * thanks to {@link #getNextEvent()}. Furthermore, using {@link #isTimeElapsed()} after having called this function will + * let you test whether the next event should (have) occur(red). + *

      + * + * @return Date at which the next event should occur. (basically, it is lastEventDate + frequency) + */ + public Date nextEvent(final Date lastEventDate){ + // Set the calendar to the given date: + GregorianCalendar date = new GregorianCalendar(); + date.setTime(lastEventDate); + + // Compute the date of the next event: + switch(dwm){ + // CASE: DAILY + case 'D': + date.add(Calendar.DAY_OF_YEAR, 1); + date.set(Calendar.HOUR_OF_DAY, hour); + date.set(Calendar.MINUTE, min); + date.set(Calendar.SECOND, 0); + break; + + // CASE: WEEKLY + case 'W': + // find the next right day to trigger the rotation + int weekday = date.get(Calendar.DAY_OF_WEEK); // sunday=1, ... saturday=7 + if (weekday == day){ + date.add(Calendar.WEEK_OF_YEAR, 1); + }else{ + // for the first scheduling which can happen any day + int delta = day - weekday; + if (delta <= 0) + delta += 7; + date.add(Calendar.DAY_OF_YEAR, delta); + } + date.set(Calendar.HOUR_OF_DAY, hour); + date.set(Calendar.MINUTE, min); + date.set(Calendar.SECOND, 0); + break; + + // CASE: MONTHLY + case 'M': + date.add(Calendar.MONTH, 1); + date.set(Calendar.DAY_OF_MONTH, day); + date.set(Calendar.HOUR_OF_DAY, hour); + date.set(Calendar.MINUTE, min); + date.set(Calendar.SECOND, 0); + break; + + // CASE: HOURLY + case 'h': + date.add(Calendar.HOUR_OF_DAY, 1); + date.set(Calendar.MINUTE, min); + date.set(Calendar.SECOND, 0); + break; + + // CASE: EVERY MINUTE + case 'm': + date.add(Calendar.MINUTE, 1); + date.set(Calendar.SECOND, 0); + break; + + /* OTHERWISE, the next event date is the given date! */ + } + + // Save it in millisecond for afterward comparison with the current time (so that telling whether the time is elapsed or not): + nextEvent = date.getTimeInMillis(); + + // Build the ID of this waiting period (the period between the last event and the next one): + eventID = EVENT_ID_FORMAT.format(new Date()); + + // Return the date of the next event: + return date.getTime(); + } + + /** + * Display in a human readable way the frequency represented by this object. + * + * @return a string, i.e. weekly on Sunday at HH:MM + */ + @Override + public String toString(){ + StringBuilder str = new StringBuilder(); + switch(dwm){ + case 'D': + str.append("daily"); + str.append(" at ").append(NN.format(hour)).append(':').append(NN.format(min)); + break; + case 'W': + str.append("weekly on ").append(WEEK_DAYS[day % 7]); + str.append(" at ").append(NN.format(hour)).append(':').append(NN.format(min)); + break; + case 'M': + str.append("monthly on the ").append(day).append(DAY_SUFFIX[Math.min(day - 1, 3)]); + str.append(" at ").append(NN.format(hour)).append(':').append(NN.format(min)); + break; + case 'h': + str.append("hourly at ").append(NN.format(min)); + break; + case 'm': + str.append("every minute"); + break; + } + + return str.toString(); + } +} \ No newline at end of file diff --git a/src/uws/service/file/LocalUWSFileManager.java b/src/uws/service/file/LocalUWSFileManager.java index 41f89f7..0bbb628 100644 --- a/src/uws/service/file/LocalUWSFileManager.java +++ b/src/uws/service/file/LocalUWSFileManager.java @@ -16,10 +16,14 @@ package uws.service.file; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; import java.io.File; +import java.io.FileFilter; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; @@ -27,63 +31,74 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; - +import java.net.URI; +import java.net.URISyntaxException; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; -import java.util.HashMap; import java.util.Iterator; -import java.util.Map; import java.util.NoSuchElementException; -import java.io.FileFilter; - import uws.UWSException; import uws.UWSToolBox; - import uws.job.ErrorSummary; import uws.job.Result; import uws.job.UWSJob; - import uws.job.user.JobOwner; - -import uws.service.log.UWSLogType; +import uws.service.log.UWSLog.LogLevel; +import uws.service.request.UploadFile; /** *

      All UWS files are stored in the local machine into the specified directory.

      + * *

      * The name of the log file, the result files and the backup files may be customized by overriding the following functions: - * {@link #getLogFileName()}, {@link #getResultFileName(Result, UWSJob)}, {@link #getBackupFileName(JobOwner)} and {@link #getBackupFileName()}. + * {@link #getLogFileName(uws.service.log.UWSLog.LogLevel, String)}, {@link #getResultFileName(Result, UWSJob)}, {@link #getBackupFileName(JobOwner)} and {@link #getBackupFileName()}. *

      + * *

      * By default, results and backups are grouped by owner/user and owners/users are grouped thanks to {@link DefaultOwnerGroupIdentifier}. * By using the appropriate constructor, you can change these default behaviors. *

      * - * @author Grégory Mantelet (CDS) - * @version 06/2012 + *

      + * A log file rotation is set by default so that avoiding a too big log file after several months/years of use. + * By default the rotation is done every month on the 1st at 6am. This frequency can be changed easily thanks to the function + * {@link #setLogRotationFreq(String)}. + *

      + * + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (02/2015) */ public class LocalUWSFileManager implements UWSFileManager { /** Format to use to format dates. */ private DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); - protected static final String DEFAULT_HTTP_LOG_FILE_NAME = "service_http_activity.log"; - protected static final String DEFAULT_DEBUG_LOG_FILE_NAME = "service_debug.log"; - protected static final String DEFAULT_LOG_FILE_NAME = "service_activity.log"; - protected static final String DEFAULT_BACKUP_FILE_NAME = "uws.backup"; - - private static final String UNKNOWN_LOG_TYPE_GROUP = "???"; + /** Default name of the log file. */ + protected static final String DEFAULT_LOG_FILE_NAME = "service.log"; + /** Default name of the general UWS backup file. */ + protected static final String DEFAULT_BACKUP_FILE_NAME = "service.backup"; + /** Directory in which all files managed by this class will be written and read. */ protected final File rootDirectory; + /** Output toward the service log file. */ + protected PrintWriter logOutput = null; + /** Frequency at which the log file must be "rotated" (the file is renamed with the date of its first write and a new log file is created). + * Thus, too big log files can be avoided. */ + protected EventFrequency logRotation = new EventFrequency("D 0 0"); // Log file rotation every day at midnight. + + /** Indicate whether a directory must be used to gather all jobs, results and errors related to one identified user. + * If FALSE, all jobs, results and errors will be in only one directory, whoever owns them. */ protected final boolean oneDirectoryForEachUser; + /** Gather user directories, set by set. At the end, several user group directories may be created. + * This option is considered only if {@link #oneDirectoryForEachUser} is TRUE. */ protected final boolean groupUserDirectories; + /** Object giving the policy about how to group user directories. */ protected final OwnerGroupIdentifier ownerGroupId; - protected Map logOutputs = new HashMap(); - /** *

      Builds a {@link UWSFileManager} which manages all UWS files in the given directory.

      *

      @@ -93,7 +108,8 @@ public class LocalUWSFileManager implements UWSFileManager { * * @param root UWS root directory. * - * @throws UWSException If the given root directory is null, is not a directory or has not the READ and WRITE permissions. + * @throws NullPointerException If the given root directory is null. + * @throws UWSException If the given file is not a directory or has not the READ and WRITE permissions. * * @see #LocalUWSFileManager(File, boolean, boolean, OwnerGroupIdentifier) */ @@ -113,7 +129,8 @@ public class LocalUWSFileManager implements UWSFileManager { * @param groupUserDirectories true to group user directories, false otherwise. * note: this value is ignored if the previous parameter is false. * - * @throws UWSException If the given root directory is null, is not a directory or has not the READ and WRITE permissions. + * @throws NullPointerException If the given root directory is null. + * @throws UWSException If the given file is not a directory or has not the READ and WRITE permissions. * * @see #LocalUWSFileManager(File, boolean, boolean, OwnerGroupIdentifier) */ @@ -135,7 +152,8 @@ public class LocalUWSFileManager implements UWSFileManager { * {@link DefaultOwnerGroupIdentifier} will be chosen as default group identifier. *

    * - * @throws UWSException If the given root directory is null, is not a directory or has not the READ and WRITE permissions. + * @throws NullPointerException If the given root directory is null. + * @throws UWSException If the given file is not a directory or has not the READ and WRITE permissions. */ public LocalUWSFileManager(final File root, final boolean oneDirectoryForEachUser, final boolean groupUserDirectories, final OwnerGroupIdentifier ownerGroupIdentifier) throws UWSException{ if (root == null) @@ -225,64 +243,109 @@ public class LocalUWSFileManager implements UWSFileManager { /* ******************* */ /* LOG FILE MANAGEMENT */ /* ******************* */ + /** - *

    Lets grouping log messages by log type.

    - *

    For instance: by default all messages of type INFO, WARNING and ERROR are written in the same file.

    + * Get the frequency of the log file rotation + * in a human readable way. * - * @param logType Type of the message to log. + * @return A human readable frequency of the log file rotation. + */ + public final String getLogRotationFreq(){ + return logRotation.toString(); + } + + /** + *

    Set the frequency at which a rotation of the log file must be done.

    + * + *

    + * "rotation" means here, to close the currently used log file, to rename it so that suffixing it + * with the date at which the first log has been written in it, and to create a new log file. + *

    + * + *

    The frequency string must respect the following syntax:

    + *
      + *
    • 'D' hh mm : daily schedule at hh:mm
    • + *
    • 'W' dd hh mm : weekly schedule at the given day of the week (1:sunday, 2:monday, ..., 7:saturday) at hh:mm
    • + *
    • 'M' dd hh mm : monthly schedule at the given day of the month at hh:mm
    • + *
    • 'h' mm : hourly schedule at the given minute
    • + *
    • 'm' : scheduled every minute (for completness :-))
    • + *
    + *

    Where: hh = integer between 0 and 23, mm = integer between 0 and 59, dd (for 'W') = integer between 1 and 7 (1:sunday, 2:monday, ..., 7:saturday), + * dd (for 'M') = integer between 1 and 31.

    + * + *

    Warning: + * The frequency type is case sensitive! Then you should particularly pay attention at the case + * when using the frequency types 'M' (monthly) and 'm' (every minute). + *

    * - * @return Name of the log type group. + *

    + * Parsing errors are not thrown but "resolved" silently. The "solution" depends of the error. + * 2 cases of errors are considered: + *

    + *
      + *
    • Frequency type mismatch: It happens when the first character is not one of the expected (D, W, M, h, m). + * That means: bad case (i.e. 'd' rather than 'D'), another character. + * In this case, the frequency will be: daily at 00:00.
    • + * + *
    • Parameter(s) missing or incorrect: With the "daily" frequency ('D'), at least 2 parameters must be provided ; + * 3 for "weekly" ('W') and "monthly" ('M') ; only 1 for "hourly" ('h') ; none for "every minute" ('m'). + * This number of parameters is a minimum: only the n first parameters will be considered while + * the others will be ignored. + * If this minimum number of parameters is not respected or if a parameter value is incorrect, + * all parameters will be set to their default value + * (which is 0 for all parameter except dd for which it is 1).
    • + *
    + * + *

    Examples:

    + *
      + *
    • "" or NULL = every day at 00:00
    • + *
    • "D 06 30" or "D 6 30" = every day at 06:30
    • + *
    • "D 24 30" = every day at 00:00, because hh must respect the rule: 0 ≤ hh ≤ 23
    • + *
    • "d 06 30" or "T 06 30" = every day at 00:00, because the frequency type "d" (lower case of "D") or "T" do not exist
    • + *
    • "W 2 6 30" = every week on Tuesday at 06:30
    • + *
    • "W 8 06 30" = every week on Sunday at 00:00, because with 'W' dd must respect the rule: 1 ≤ dd ≤ 7
    • + *
    • "M 2 6 30" = every month on the 2nd at 06:30
    • + *
    • "M 32 6 30" = every month on the 1st at 00:00, because with 'M' dd must respect the rule: 1 ≤ dd ≤ 31
    • + *
    • "M 5 6 30 12" = every month on the 5th at 06:30, because at least 3 parameters are expected and so considered: "12" and eventual other parameters are ignored
    • + *
    + * + * @param interval Interval between two log rotations. */ - protected String getLogTypeGroup(final UWSLogType logType){ - switch(logType){ - case INFO: - case WARNING: - case ERROR: - return "DefaultLog"; - case DEBUG: - case HTTP_ACTIVITY: - return logType.toString(); - case CUSTOM: - return logType.getCustomType(); - default: - return UNKNOWN_LOG_TYPE_GROUP; - } + public final void setLogRotationFreq(final String interval){ + logRotation = new EventFrequency(interval); } /** *

    Gets the name of the UWS log file.

    - *

    By default: {@link #DEFAULT_LOG_FILE_NAME} or {@link #DEFAULT_HTTP_LOG_FILE_NAME} (to log an activity message, that's to say: thread status or http request).

    * - * @param logType Type of message to log. + *

    By default: {@link #DEFAULT_LOG_FILE_NAME}.

    + * + * @param level Level of the message to log (DEBUG, INFO, WARNING, ERROR, FATAL). + * @param context Context of the message to log (UWS, HTTP, THREAD, JOB, ...). * * @return The name of the UWS log file. */ - protected String getLogFileName(final String logTypeGroup){ - if (logTypeGroup == UWSLogType.HTTP_ACTIVITY.toString()) - return DEFAULT_HTTP_LOG_FILE_NAME; - else if (logTypeGroup.equals(UWSLogType.DEBUG.toString())) - return DEFAULT_DEBUG_LOG_FILE_NAME; - else - return DEFAULT_LOG_FILE_NAME; + protected String getLogFileName(final LogLevel level, final String context){ + return DEFAULT_LOG_FILE_NAME; } /** * Gets the UWS log file. * - * @param logType Type of message to log. + * @param level Level of the message to log (DEBUG, INFO, WARNING, ERROR, FATAL). + * @param context Context of the message to log (UWS, HTTP, THREAD, JOB, ...). * * @return The UWS log file. * - * @see #getLogFileName() + * @see #getLogFileName(uws.service.log.UWSLog.LogLevel, String) */ - protected File getLogFile(final String logTypeGroup){ - return new File(rootDirectory, getLogFileName(logTypeGroup)); + protected File getLogFile(final LogLevel level, final String context){ + return new File(rootDirectory, getLogFileName(level, context)); } @Override - public InputStream getLogInput(final UWSLogType logType) throws IOException{ - String logTypeGroup = getLogTypeGroup(logType); - File logFile = getLogFile(logTypeGroup); + public InputStream getLogInput(final LogLevel level, final String context) throws IOException{ + File logFile = getLogFile(level, context); if (logFile.exists()) return new FileInputStream(logFile); else @@ -290,24 +353,52 @@ public class LocalUWSFileManager implements UWSFileManager { } @Override - public PrintWriter getLogOutput(final UWSLogType logType) throws IOException{ - String logTypeGroup = getLogTypeGroup(logType); - PrintWriter output = logOutputs.get(logTypeGroup); - if (output == null){ - File logFile = getLogFile(logTypeGroup); + public synchronized PrintWriter getLogOutput(final LogLevel level, final String context) throws IOException{ + // If a file rotation is needed... + if (logOutput != null && logRotation != null && logRotation.isTimeElapsed()){ + // ...Close the output stream: + logOutput.close(); + logOutput = null; + + // ...Rename this log file: + // get the file: + File logFile = getLogFile(level, context); + // and its name: + String logFileName = logFile.getName(); + // separate the file name from the extension: + String fileExt = ""; + int indFileExt = logFileName.lastIndexOf('.'); + if (indFileExt >= 0){ + fileExt = logFileName.substring(indFileExt); + logFileName = logFileName.substring(0, indFileExt); + } + // build the new file name and rename the log file: + logFile.renameTo(new File(logFile.getParentFile(), logFileName + "_" + logRotation.getEventID() + fileExt)); + } + + // If the log output is not yet set or if a file rotation has been done... + if (logOutput == null){ + // ...Create the output: + File logFile = getLogFile(level, context); createParentDir(logFile); - output = new PrintWriter(new FileOutputStream(logFile, true), true); - printLogHeader(output); - logOutputs.put(logTypeGroup, output); + logOutput = new PrintWriter(new FileOutputStream(logFile, true), true); + + // ...Write a log header: + printLogHeader(logOutput); + + // ...Set the date of the next rotation: + if (logRotation != null) + logRotation.nextEvent(); } - return output; + + return logOutput; } /** * Print a header into the log file so that separating older log messages to the new ones. */ protected void printLogHeader(final PrintWriter out){ - String msgHeader = "########################################### LOG STARTS " + dateFormat.format(new Date()) + " ###########################################"; + String msgHeader = "########################################### LOG STARTS " + dateFormat.format(new Date()) + " (file rotation: " + logRotation + ") ###########################################"; StringBuffer buf = new StringBuffer(""); for(int i = 0; i < msgHeader.length(); i++) buf.append('#'); @@ -320,6 +411,120 @@ public class LocalUWSFileManager implements UWSFileManager { out.flush(); } + /* ************************* */ + /* UPLOADED FILES MANAGEMENT */ + /* ************************* */ + + /** + * Create a File instance from the given upload file description. + * This function is able to deal with location as URI and as file path. + * + * @param upload Description of an uploaded file. + * + * @return The corresponding File object. + * + * @since 4.1 + */ + protected final File getFile(final UploadFile upload){ + if (upload.getLocation().startsWith("file:")){ + try{ + return new File(new URI(upload.getLocation())); + }catch(URISyntaxException use){ + return new File(upload.getLocation()); + } + }else + return new File(upload.getLocation()); + } + + @Override + public InputStream getUploadInput(final UploadFile upload) throws IOException{ + // Check the source file: + File source = getFile(upload); + if (!source.exists()) + throw new FileNotFoundException("The uploaded file submitted with the parameter \"" + upload.paramName + "\" can not be found any more on the server!"); + // Return the stream: + return new FileInputStream(source); + } + + @Override + public InputStream openURI(final URI uri) throws UnsupportedURIProtocolException, IOException{ + String scheme = uri.getScheme(); + if (scheme.equalsIgnoreCase("http") || scheme.equalsIgnoreCase("ftp")) + return uri.toURL().openStream(); + else + throw new UnsupportedURIProtocolException(uri); + } + + @Override + public void deleteUpload(final UploadFile upload) throws IOException{ + File f = getFile(upload); + if (!f.exists()) + return; + else if (f.isDirectory()) + throw new IOException("Incorrect location! An uploaded file must be a regular file, not a directory. (file location: \"" + f.getPath() + "\")"); + else{ + try{ + if (!f.delete()) + throw new IOException("Can not delete the file!"); + }catch(SecurityException se){ + throw new IOException("Unexpected permission restriction on the uploaded file \"" + f.getPath() + "\" => can not delete it!"); + } + } + } + + @Override + public String moveUpload(final UploadFile upload, final UWSJob destination) throws IOException{ + // Check the source file: + File source = getFile(upload); + if (!source.exists()) + throw new FileNotFoundException("The uploaded file submitted with the parameter \"" + upload.paramName + "\" can not be found any more on the server!"); + + // Build the final location (in the owner directory, under the name "UPLOAD_{job-id}_{param-name}": + File ownerDir = getOwnerDirectory(destination.getOwner()); + File copy = new File(ownerDir, "UPLOAD_" + destination.getJobId() + "_" + upload.paramName); + + OutputStream output = null; + InputStream input = null; + boolean done = false; + try{ + // open the input and output: + input = new BufferedInputStream(getUploadInput(upload)); + output = new BufferedOutputStream(new FileOutputStream(copy)); + // proceed to the copy: + byte[] buffer = new byte[2048]; + int len; + while((len = input.read(buffer)) > 0) + output.write(buffer, 0, len); + output.flush(); + output.close(); + output = null; + // close the input and delete the source file: + input.close(); + input = null; + source.delete(); + // return the new location: + done = true; + return copy.toURI().toString(); + }finally{ + if (output != null){ + try{ + output.close(); + }catch(IOException ioe){} + } + if (input != null){ + try{ + input.close(); + }catch(IOException ioe){} + } + // In case of problem, the copy must be deleted: + if (!done && copy.exists()){ + try{ + copy.delete(); + }catch(SecurityException ioe){} + } + } + } + /* *********************** */ /* RESULT FILES MANAGEMENT */ /* *********************** */ @@ -477,7 +682,7 @@ public class LocalUWSFileManager implements UWSFileManager { */ protected String getBackupFileName(final JobOwner owner) throws IllegalArgumentException{ if (owner == null || owner.getID() == null || owner.getID().trim().isEmpty()) - throw new IllegalArgumentException("Missing owner ! Can not get the backup file of an unknown owner. See LocalUWSFileManager.getBackupFile(JobOwner)"); + throw new IllegalArgumentException("Missing owner! Can not get the backup file of an unknown owner."); return owner.getID().replaceAll(File.separator, "_") + ".backup"; } diff --git a/src/uws/service/file/UWSFileManager.java b/src/uws/service/file/UWSFileManager.java index bc8520b..8dbc7f7 100644 --- a/src/uws/service/file/UWSFileManager.java +++ b/src/uws/service/file/UWSFileManager.java @@ -16,23 +16,25 @@ package uws.service.file; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; - +import java.net.URI; +import java.net.URL; import java.util.Iterator; import uws.job.ErrorSummary; -import uws.job.UWSJob; import uws.job.Result; - +import uws.job.UWSJob; import uws.job.user.JobOwner; - -import uws.service.log.UWSLogType; +import uws.service.log.UWSLog.LogLevel; +import uws.service.request.UploadFile; /** *

    Lets accessing any file managed by a UWS service.

    @@ -42,8 +44,8 @@ import uws.service.log.UWSLogType; * the results, log or backup file generated and read by a UWS. *

    * - * @author Grégory Mantelet (CDS) - * @version 05/2012 + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (11/2014) * * @see LocalUWSFileManager */ @@ -55,20 +57,117 @@ public interface UWSFileManager { /** * Gets an input stream on the log file of this UWS. - * @param logType Type of the message to log. + * + * @param level Level of the message to log (DEBUG, INFO, WARNING, ERROR or FATAL). + * @param context Context of the message to log (UWS, HTTP, JOB, THREAD, ...). + * * @return An input on the log file or null if there is no log file. + * * @throws IOException If there is an error while opening an input stream on the log file. */ - public InputStream getLogInput(final UWSLogType logType) throws IOException; + public InputStream getLogInput(final LogLevel level, final String context) throws IOException; /** *

    Gets an output stream on the log file of this UWS.

    *

    note: The log file must be automatically created if needed.

    - * @param logType Type of the message to log. + * + * @param level Level of the message to log (DEBUG, INFO, WARNING, ERROR or FATAL). + * @param context Context of the message to log (UWS, HTTP, JOB, THREAD, ...). + * * @return An output on the log file. + * * @throws IOException If there is an error while creating the log file or while opening an output stream on it. */ - public PrintWriter getLogOutput(final UWSLogType logType) throws IOException; + public PrintWriter getLogOutput(final LogLevel level, final String context) throws IOException; + + /* ************************* */ + /* UPLOADED FILES MANAGEMENT */ + /* ************************* */ + + /**

    Temporary directory in which uploaded files will be stored when parsing the HTTP request.

    + *

    IMPORTANT 1: + * Uploaded files should be then moved using {@link UploadFile#move(UWSJob)} when the job creation or update is validated. + *

    + *

    IMPORTANT 2: + * As qualified above, this directory is temporary. It means that it should be emptied sometimes. + * It is particularly important because when a delete or move operation fails on uploaded files, no log or error might + * be published. + *

    + *

    Note: + * The default value is the temporary directory of the system (i.e. \tmp or \var\tmp on Unix/Linux/MacOS, c:\temp on Windows). + *

    + * @since 4.1 */ + public static File TMP_UPLOAD_DIR = new File(System.getProperty("java.io.tmpdir")); + + /** + * Open a stream toward the specified file, submitted inline in an HTTP request. + * + * @param upload Description of the uploaded file. + * + * @return Input to the specified uploaded file. + * + * @throws IOException If any error occurs while opening the stream. + * + * @since 4.1 + */ + public InputStream getUploadInput(final UploadFile upload) throws IOException; + + /** + *

    Open a stream toward the given URI.

    + * + *

    + * Most of the time, the given URI uses the protocol http, https or ftp, which makes + * the URI perfectly understandable by {@link URL} which is then able to open easily + * a stream (cf {@link URL#openStream()}). However, a different scheme/protocol could + * be used ; particularly VO ones like "ivo" and "vos". It is for these particular + * cases that this function has been designed: in order to provide an implementation + * supporting additional protocols. + *

    + * + * @param uri URI of any resource to read. + * + * @return Input to the specified resource. + * + * @throws UnsupporteURIProtocol If the protocol is not supported by this implementation. + * @throws IOException If another error occurs while opening the stream. + * + * @since 4.1 + */ + public InputStream openURI(final URI uri) throws UnsupportedURIProtocolException, IOException; + + /** + * Delete definitely the specified file, submitted inline in an HTTP request. + * + * @param upload Description of the uploaded file. + * + * @throws IOException If any error occurs while deleting the file. + * + * @since 4.1 + */ + public void deleteUpload(final UploadFile upload) throws IOException; + + /** + *

    Move the specified file from its current location to a location related to the given job.

    + * + *

    Note: + * This function is generally used only once: after the HTTP request parsing, when creating or updating a job and only if the action has been accepted. + *

    + * + *

    IMPORTANT: + * This function might not be able to update the location inside the given {@link UploadFile}. For this reason, + * it is strongly recommended to not call directly this function, but to use {@link UploadFile#move(UWSJob)}. + *

    + * + * @param upload Description of the uploaded file to move. + * @param destination Job in which the uploaded file will be used. + * + * @return The new location of the uploaded file. + * + * @throws IOException If any error occurs while moving the file. + * + * @since 4.1 + */ + public String moveUpload(final UploadFile upload, final UWSJob destination) throws IOException; /* *********************** */ /* RESULT FILES MANAGEMENT */ diff --git a/src/uws/service/file/UnsupportedURIProtocolException.java b/src/uws/service/file/UnsupportedURIProtocolException.java new file mode 100644 index 0000000..cc6a2da --- /dev/null +++ b/src/uws/service/file/UnsupportedURIProtocolException.java @@ -0,0 +1,45 @@ +package uws.service.file; + +/* + * This file is part of UWSLibrary. + * + * UWSLibrary is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * UWSLibrary is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with UWSLibrary. If not, see . + * + * Copyright 2014 - Astronomisches Rechen Institut (ARI) + */ + +import java.net.URI; + +import uws.UWSException; + +/** + * Error sent when trying to read a remote file using a URI whose the scheme/protocol is not supported. + * + * @author Grégory Mantelet (ARI) + * @version 4.1 (11/2014) + * @since 4.1 + */ +public class UnsupportedURIProtocolException extends UWSException { + private static final long serialVersionUID = 1L; + + /** + * Build an {@link UnsupportedURIProtocolException}. + * + * @param uri The URI whose the scheme/protocol is incorrect. + */ + public UnsupportedURIProtocolException(final URI uri){ + super(UWSException.BAD_REQUEST, "Unsupported protocol: \"" + (uri != null ? uri.getScheme() : "") + "\"! => can not open the resource \"" + uri + "\"."); + } + +} diff --git a/src/uws/service/log/DefaultUWSLog.java b/src/uws/service/log/DefaultUWSLog.java index 5fd9586..f0ff54c 100644 --- a/src/uws/service/log/DefaultUWSLog.java +++ b/src/uws/service/log/DefaultUWSLog.java @@ -16,37 +16,34 @@ package uws.service.log; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; - import java.text.DateFormat; import java.text.SimpleDateFormat; - import java.util.Date; -import java.util.Enumeration; +import java.util.Map; +import java.util.Map.Entry; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import uws.UWSException; - -import uws.job.JobList; +import uws.UWSToolBox; import uws.job.UWSJob; - import uws.job.user.JobOwner; - import uws.service.UWS; - import uws.service.file.UWSFileManager; /** *

    Default implementation of {@link UWSLog} interface which lets logging any message about a UWS.

    * - * @author Grégory Mantelet (CDS) - * @version 06/2012 + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (04/2015) */ public class DefaultUWSLog implements UWSLog { @@ -57,15 +54,21 @@ public class DefaultUWSLog implements UWSLog { protected final UWSFileManager fileManager; protected final PrintWriter defaultOutput; - /** - *

    The minimum value of the HTTP status code required to print the stack trace of a HTTP error.

    - *

    note: This value is used only by the function {@link #httpRequest(HttpServletRequest, JobOwner, String, int, String, Throwable)}.

    - */ - protected int minResponseCodeForStackTrace = 500; + /**

    Minimum level that a message must have in order to be logged.

    + *

    The default behavior is the following:

    + *
      + *
    • DEBUG: every messages are logged.
    • + *
    • INFO: every messages EXCEPT DEBUG are logged.
    • + *
    • WARNING: every messages EXCEPT DEBUG and INFO are logged.
    • + *
    • ERROR: only ERROR and FATAL messages are logged.
    • + *
    • FATAL: only FATAL messages are logged.
    • + *
    + * @since 4.1 */ + protected LogLevel minLogLevel = LogLevel.DEBUG; /** *

    Builds a {@link UWSLog} which will use the file manager - * of the given UWS to get the log output (see {@link UWSFileManager#getLogOutput(UWSLogType)}).

    + * of the given UWS to get the log output (see {@link UWSFileManager#getLogOutput(uws.service.log.UWSLog.LogLevel, String)}).

    * *

    note 1: This constructor is particularly useful if the file manager of the given UWS may change.

    *

    note 2: If no output can be found in the file manager (or if there is no file manager), @@ -81,10 +84,11 @@ public class DefaultUWSLog implements UWSLog { /** *

    Builds a {@link UWSLog} which will use the given file - * manager to get the log output (see {@link UWSFileManager#getLogOutput(UWSLogType)}).

    + * manager to get the log output (see {@link UWSFileManager#getLogOutput(uws.service.log.UWSLog.LogLevel, String)}).

    * *

    note 1: This constructor is particularly useful if the way of managing log output may change in the given file manager. - * Indeed, the output may change in function of the type of message to log ({@link UWSLogType}).

    + * Indeed, the output may change in function of the type of message to log ({@link uws.service.log.UWSLog.LogLevel}).

    + * *

    note 2 If no output can be found in the file manager the standard error output ({@link System#err}) * will be chosen automatically for all log messages.

    * @@ -100,7 +104,7 @@ public class DefaultUWSLog implements UWSLog { *

    Builds a {@link UWSLog} which will print all its * messages into the given stream.

    * - *

    note: the given output will be used whatever is the type of message to log ({@link UWSLogType}).

    + *

    note: the given output will be used whatever is the type of message to log ({@link uws.service.log.UWSLog.LogLevel}).

    * * @param output An output stream. */ @@ -114,7 +118,7 @@ public class DefaultUWSLog implements UWSLog { *

    Builds a {@link UWSLog} which will print all its * messages into the given stream.

    * - *

    note: the given output will be used whatever is the type of message to log ({@link UWSLogType}).

    + *

    note: the given output will be used whatever is the type of message to log ({@link uws.service.log.UWSLog.LogLevel}).

    * * @param writer A print writer. */ @@ -124,6 +128,51 @@ public class DefaultUWSLog implements UWSLog { defaultOutput = writer; } + /** + *

    Get the minimum level that a message must have in order to be logged.

    + * + *

    The default behavior is the following:

    + *
      + *
    • DEBUG: every messages are logged.
    • + *
    • INFO: every messages EXCEPT DEBUG are logged.
    • + *
    • WARNING: every messages EXCEPT DEBUG and INFO are logged.
    • + *
    • ERROR: only ERROR and FATAL messages are logged.
    • + *
    • FATAL: only FATAL messages are logged.
    • + *
    + * + * @return The minimum log level. + * + * @since 4.1 + */ + public final LogLevel getMinLogLevel(){ + return minLogLevel; + } + + /** + *

    Set the minimum level that a message must have in order to be logged.

    + * + *

    The default behavior is the following:

    + *
      + *
    • DEBUG: every messages are logged.
    • + *
    • INFO: every messages EXCEPT DEBUG are logged.
    • + *
    • WARNING: every messages EXCEPT DEBUG and INFO are logged.
    • + *
    • ERROR: only ERROR and FATAL messages are logged.
    • + *
    • FATAL: only FATAL messages are logged.
    • + *
    + * + *

    Note: + * If the given level is NULL, this function has no effect. + *

    + * + * @param newMinLevel The new minimum log level. + * + * @since 4.1 + */ + public final void setMinLogLevel(final LogLevel newMinLevel){ + if (newMinLevel != null) + minLogLevel = newMinLevel; + } + /** * Gets the date formatter/parser to use for any date read/write into this logger. * @return A date formatter/parser. @@ -141,44 +190,24 @@ public class DefaultUWSLog implements UWSLog { this.dateFormat = dateFormat; } - /** - *

    Gets the minimum value of the HTTP status code required to print the stack trace of a HTTP error.

    - * - *

    note: This value is used only by the function {@link #httpRequest(HttpServletRequest, JobOwner, String, int, String, Throwable)}.

    - * - * @return A HTTP response status code. - */ - public int getMinResponseCodeForStackTrace(){ - return minResponseCodeForStackTrace; - } - - /** - *

    Sets the minimum value of the HTTP status code required to print the stack trace of a HTTP error.

    - * - *

    note: This value is used only by the function {@link #httpRequest(HttpServletRequest, JobOwner, String, int, String, Throwable)}.

    - * - * @param httpCode A HTTP response status code. - */ - public void setMinResponseCodeForStackTrace(final int httpCode){ - minResponseCodeForStackTrace = httpCode; - } - /** *

    Gets an output for the given type of message to print.

    * *

    The {@link System#err} output is used if none can be found in the {@link UWS} or the {@link UWSFileManager} * given at the creation, or if the given output stream or writer is NULL.

    * - * @param logType Type of the message to print; + * @param level Level of the message to print (DEBUG, INFO, WARNING, ERROR or FATAL). + * @param context Context of the message to print (UWS, HTTP, JOB, THREAD). + * * @return A writer. */ - protected PrintWriter getOutput(final UWSLogType logType){ + protected PrintWriter getOutput(final LogLevel level, final String context){ try{ if (uws != null){ if (uws.getFileManager() != null) - return uws.getFileManager().getLogOutput(logType); + return uws.getFileManager().getLogOutput(level, context); }else if (fileManager != null) - return fileManager.getLogOutput(logType); + return fileManager.getLogOutput(level, context); else if (defaultOutput != null) return defaultOutput; }catch(IOException ioe){ @@ -192,249 +221,414 @@ public class DefaultUWSLog implements UWSLog { /* *********************** */ /** - * Logs the given message (and exception, if any). + *

    Normalize a log message.

    + * + *

    + * Since a log entry will a tab-separated concatenation of information, additional tabulations or new-lines + * would corrupt a log entry. This function replaces such characters by one space. Only \r are definitely deleted. + *

    + * + * @param message Log message to normalize. + * + * @return The normalized log message. + * + * @since 4.1 + */ + protected String normalizeMessage(final String message){ + if (message == null) + return null; + else + return message.replaceAll("[\n\t]", " ").replaceAll("\r", ""); + } + + /** + *

    Tells whether a message with the given error level can be logged or not.

    + * + *

    In function of the minimum log level of this class, the default behavior is the following:

    + *
      + *
    • DEBUG: every messages are logged.
    • + *
    • INFO: every messages EXCEPT DEBUG are logged.
    • + *
    • WARNING: every messages EXCEPT DEBUG and INFO are logged.
    • + *
    • ERROR: only ERROR and FATAL messages are logged.
    • + *
    • FATAL: only FATAL messages are logged.
    • + *
    + * + * @param msgLevel Level of the message which has been asked to log. Note: if NULL, it will be considered as DEBUG. + * + * @return true if the message associated with the given log level can be logged, false otherwise. + * + * @since 4.1 + */ + protected boolean canLog(LogLevel msgLevel){ + // No level specified => DEBUG + if (msgLevel == null) + msgLevel = LogLevel.DEBUG; + + // Decide in function of the minimum log level set in this class: + switch(minLogLevel){ + case INFO: + return (msgLevel != LogLevel.DEBUG); + case WARNING: + return (msgLevel != LogLevel.DEBUG && msgLevel != LogLevel.INFO); + case ERROR: + return (msgLevel == LogLevel.ERROR || msgLevel == LogLevel.FATAL); + case FATAL: + return (msgLevel == LogLevel.FATAL); + case DEBUG: + default: + return true; + } + } + + @Override + public void log(LogLevel level, final String context, final String message, final Throwable error){ + log(level, context, null, null, message, null, error); + } + + /** + *

    Logs a full message and/or error.

    + * + *

    Note: + * If no message and error is provided, nothing will be written. + *

    * - * @param type Type of the message to print. note: (If NULL, it will be ERROR if an exception is given, INFO otherwise.) - * @param msg Message to print. (may be NULL) - * @param t Exception to print. (may be NULL) + * @param level Level of the error (DEBUG, INFO, WARNING, ERROR, FATAL). SHOULD NOT be NULL + * @param context Context of the error (UWS, HTTP, THREAD, JOB). MAY be NULL + * @param event Context event during which this log is emitted. MAY be NULL + * @param ID ID of the job or HTTP request (it may also be an ID of anything else). MAY BE NULL + * @param message Message of the error. MAY be NULL + * @param addColumn Additional column to append after the message and before the stack trace. + * @param error Error at the origin of the log error/warning/fatal. MAY be NULL + * + * @since 4.1 */ - public void log(UWSLogType type, final String msg, final Throwable t){ + protected final void log(LogLevel level, final String context, final String event, final String ID, final String message, final String addColumn, final Throwable error){ + // If no message and no error is provided, nothing to log, so nothing to write: + if ((message == null || message.length() <= 0) && error == null) + return; + // If the type is missing: - if (type == null) - type = (t != null) ? UWSLogType.ERROR : UWSLogType.INFO; + if (level == null) + level = (error != null) ? LogLevel.ERROR : LogLevel.INFO; - PrintWriter out = getOutput(type); + // Log or not? + if (!canLog(level)) + return; + + StringBuffer buf = new StringBuffer(); // Print the date/time: - out.print(dateFormat.format(new Date())); - out.print('\t'); - out.print(String.format("%1$-13s", type.toString())); - out.print('\t'); + buf.append(dateFormat.format(new Date())).append('\t'); + // Print the level of error (debug, info, warning, error, fatal): + buf.append(level.toString()).append('\t'); + // Print the context of the error (uws, thread, job, http): + buf.append((context == null) ? "" : context).append('\t'); + // Print the context event: + buf.append((event == null) ? "" : event).append('\t'); + // Print an ID (jobID, requestID): + buf.append((ID == null) ? "" : ID).append('\t'); // Print the message: - if (msg != null) - out.println(msg); - else if (t != null && t instanceof UWSException){ - UWSException uwsEx = (UWSException)t; - out.println("EXCEPTION " + uwsEx.getClass().getName() + "\t" + uwsEx.getUWSErrorType() + "\tHTTP-" + uwsEx.getHttpErrorCode() + "\t" + uwsEx.getMessage()); - }else - out.println(); + if (message != null) + buf.append(normalizeMessage(message)); + else if (error != null) + buf.append("[EXCEPTION ").append(error.getClass().getName()).append("] ").append(normalizeMessage(error.getMessage())); + // Print the additional column, if any: + if (addColumn != null) + buf.append('\t').append(normalizeMessage(addColumn)); + + // Write the whole log line: + PrintWriter out = getOutput(level, context); + out.println(buf.toString()); + // Print the stack trace, if any: - if (t != null) - t.printStackTrace(out); + printException(error, out); + out.flush(); } + /** + *

    Format and print the given exception inside the given writer.

    + * + *

    This function does nothing if the given error is NULL.

    + * + *

    The full stack trace is printed ONLY for unknown exceptions.

    + * + *

    The printed text has the following format for known exceptions:

    + *
    +	 * Caused by a {ExceptionClassName} {ExceptionOrigin}
    +	 *     {ExceptionMessage}
    +	 * 
    + * + *

    The printed text has the following format for unknown exceptions:

    + *
    +	 * Caused by a {ExceptionFullStackTrace}
    +	 * 
    + * + * @param error The exception to print. + * @param out The output in which the exception must be written. + * + * @see #getExceptionOrigin(Throwable) + * + * @since 4.1 + */ + protected void printException(final Throwable error, final PrintWriter out){ + if (error != null){ + if (error instanceof UWSException){ + if (error.getCause() != null) + printException(error.getCause(), out); + else{ + out.println("Caused by a " + error.getClass().getName() + " " + getExceptionOrigin(error)); + if (error.getMessage() != null) + out.println("\t" + error.getMessage()); + } + }else{ + out.print("Caused by a "); + error.printStackTrace(out); + } + } + } + + /** + *

    Format and return the origin of the given error. + * "Origin" means here: "where the error has been thrown from?" (from which class? method? file? line?).

    + * + *

    This function does nothing if the given error is NULL or if the origin information is missing.

    + * + *

    The returned text has the following format:

    + *
    +	 * at {OriginClass}.{OriginMethod}({OriginFile}:{OriginLine})
    +	 * 
    + * + *

    {OriginFile} and {OriginLine} are written only if provided.

    + * + * @param error Error whose the origin should be returned. + * + * @return A string which contains formatted information about the origin of the given error. + * + * @since 4.1 + */ + protected String getExceptionOrigin(final Throwable error){ + if (error != null && error.getStackTrace() != null && error.getStackTrace().length > 0){ + StackTraceElement src = error.getStackTrace()[0]; + return "at " + src.getClassName() + "." + src.getMethodName() + ((src.getFileName() != null) ? "(" + src.getFileName() + ((src.getLineNumber() >= 0) ? ":" + src.getLineNumber() : "") + ")" : ""); + }else + return ""; + } + @Override public void debug(String msg){ - log(UWSLogType.DEBUG, msg, null); + log(LogLevel.DEBUG, null, msg, null); } @Override public void debug(Throwable t){ - log(UWSLogType.DEBUG, null, t); + log(LogLevel.DEBUG, null, null, t); } @Override public void debug(String msg, Throwable t){ - log(UWSLogType.DEBUG, msg, t); + log(LogLevel.DEBUG, null, msg, t); } @Override public void info(String msg){ - log(UWSLogType.INFO, msg, null); + log(LogLevel.INFO, null, msg, null); } @Override public void warning(String msg){ - log(UWSLogType.WARNING, msg, null); + log(LogLevel.WARNING, null, msg, null); } @Override public void error(String msg){ - log(UWSLogType.ERROR, msg, null); + log(LogLevel.ERROR, null, msg, null); } @Override public void error(Throwable t){ - log(UWSLogType.ERROR, null, t); + log(LogLevel.ERROR, null, null, t); } @Override public void error(String msg, Throwable t){ - log(UWSLogType.ERROR, msg, t); + log(LogLevel.ERROR, null, msg, t); } - /* **************************** */ - /* METHODS ABOUT THE UWS STATUS */ - /* **************************** */ + /* ************* */ + /* HTTP ACTIVITY */ + /* ************* */ /** - * Gets the name of the UWS, if any. - * - * @param uws UWS whose the name must be returned. + *

    A message/error logged with this function will have the following format:

    + *
    <TIMESTAMP>	<LEVEL>	HTTP	REQUEST_RECEIVED	<REQUEST_ID>	<MESSAGE>	<HTTP_METHOD> in <CONTENT_TYPE> at <URL> from <IP_ADDR> using <USER_AGENT> with parameters (<PARAM1>=<VAL1>&...)
    * - * @return Name of the given UWS (followed by a space: " ") or an empty string (""). + * @see uws.service.log.UWSLog#logHttp(uws.service.log.UWSLog.LogLevel, javax.servlet.http.HttpServletRequest, java.lang.String, java.lang.String, java.lang.Throwable) */ - protected final static String getUWSName(final UWS uws){ - return ((uws != null && uws.getName() != null && !uws.getName().trim().isEmpty()) ? (uws.getName() + " ") : ""); - } - @Override - public void uwsInitialized(UWS uws){ - if (uws != null){ - String msg = "UWS " + getUWSName(uws) + "INITIALIZED !"; - info(msg); - log(UWSLogType.HTTP_ACTIVITY, msg, null); - } - } + public void logHttp(LogLevel level, final HttpServletRequest request, final String requestId, final String message, final Throwable error){ + // IF A REQUEST IS PROVIDED, write its details after the message in a new column: + if (request != null){ + // If the type is missing: + if (level == null) + level = (error != null) ? LogLevel.ERROR : LogLevel.INFO; - @Override - public void ownerJobsSaved(JobOwner owner, int[] report){ - if (owner != null){ - String strReport = (report == null || report.length != 2) ? "???" : (report[0] + "/" + report[1]); - String ownerPseudo = (owner.getPseudo() != null && !owner.getPseudo().trim().isEmpty() && !owner.getID().equals(owner.getPseudo())) ? (" (alias " + owner.getPseudo() + ")") : ""; - info(strReport + " saved jobs for the user " + owner.getID() + ownerPseudo + " !"); - } - } + // Log or not? + if (!canLog(level)) + return; - @Override - public void uwsRestored(UWS uws, int[] report){ - if (uws != null){ - String strReport = (report == null || report.length != 4) ? "[Unknown report format !]" : (report[0] + "/" + report[1] + " restored jobs and " + report[2] + "/" + report[3] + " restored users"); - info("UWS " + getUWSName(uws) + "RESTORED => " + strReport); - } - } + StringBuffer str = new StringBuffer(); - @Override - public void uwsSaved(UWS uws, int[] report){ - if (uws != null){ - String strReport = (report == null || report.length != 4) ? "[Unknown report format !]" : (report[0] + "/" + report[1] + " saved jobs and " + report[2] + "/" + report[3] + " saved users"); - info("UWS " + getUWSName(uws) + "SAVED => " + strReport); - } - } + // Write the request type, content type and the URL: + str.append(request.getMethod()); + str.append(" as "); + if (request.getContentType() != null){ + if (request.getContentType().indexOf(';') > 0) + str.append(request.getContentType().substring(0, request.getContentType().indexOf(';'))); + else + str.append(request.getContentType()); + } + str.append(" at ").append(request.getRequestURL()); - @Override - public void jobCreated(UWSJob job){ - if (job != null){ - String jlName = (job.getJobList() != null) ? job.getJobList().getName() : null; - info("JOB " + job.getJobId() + " CREATED" + ((jlName != null) ? (" and added into " + jlName) : "") + " !"); - } - } + // Write the IP address: + str.append(" from ").append(request.getRemoteAddr()); - @Override - public void jobDestroyed(UWSJob job, JobList jl){ - if (job != null){ - String jlName = (jl != null) ? jl.getName() : null; - info("JOB " + job.getJobId() + " DESTROYED" + ((jlName != null) ? (" and removed from " + jlName) : "") + " !"); - } - } + // Write the user agent: + str.append(" using ").append(request.getHeader("User-Agent") == null ? "" : request.getHeader("User-Agent")); - @Override - public void jobStarted(UWSJob job){ - if (job != null){ - info("JOB " + job.getJobId() + " STARTED !"); + // Write the posted parameters: + str.append(" with parameters ("); + Map params = UWSToolBox.getParamsMap(request); + int i = -1; + for(Entry p : params.entrySet()){ + if (++i > 0) + str.append('&'); + str.append(p.getKey()).append('=').append((p.getValue() != null) ? p.getValue() : ""); + } + str.append(')'); + + // Send the log message to the log file: + log(level, "HTTP", "REQUEST_RECEIVED", requestId, (message != null ? message : str.toString()), (message != null ? str.toString() : null), error); } + // OTHERWISE, just write the given message: + else + log(level, "HTTP", "REQUEST_RECEIVED", requestId, message, null, error); } + /** + *

    A message/error logged with this function will have the following format:

    + *
    <TIMESTAMP>	<LEVEL>	HTTP	RESPONSE_SENT	<REQUEST_ID>	<MESSAGE>	HTTP-<STATUS_CODE> to the user <USER> as <CONTENT_TYPE>
    + *

    ,where <USER> may be either "(id:<USER_ID>;pseudo:<USER_PSEUDO>)" or "ANONYMOUS".

    + * + * @see uws.service.log.UWSLog#logHttp(uws.service.log.UWSLog.LogLevel, javax.servlet.http.HttpServletResponse, java.lang.String, uws.job.user.JobOwner, java.lang.String, java.lang.Throwable) + */ @Override - public void jobFinished(UWSJob job){ - if (job != null){ - long endTime = (job.getEndTime() == null) ? -1 : job.getEndTime().getTime(); - long startTime = (job.getStartTime() == null) ? -1 : job.getStartTime().getTime(); - long duration = (endTime > 0 && startTime > 0) ? (endTime - startTime) : -1; - info("JOB " + job.getJobId() + " FINISHED with the phase " + job.getPhase() + ((duration > 0) ? " after an execution of " + duration + "ms" : "") + " !"); - } - } + public void logHttp(LogLevel level, HttpServletResponse response, String requestId, JobOwner user, String message, Throwable error){ + if (response != null){ + // If the type is missing: + if (level == null) + level = (error != null) ? LogLevel.ERROR : LogLevel.INFO; - /* ************* */ - /* HTTP ACTIVITY */ - /* ************* */ + // Log or not? + if (!canLog(level)) + return; - @SuppressWarnings("unchecked") - public void httpRequest(final HttpServletRequest request, final JobOwner user, final String uwsAction, final int responseStatusCode, final String responseMsg, final Throwable responseError){ - if (request != null){ StringBuffer str = new StringBuffer(); - // Write the executed UWS action: - if (uwsAction == null || uwsAction.trim().isEmpty()) - str.append("???"); - else - str.append(uwsAction); - str.append('\t'); - // Write the response status code: - if (responseStatusCode > 0) - str.append("HTTP-").append(responseStatusCode); - else - str.append("HTTP-???"); - str.append('\t'); - - // Write the "response" message: - if (responseMsg != null) - str.append('[').append(responseMsg).append(']'); - else - str.append("[]"); - str.append('\t'); - - // Write the request type and the URL: - str.append("[HTTP-").append(request.getMethod()).append("] ").append(request.getRequestURL()).append('\t'); - - // Write the posted parameters: - Enumeration paramNames = request.getParameterNames(); - while(paramNames.hasMoreElements()){ - String param = paramNames.nextElement(); - String paramValue = request.getParameter(param); - if (paramValue != null) - paramValue = paramValue.replaceAll("[\t\n\r]", " "); - else - paramValue = ""; - str.append(param).append('=').append(paramValue); - if (paramNames.hasMoreElements()) - str.append('&'); - } - str.append('\t'); + str.append("HTTP-").append(response.getStatus()); - // Write the IP address and the corresponding user: - str.append(request.getRemoteAddr()).append('['); + // Write the user to whom the response is sent: + str.append(" to the user "); if (user != null){ - str.append("id:").append(user.getID()); + str.append("(id:").append(user.getID()); if (user.getPseudo() != null) str.append(";pseudo:").append(user.getPseudo()); + str.append(')'); }else - str.append("???"); - str.append("]\t"); + str.append("ANONYMOUS"); - // Write the user agent: - str.append(request.getHeader("User-Agent")); + // Write the response's MIME type: + if (response.getContentType() != null) + str.append(" as ").append(response.getContentType()); // Send the log message to the log file: - log(UWSLogType.HTTP_ACTIVITY, str.toString(), (responseStatusCode >= minResponseCodeForStackTrace) ? responseError : null); + log(level, "HTTP", "RESPONSE_SENT", requestId, message, str.toString(), error); } + // OTHERWISE, just write the given message: + else + log(level, "HTTP", "RESPONSE_SENT", requestId, message, null, error); } - /* ********************** */ - /* THREAD STATUS MESSAGES */ - /* ********************** */ + /* ************ */ + /* UWS ACTIVITY */ + /* ************ */ @Override - public void threadStarted(Thread t, String task){ - if (t != null) - info("THREAD " + t.getId() + " STARTED\t" + t.getName() + "\t" + t.getState() + "\t" + t.getThreadGroup().activeCount() + " active threads"); + public void logUWS(LogLevel level, Object obj, String event, String message, Throwable error){ + // If the type is missing: + if (level == null) + level = (error != null) ? LogLevel.ERROR : LogLevel.INFO; + + // Log or not? + if (!canLog(level)) + return; + + // CASE "BACKUPED": Append to the message the backup report: + String report = null; + if (event != null && event.equalsIgnoreCase("BACKUPED") && obj != null && obj.getClass().getName().equals("[I")){ + int[] backupReport = (int[])obj; + report = "(" + backupReport[0] + "/" + backupReport[1] + " jobs backuped ; " + backupReport[2] + "/" + backupReport[3] + " users backuped)"; + }else if (event != null && event.equalsIgnoreCase("RESTORED") && obj != null && obj.getClass().getName().equals("[I")){ + int[] restoreReport = (int[])obj; + report = "(" + restoreReport[0] + "/" + restoreReport[1] + " jobs restored ; " + restoreReport[2] + "/" + restoreReport[3] + " users restored)"; + } + + // Log the message + log(level, "UWS", event, null, message, report, error); } + /* ************ */ + /* JOB ACTIVITY */ + /* ************ */ + @Override - public void threadFinished(Thread t, String task){ - if (t != null) - info("THREAD " + t.getId() + " ENDED\t" + t.getName() + "\t" + t.getState() + "\t" + t.getThreadGroup().activeCount() + " active threads"); + public void logJob(LogLevel level, UWSJob job, String event, String message, Throwable error){ + log(level, "JOB", event, (job == null) ? null : job.getJobId(), message, null, error); } + /* ********************** */ + /* THREAD STATUS MESSAGES */ + /* ********************** */ + @Override - public void threadInterrupted(Thread t, String task, Throwable error){ - if (t != null){ - if (error == null || error instanceof InterruptedException) - info("THREAD " + t.getId() + " CANCELLED\t" + t.getName() + "\t" + t.getState() + "\t" + t.getThreadGroup().activeCount() + " active threads"); - else - error("THREAD " + t.getId() + " INTERRUPTED\t" + t.getName() + "\t" + t.getState() + "\t" + t.getThreadGroup().activeCount() + " active threads", error); - } + public void logThread(LogLevel level, Thread thread, String event, String message, Throwable error){ + if (thread != null){ + // If the type is missing: + if (level == null) + level = (error != null) ? LogLevel.ERROR : LogLevel.INFO; + + // Log or not? + if (!canLog(level)) + return; + + StringBuffer str = new StringBuffer(); + + // Write the thread name and ID: + str.append(thread.getName()).append(" (thread ID: ").append(thread.getId()).append(")"); + + // Write the thread state: + str.append(" is ").append(thread.getState()); + + // Write its thread group name: + str.append(" in the group " + thread.getThreadGroup().getName()); + + // Write the number of active threads: + str.append(" where ").append(thread.getThreadGroup().activeCount()).append(" threads are active"); + + log(level, "THREAD", event, thread.getName(), message, str.toString(), error); + + }else + log(level, "THREAD", event, null, message, null, error); } } diff --git a/src/uws/service/log/UWSLog.java b/src/uws/service/log/UWSLog.java index 350fe02..0448234 100644 --- a/src/uws/service/log/UWSLog.java +++ b/src/uws/service/log/UWSLog.java @@ -1,5 +1,19 @@ package uws.service.log; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.json.JSONArray; +import org.json.JSONObject; + +import uws.job.ErrorSummary; +import uws.job.JobList; +import uws.job.Result; +import uws.job.UWSJob; +import uws.job.user.JobOwner; +import uws.service.UWS; +import uws.service.UWSUrl; + /* * This file is part of UWSLibrary. * @@ -16,174 +30,276 @@ package uws.service.log; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see . * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ -import javax.servlet.http.HttpServletRequest; - -import uws.job.JobList; -import uws.job.UWSJob; - -import uws.job.user.JobOwner; - -import uws.service.UWS; - /** - * Lets logging any kind of message about a UWS. + * Let log any kind of message about a UWS service. * - * @author Grégory Mantelet (CDS) - * @version 05/2012 + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (12/2014) */ public interface UWSLog { + /** + * Indicate the level of the error: debug, info, warning or error. + * + * @author Grégory Mantelet (ARI) + * @version 4.1 (09/2014) + * @since 4.1 + */ + public static enum LogLevel{ + DEBUG, INFO, WARNING, ERROR, FATAL; + } + /* *********************** */ /* GENERAL LOGGING METHODS */ /* *********************** */ /** - * Logs a debug message. + *

    Generic way to log a message and/or an exception.

    + * + *

    Note: + * The other functions of this class or extension, MAY be equivalent to a call to this function with some specific parameter values. + * It should be especially the case for the debug(...), info(...), warning(...) and error(...) functions. + *

    + * + * @param level Level of the error (info, warning, error, ...). SHOULD NOT be NULL, but if NULL anyway, the level SHOULD be considered as INFO + * @param context Context of the log item (HTTP, Thread, Job, UWS, ...). MAY be NULL + * @param message Message to log. MAY be NULL + * @param error Error/Exception to log. MAY be NULL + * + * @since 4.1 + */ + public void log(final LogLevel level, final String context, final String message, final Throwable error); + + /** + *

    Logs a debug message.

    + * + *

    Note: + * This function should be equals to: log(LogLevel.WARNING, null, msg, null). + *

    + * * @param msg A DEBUG message. */ public void debug(final String msg); /** - * Logs an exception as a debug message. + *

    Logs an exception as a debug message.

    + * + *

    Note: + * This function should be equals to: log(LogLevel.WARNING, null, null, t). + *

    + * * @param t An exception. */ public void debug(final Throwable t); /** - * Logs a full (message+exception) debug message. + *

    Logs a full (message+exception) debug message.

    + * + *

    Note: + * This function should be equals to: log(LogLevel.WARNING, null, msg, t). + *

    + * * @param msg A DEBUG message. * @param t An exception. */ public void debug(final String msg, final Throwable t); /** - * Logs the given information. + *

    Logs the given information.

    + * + *

    Note: + * This function should be equals to: log(LogLevel.INFO, null, msg, null). + *

    + * * @param msg An INFO message. */ public void info(final String msg); /** - * Logs the given warning. + *

    Logs the given warning.

    + * + *

    Note: + * This function should be equals to: log(LogLevel.WARNING, null, msg, null). + *

    + * * @param msg A WARNING message. */ public void warning(final String msg); /** - * Logs the given error. + *

    Logs the given error.

    + * + *

    Note: + * This function should be equals to: log(LogLevel.ERROR, null, msg, null). + *

    + * * @param msg An ERROR message. */ public void error(final String msg); /** - * Logs the given exception as an error. + *

    Logs the given exception as an error.

    + * + *

    Note: + * This function should be equals to: log(LogLevel.ERROR, null, null, t). + *

    + * * @param t An exception. */ public void error(final Throwable t); /** - * Logs a full (message+exception) error message. + *

    Logs a full (message+exception) error message.

    + * + *

    Note: + * This function should be equals to: log(LogLevel.ERROR, null, msg, t). + *

    + * * @param msg An ERROR message. * @param t An exception. */ public void error(final String msg, final Throwable t); - /* *************************************** */ - /* LOGGING METHODS TO WATCH THE UWS STATUS */ - /* *************************************** */ - - /** - *

    Logs the fact that the given UWS has just been initialized.

    - *

    note: Theoretically, no restoration has been done when this method is called.

    - * @param uws The UWS which has just been initialized. - */ - public void uwsInitialized(final UWS uws); - - /** - * Logs the fact that the given UWS has just been restored. - * @param uws The restored UWS. - * @param report Report of the restoration (in the order: nb restored jobs, nb jobs, nb restored users, nb users). - */ - public void uwsRestored(final UWS uws, final int[] report); - - /** - * Logs the fact that the given UWS has just been saved. - * @param uws The saved UWS. - * @param report Report of the save (in the order: nb saved jobs, nb jobs, nb saved users, nb users). - */ - public void uwsSaved(final UWS uws, final int[] report); - - /** - * Logs the fact that all the jobs of the given user have just been saved. - * @param owner The owner whose all the jobs have just been saved. - * @param report Report of the save (in the order: nb saved jobs, nb jobs). - */ - public void ownerJobsSaved(final JobOwner owner, final int[] report); - - /** - * Logs the fact that the given job has just been created. - * @param job The created job. - */ - public void jobCreated(final UWSJob job); + /* ****************** */ + /* SPECIFIC FUNCTIONS */ + /* ****************** */ /** - * Logs the fact that the given job has just started. - * @param job The started job. + *

    Log a message and/or an error in the general context of UWS.

    + * + *

    + * One of the parameter is of type {@link Object}. This object can be used to provide more information to the log function + * in order to describe as much as possible the state and/or result event. + *

    + * + *

    List of all events sent by the library (case sensitive):

    + *
      + *
    • INIT (with "obj" as an instance of {@link UWS} except in case of error where "obj" is NULL)
    • + *
    • ADD_JOB_LIST (with "obj" as an instance of {@link JobList})
    • + *
    • DESTROY_JOB_LIST (with "obj" as an instance of {@link JobList})
    • + *
    • DESTROY_JOB (with "obj" as an instance of {@link UWSUrl})
    • + *
    • SERIALIZE (with "obj" as an instance of {@link UWSUrl})
    • + *
    • SET_PARAM (with "obj" as an instance of {@link HttpServletRequest} in case of error)
    • + *
    • GET_RESULT (with "obj" as an instance of {@link Result})
    • + *
    • GET_ERROR (with "obj" as an instance of {@link ErrorSummary})
    • + *
    • RESTORATION (with "obj" the raw object to de-serialize (may be {@link JSONObject} or {@link JSONArray} or NULL))
    • + *
    • BACKUP (with "obj" the object to backup ; may be {@link JobOwner}, a {@link UWSJob}, ...)
    • + *
    • RESTORED (with "obj" as an integer array of 4 items: nb of restored jobs, total nb of jobs, nb of restored users, total nb of users)
    • + *
    • BACKUPED (with "obj" as an integer array of 4 items: nb of saved jobs, total nb of jobs, nb of saved users, total nb of users or with just 2 items (the two last ones))
    • + *
    • FORMAT_ERROR (with a NULL "obj")
    • + *
    • STOP (with "obj" as an instance of {@link UWS})
    • + *
    + * + * @param level Level of the log (info, warning, error, ...). SHOULD NOT be NULL, but if NULL anyway, the level SHOULD be considered as INFO + * @param obj Object providing more information about the event/object at the origin of this log. MAY be NULL + * @param event Event at the origin of this log or action currently executed by UWS while this log is sent. MAY be NULL + * @param message Message to log. MAY be NULL + * @param error Error/Exception to log. MAY be NULL + * + * @since 4.1 */ - public void jobStarted(final UWSJob job); + public void logUWS(final LogLevel level, final Object obj, final String event, final String message, final Throwable error); /** - * Logs the fact that the given job has just finished. - * @param job The finished job. + *

    Log a message and/or an error in the HTTP context. + * This log function is called when a request is received by the service. Consequently, the event is: REQUEST_RECEIVED.

    + * + *

    Note: + * When a request is received, this function is called, and then, when the response has been written and sent to the client, + * {@link #logHttp(LogLevel, HttpServletResponse, String, JobOwner, String, Throwable)} should be called. + * These functions should always work together. + *

    + * + * @param level Level of the log (info, warning, error, ...). SHOULD NOT be NULL, but if NULL anyway, the level SHOULD be considered as INFO + * @param request HTTP request received by the service. SHOULD NOT be NULL + * @param requestId ID to use to identify this request until its response is sent. + * @param message Message to log. MAY be NULL + * @param error Error/Exception to log. MAY be NULL + * + * @see #logHttp(LogLevel, HttpServletResponse, String, JobOwner, String, Throwable) + * + * @since 4.1 */ - public void jobFinished(final UWSJob job); + public void logHttp(final LogLevel level, final HttpServletRequest request, final String requestId, final String message, final Throwable error); /** - * Logs the fact that the given job has just been destroyed. - * @param job The destroyed job. - * @param jl The job list from which the given job has just been removed. + *

    Log a message and/or an error in the HTTP context. + * This log function is called when a response is sent to the client by the service. Consequently, the event is: RESPONSE_SENT.

    + * + *

    Note: + * When a request is received, {@link #logHttp(LogLevel, HttpServletRequest, String, String, Throwable)} is called, and then, + * when the response has been written and sent to the client, this function should be called. + * These functions should always work together. + *

    + * + * @param level Level of the log (info, warning, error, ...). SHOULD NOT be NULL, but if NULL anyway, the level SHOULD be considered as INFO + * @param response HTTP response sent by the service to the client. MAY be NULL if an error occurs while writing the response + * @param requestId ID to use to identify the request to which the given response is answering. + * @param user Identified user which has sent the received request. + * @param message Message to log. MAY be NULL + * @param error Error/Exception to log. MAY be NULL + * + * @see #logHttp(LogLevel, HttpServletRequest, String, String, Throwable) + * + * @since 4.1 */ - public void jobDestroyed(final UWSJob job, final JobList jl); - - /* ************* */ - /* HTTP ACTIVITY */ - /* ************* */ + public void logHttp(final LogLevel level, final HttpServletResponse response, final String requestId, final JobOwner user, final String message, final Throwable error); /** - * Logs any HTTP request received by the UWS and also the send response. - * @param request The HTTP request received by the UWS. - * @param user The identified user which sends this request. (MAY BE NULL) - * @param uwsAction The identified UWS action. (MAY BE NULL) - * @param responseStatusCode The HTTP status code of the response given by the UWS. - * @param responseMsg The message (or a summary of the message) returned by the UWS. (MAY BE NULL) - * @param responseError The error sent by the UWS. (MAY BE NULL) + *

    Log a message and/or an error in the JOB context.

    + * + *

    List of all events sent by the library (case sensitive):

    + *
      + *
    • CREATED
    • + *
    • QUEUE
    • + *
    • START
    • + *
    • ABORT
    • + *
    • ERROR
    • + *
    • EXECUTING
    • + *
    • CHANGE_PHASE
    • + *
    • NOTIFY
    • + *
    • END
    • + *
    • SERIALIZE
    • + *
    • MOVE_UPLOAD
    • + *
    • ADD_RESULT
    • + *
    • SET_DESTRUCTION
    • + *
    • SET_ERROR
    • + *
    • CLEAR_RESOURCES
    • + *
    • DESTROY
    • + *
    + * + * @param level Level of the log (info, warning, error, ...). SHOULD NOT be NULL, but if NULL anyway, the level SHOULD be considered as INFO + * @param job Job from which this log comes. MAY be NULL + * @param event Event at the origin of this log or action executed by the given job while this log is sent. MAY be NULL + * @param message Message to log. MAY be NULL + * @param error Error/Exception to log. MAY be NULL + * + * @since 4.1 */ - public void httpRequest(final HttpServletRequest request, final JobOwner user, final String uwsAction, final int responseStatusCode, final String responseMsg, final Throwable responseError); + public void logJob(final LogLevel level, final UWSJob job, final String event, final String message, final Throwable error); - /* ********************** */ - /* THREAD STATUS MESSAGES */ - /* ********************** */ /** - * Logs the fact that the given thread has just started. - * @param t The started thread. - * @param task Name/Description of the task that the given thread is executing. + *

    Log a message and/or an error in the THREAD context.

    + * + *

    List of all events sent by the library (case sensitive):

    + *
      + *
    • START
    • + *
    • SET_ERROR
    • + *
    • END
    • + *
    + * + * @param level Level of the log (info, warning, error, ...). SHOULD NOT be NULL, but if NULL anyway, the level SHOULD be considered as INFO + * @param thread Thread from which this log comes. MAY be NULL + * @param event Event at the origin of this log or action currently executed by the given thread while this log is sent. MAY be NULL + * @param message Message to log. MAY be NULL + * @param error Error/Exception to log. MAY be NULL + * + * @since 4.1 */ - public void threadStarted(final Thread t, final String task); + public void logThread(final LogLevel level, final Thread thread, final String event, final String message, final Throwable error); - /** - * Logs the fact that the given thread has just been interrupted. - * @param t The interrupted thread. - * @param task Name/Description of the task that the given thread was trying to execute. - * @param error Exception that has interrupted the given thread. - */ - public void threadInterrupted(final Thread t, final String task, final Throwable error); - - /** - * Logs the fact that the given thread has just finished. - * @param t The finished thread. - * @param task Name/Description of the task that the given thread was executing. - */ - public void threadFinished(final Thread t, final String task); } diff --git a/src/uws/service/log/UWSLogType.java b/src/uws/service/log/UWSLogType.java deleted file mode 100644 index 1aed19b..0000000 --- a/src/uws/service/log/UWSLogType.java +++ /dev/null @@ -1,53 +0,0 @@ -package uws.service.log; - -/* - * This file is part of UWSLibrary. - * - * UWSLibrary is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * UWSLibrary is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with UWSLibrary. If not, see . - * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) - */ - -/** - * Different types of log messages. - * - * @author Grégory Mantelet (CDS) - * @version 06/2012 - * - * @see UWSLog - * @see DefaultUWSLog - */ -public enum UWSLogType{ - HTTP_ACTIVITY, DEBUG, INFO, WARNING, ERROR, CUSTOM; - - protected String customType = this.name(); - - public final String getCustomType(){ - return customType; - } - - public static final UWSLogType createCustomLogType(final String customType){ - UWSLogType type = UWSLogType.CUSTOM; - type.customType = customType; - return type; - } - - @Override - public String toString(){ - if (this == CUSTOM) - return customType; - else - return name(); - } -} diff --git a/src/uws/service/request/FormEncodedParser.java b/src/uws/service/request/FormEncodedParser.java new file mode 100644 index 0000000..d9882d3 --- /dev/null +++ b/src/uws/service/request/FormEncodedParser.java @@ -0,0 +1,178 @@ +package uws.service.request; + +/* + * This file is part of UWSLibrary. + * + * UWSLibrary is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * UWSLibrary is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with UWSLibrary. If not, see . + * + * Copyright 2014 - Astronomisches Rechen Institut (ARI) + */ + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.nio.charset.Charset; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.Scanner; + +import javax.servlet.http.HttpServletRequest; + +import uws.UWSException; + +/** + *

    Extract parameters encoded using the HTTP-GET method or the Content-type application/x-www-form-urlencoded + * with the HTTP-POST or HTTP-PUT method in an {@link HttpServletRequest}.

    + * + *

    + * By default, this {@link RequestParser} overwrite parameter occurrences in the map: that's to say if a parameter is provided several times, + * only the last value will be kept. This behavior can be changed by overwriting the function {@link #consumeParameter(String, Object, Map)} + * of this class. + *

    + * + *

    Note: + * When HTTP-POST is used, these parameters are actually already extracted by the server application (like Apache/Tomcat) + * and are available with {@link HttpServletRequest#getParameterMap()}. + * However, when using HTTP-PUT, the parameters are extracted manually from the request content. + *

    + * + * @author Grégory Mantelet (ARI) + * @version 4.1 (11/2014) + * @since 4.1 + */ +public class FormEncodedParser implements RequestParser { + + /** HTTP content-type for HTTP request formated in url-form-encoded. */ + public final static String EXPECTED_CONTENT_TYPE = "application/x-www-form-urlencoded"; + + @Override + public final Map parse(HttpServletRequest request) throws UWSException{ + if (request == null) + return new HashMap(); + + HashMap params = new HashMap(); + + // Normal extraction for HTTP-POST and other HTTP methods: + if (request.getMethod() == null || !request.getMethod().equalsIgnoreCase("put")){ + Enumeration names = request.getParameterNames(); + String paramName; + String[] values; + int i; + while(names.hasMoreElements()){ + paramName = names.nextElement(); + values = request.getParameterValues(paramName); + // search for the last non-null occurrence: + i = values.length - 1; + while(i >= 0 && values[i] == null) + i--; + // if there is one, keep it: + if (i >= 0) + consumeParameter(paramName, values[i], params); + } + } + /* Parameters are not extracted when using the HTTP-PUT method. + * This block is doing this extraction manually. */ + else{ + InputStream input = null; + try{ + + // Get the character encoding: + String charEncoding = request.getCharacterEncoding(); + try{ + if (charEncoding == null || charEncoding.trim().length() == 0 || Charset.isSupported(charEncoding)) + charEncoding = "UTF-8"; + }catch(Exception ex){ + charEncoding = "UTF-8"; + } + + // Get a stream on the request content: + input = new BufferedInputStream(request.getInputStream()); + // Read the stream by iterating on each parameter pairs: + Scanner scanner = new Scanner(input); + scanner.useDelimiter("&"); + String pair; + int indSep; + while(scanner.hasNext()){ + // get the pair: + pair = scanner.next(); + // split it between the parameter name and value: + indSep = pair.indexOf('='); + try{ + if (indSep >= 0) + consumeParameter(URLDecoder.decode(pair.substring(0, indSep), charEncoding), URLDecoder.decode(pair.substring(indSep + 1), charEncoding), params); + else + consumeParameter(URLDecoder.decode(pair, charEncoding), "", params); + }catch(UnsupportedEncodingException uee){ + if (indSep >= 0) + consumeParameter(pair.substring(0, indSep), pair.substring(indSep + 1), params); + else + consumeParameter(pair, "", params); + } + } + + }catch(IOException ioe){}finally{ + if (input != null){ + try{ + input.close(); + }catch(IOException ioe2){} + } + } + } + + return params; + } + + /** + *

    Consume the specified parameter: add it inside the given map.

    + * + *

    + * By default, this function is just putting the given value inside the map. So, if the parameter already exists in the map, + * its old value will be overwritten by the given one. + *

    + * + * @param name Name of the parameter to consume. + * @param value Its value. + * @param allParams The list of all parameters read until now. + */ + protected void consumeParameter(final String name, final Object value, final Map allParams){ + allParams.put(name, value); + } + + /** + *

    Utility method that determines whether the content of the given request is a application/x-www-form-urlencoded.

    + * + *

    Important: + * This function just test the content-type of the request. The HTTP method (e.g. GET, POST, ...) is not tested. + *

    + * + * @param request The servlet request to be evaluated. Must be non-null. + * + * @return true if the request is url-form-encoded, + * false otherwise. + */ + public final static boolean isFormEncodedRequest(final HttpServletRequest request){ + // Extract the content type and determine if it is a url-form-encoded request: + String contentType = request.getContentType(); + if (contentType == null) + return false; + else if (contentType.toLowerCase().equals(EXPECTED_CONTENT_TYPE)) + return true; + else + return false; + } + +} diff --git a/src/uws/service/request/MultipartParser.java b/src/uws/service/request/MultipartParser.java new file mode 100644 index 0000000..a6760ac --- /dev/null +++ b/src/uws/service/request/MultipartParser.java @@ -0,0 +1,250 @@ +package uws.service.request; + +/* + * This file is part of UWSLibrary. + * + * UWSLibrary is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * UWSLibrary is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with UWSLibrary. If not, see . + * + * Copyright 2014 - Astronomisches Rechen Institut (ARI) + */ + +import java.io.File; +import java.io.IOException; +import java.util.Date; +import java.util.Enumeration; +import java.util.LinkedHashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + +import uws.UWSException; +import uws.service.UWS; +import uws.service.file.UWSFileManager; + +import com.oreilly.servlet.MultipartRequest; +import com.oreilly.servlet.multipart.FileRenamePolicy; + +/** + *

    Extract parameters encoded using the Content-type multipart/form-data + * in an {@link HttpServletRequest}.

    + * + *

    + * The created file(s) is(are) stored in the temporary upload directory ({@link UWSFileManager#TMP_UPLOAD_DIR} ; this attribute can be modified if needed). + * This directory is supposed to be emptied regularly in case it is forgotten at any moment by the UWS service implementation to delete unused request files. + *

    + * + *

    + * The size of the full request body is limited by the static attribute {@link #SIZE_LIMIT} before the creation of the file. + * Its default value is: {@link #DEFAULT_SIZE_LIMIT}={@value #DEFAULT_SIZE_LIMIT} bytes. + *

    + * + *

    + * By default, this {@link RequestParser} overwrite parameter occurrences in the map: that's to say if a parameter is provided several times, + * only the last value will be kept. This behavior can be changed by overwriting the function {@link #consumeParameter(String, Object, Map)} + * of this class. + *

    + * + * @author Grégory Mantelet (ARI) + * @version 4.1 (12/2014) + * @since 4.1 + */ +public class MultipartParser implements RequestParser { + + /** HTTP content-type for HTTP request formated in multipart. */ + public static final String EXPECTED_CONTENT_TYPE = "multipart/form-data"; + + /** Default maximum allowed size for an HTTP request content: 10 MiB. */ + public static final int DEFAULT_SIZE_LIMIT = 10 * 1024 * 1024; + + /**

    Maximum allowed size for an HTTP request content. Over this limit, an exception is thrown and the request is aborted.

    + *

    Note: + * The default value is {@link #DEFAULT_SIZE_LIMIT} (= {@value #DEFAULT_SIZE_LIMIT} MiB). + *

    + *

    Note: + * This limit is expressed in bytes and can not be negative. + * Its smallest possible value is 0. If the set value is though negative, + * it will be ignored and {@link #DEFAULT_SIZE_LIMIT} will be used instead. + *

    */ + public static int SIZE_LIMIT = DEFAULT_SIZE_LIMIT; + + /** Indicates whether this parser should allow inline files or not. */ + public final boolean allowUpload; + + /** File manager to use to create {@link UploadFile} instances. + * It is required by this new object to execute open, move and delete operations whenever it could be asked. */ + protected final UWSFileManager fileManager; + + /** + *

    Build a {@link MultipartParser} forbidding uploads (i.e. inline files).

    + * + *

    + * With this parser, when an upload (i.e. submitted inline files) is detected, an exception is thrown by {@link #parse(HttpServletRequest)} + * which cancels immediately the request. + *

    + */ + public MultipartParser(){ + this(false, null); + } + + /** + * Build a {@link MultipartParser} allowing uploads (i.e. inline files). + * + * @param fileManager The file manager to use in order to store any eventual upload. MUST NOT be NULL + */ + public MultipartParser(final UWSFileManager fileManager){ + this(true, fileManager); + } + + /** + *

    Build a {@link MultipartParser}.

    + * + *

    + * If the first parameter is false, then when an upload (i.e. submitted inline files) is detected, an exception is thrown + * by {@link #parse(HttpServletRequest)} which cancels immediately the request. + *

    + * + * @param uploadEnabled true to allow uploads (i.e. inline files), false otherwise. + * If false, the two other parameters are useless. + * @param fileManager The file manager to use in order to store any eventual upload. MUST NOT be NULL + */ + protected MultipartParser(final boolean uploadEnabled, final UWSFileManager fileManager){ + if (uploadEnabled && fileManager == null) + throw new NullPointerException("Missing file manager although the upload capability is enabled => can not create a MultipartParser!"); + + this.allowUpload = uploadEnabled; + this.fileManager = fileManager; + } + + @Override + @SuppressWarnings("unchecked") + public final Map parse(final HttpServletRequest request) throws UWSException{ + LinkedHashMap parameters = new LinkedHashMap(); + MultipartRequest multipart = null; + + try{ + + // Parse the request body: + multipart = new MultipartRequest(request, UWSFileManager.TMP_UPLOAD_DIR.getPath(), (SIZE_LIMIT < 0 ? DEFAULT_SIZE_LIMIT : SIZE_LIMIT), new FileRenamePolicy(){ + @Override + public File rename(File file){ + Object reqID = request.getAttribute(UWS.REQ_ATTRIBUTE_ID); + if (reqID == null || !(reqID instanceof String)) + reqID = (new Date()).getTime(); + char uniq = 'A'; + File f = new File(file.getParentFile(), "UPLOAD_" + reqID + uniq + "_" + file.getName()); + while(f.exists()){ + uniq++; + f = new File(file.getParentFile(), "UPLOAD_" + reqID + "_" + file.getName()); + } + return f; + } + }); + + // Extract all "normal" parameters: + String param; + Enumeration e = multipart.getParameterNames(); + while(e.hasMoreElements()){ + param = e.nextElement(); + for(String occurence : multipart.getParameterValues(param)) + consumeParameter(param, occurence, parameters); + } + + // Extract all inline files as additional parameters: + e = multipart.getFileNames(); + if (!allowUpload && e.hasMoreElements()) + throw new UWSException(UWSException.BAD_REQUEST, "Uploads are not allowed by this service!"); + while(e.hasMoreElements()){ + param = e.nextElement(); + if (multipart.getFile(param) == null) + continue; + + /* + * TODO !!!POSSIBLE ISSUE!!! + * MultipartRequest is not able to deal with multiple files having the same parameter name. However, all files are created/uploaded + * but only the last one is accessible through this object....so only the last can be deleted, which could be a problem later + * (hence the usage of the system temporary directory). + */ + + // build its description/pointer: + UploadFile lob = new UploadFile(param, multipart.getOriginalFileName(param), multipart.getFile(param).toURI().toString(), fileManager); + lob.mimeType = multipart.getContentType(param); + lob.length = multipart.getFile(param).length(); + // add it inside the parameters map: + consumeParameter(param, lob, parameters); + } + + }catch(IOException ioe){ + throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, ioe, "Internal Error => Impossible to extract parameters from the Multipart HTTP request!"); + }catch(IllegalArgumentException iae){ + String confError = iae.getMessage(); + if (UWSFileManager.TMP_UPLOAD_DIR == null) + confError = "Missing upload directory!"; + throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, iae, "Internal Error: Incorrect UPLOAD configuration: " + confError); + } + + return parameters; + } + + /** + *

    Consume the specified parameter: add it inside the given map.

    + * + *

    + * By default, this function is just putting the given value inside the map. So, if the parameter already exists in the map, + * its old value will be overwritten by the given one. + *

    + * + *

    Note: + * If the old value was a file, it will be deleted from the file system before its replacement in the map. + *

    + * + * @param name Name of the parameter to consume. + * @param value Its value. + * @param allParams The list of all parameters read until now. + */ + protected void consumeParameter(final String name, final Object value, final Map allParams){ + // If the old value was a file, delete it before replacing its value: + if (allParams.containsKey(name) && allParams.get(name) instanceof UploadFile){ + try{ + ((UploadFile)allParams.get(name)).deleteFile(); + }catch(IOException ioe){} + } + + // Put the given value in the given map: + allParams.put(name, value); + } + + /** + *

    Utility method that determines whether the content of the given request is a multipart/form-data.

    + * + *

    Important: + * This function just test the content-type of the request. The HTTP method (e.g. GET, POST, ...) is not tested. + *

    + * + * @param request The servlet request to be evaluated. Must be non-null. + * + * @return true if the request is multipart, + * false otherwise. + */ + public static final boolean isMultipartContent(final HttpServletRequest request){ + // Extract the content type and determine if it is a multipart request (its content type should start by multipart/form-data"): + String contentType = request.getContentType(); + if (contentType == null) + return false; + else if (contentType.toLowerCase().startsWith(EXPECTED_CONTENT_TYPE)) + return true; + else + return false; + } + +} diff --git a/src/uws/service/request/NoEncodingParser.java b/src/uws/service/request/NoEncodingParser.java new file mode 100644 index 0000000..6ebcbd1 --- /dev/null +++ b/src/uws/service/request/NoEncodingParser.java @@ -0,0 +1,165 @@ +package uws.service.request; + +/* + * This file is part of UWSLibrary. + * + * UWSLibrary is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * UWSLibrary is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with UWSLibrary. If not, see . + * + * Copyright 2014 - Astronomisches Rechen Institut (ARI) + */ + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + +import uws.UWSException; +import uws.service.UWS; +import uws.service.file.UWSFileManager; + +/** + *

    This parser merely copies the whole HTTP request content inside a file. + * It names this file: "JDL" (Job Description Language).

    + * + *

    + * The created file is stored in the temporary upload directory ({@link UWSFileManager#TMP_UPLOAD_DIR} ; this attribute can be modified if needed). + * This directory is supposed to be emptied regularly in case it is forgotten at any moment by the UWS service implementation to delete unused request files. + *

    + * + *

    + * The size of the JDL is limited by the static attribute {@link #SIZE_LIMIT} before the creation of the file. + * Its default value is: {@link #DEFAULT_SIZE_LIMIT}={@value #DEFAULT_SIZE_LIMIT} bytes. + *

    + * + * @author Grégory Mantelet (ARI) + * @version 4.1 (11/2014) + * @since 4.1 + */ +public class NoEncodingParser implements RequestParser { + + /** Default maximum allowed size for an HTTP request content: 2 MiB. */ + public static final int DEFAULT_SIZE_LIMIT = 2 * 1024 * 1024; + + /**

    Maximum allowed size for an HTTP request content. Over this limit, an exception is thrown and the request is aborted.

    + *

    Note: + * The default value is {@link #DEFAULT_SIZE_LIMIT} (= {@value #DEFAULT_SIZE_LIMIT} MiB). + *

    + *

    Note: + * This limit is expressed in bytes and can not be negative. + * Its smallest possible value is 0. If the set value is though negative, + * it will be ignored and {@link #DEFAULT_SIZE_LIMIT} will be used instead. + *

    */ + public static int SIZE_LIMIT = DEFAULT_SIZE_LIMIT; + + /** File manager to use to create {@link UploadFile} instances. + * It is required by this new object to execute open, move and delete operations whenever it could be asked. */ + protected final UWSFileManager fileManager; + + /** + * Build the request parser. + * + * @param fileManager A file manager. MUST NOT be NULL + */ + public NoEncodingParser(final UWSFileManager fileManager){ + if (fileManager == null) + throw new NullPointerException("Missing file manager => can not create a SingleDataParser!"); + this.fileManager = fileManager; + } + + @Override + public Map parse(final HttpServletRequest request) throws UWSException{ + // Check the request size: + if (request.getContentLength() <= 0) + return new HashMap(); + else if (request.getContentLength() > (SIZE_LIMIT < 0 ? DEFAULT_SIZE_LIMIT : SIZE_LIMIT)) + throw new UWSException("JDL too big (>" + SIZE_LIMIT + " bytes) => Request rejected! You should see with the service administrator to extend this limit."); + + // Build the parameter name: + String paramName; + if (request.getMethod() != null && request.getMethod().equalsIgnoreCase("put")){ + paramName = request.getRequestURI(); + if (paramName.lastIndexOf('/') + 1 > 0) + paramName = paramName.substring(paramName.lastIndexOf('/') + 1); + }else + paramName = "JDL"; + + // Build the file by copy of the whole request body: + Object reqID = request.getAttribute(UWS.REQ_ATTRIBUTE_ID); + if (reqID == null || !(reqID instanceof String)) + reqID = (new Date()).getTime(); + File f = new File(UWSFileManager.TMP_UPLOAD_DIR, "REQUESTBODY_" + reqID); + OutputStream output = null; + InputStream input = null; + long totalLength = 0; + try{ + output = new BufferedOutputStream(new FileOutputStream(f)); + input = new BufferedInputStream(request.getInputStream()); + + byte[] buffer = new byte[2049]; + int len = input.read(buffer); + if (len <= 0){ + output.close(); + f.delete(); + HashMap params = new HashMap(1); + params.put(paramName, ""); + return params; + }else if (len <= 2048 && request.getMethod() != null && request.getMethod().equalsIgnoreCase("put") && request.getContentType() != null && request.getContentType().toLowerCase().startsWith("text/plain")){ + output.close(); + f.delete(); + HashMap params = new HashMap(1); + params.put(paramName, new String(buffer, 0, len)); + return params; + }else{ + do{ + output.write(buffer, 0, len); + totalLength += len; + }while((len = input.read(buffer)) > 0); + output.flush(); + } + }catch(IOException ioe){ + throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, ioe, "Internal error => Impossible to get the JDL from the HTTP request!"); + }finally{ + if (input != null){ + try{ + input.close(); + }catch(IOException ioe2){} + } + if (output != null){ + try{ + output.close(); + }catch(IOException ioe2){} + } + } + + // Build its description: + UploadFile lob = new UploadFile(paramName, f.toURI().toString(), fileManager); + lob.mimeType = request.getContentType(); + lob.length = totalLength; + + // Create the parameters map: + HashMap parameters = new HashMap(); + parameters.put(paramName, lob); + + return parameters; + } + +} diff --git a/src/uws/service/request/RequestParser.java b/src/uws/service/request/RequestParser.java new file mode 100644 index 0000000..fc1b23a --- /dev/null +++ b/src/uws/service/request/RequestParser.java @@ -0,0 +1,93 @@ +package uws.service.request; + +/* + * This file is part of UWSLibrary. + * + * UWSLibrary is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * UWSLibrary is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with UWSLibrary. If not, see . + * + * Copyright 2014 - Astronomisches Rechen Institut (ARI) + */ + +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + +import uws.UWSException; +import uws.job.parameters.InputParamController; +import uws.job.parameters.UWSParameters; + +/** + *

    This parser lets extract parameters from an {@link HttpServletRequest}. + * + *

    + * These parameters can be indeed provided in several ways. Among these ways, + * application/x-www-form-urlencoded and multipart/form-data are the most famous. + * Both are already fully supported by the UWS library by default in {@link UWSParameters}. + *

    + * + *

    IMPORTANT: + * A {@link RequestParser} extension MUST NOT be used to check the parameters' value. + * It only aims to parse an {@link HttpServletRequest} in order to extract parameters. + *

    + * + * @author Grégory Mantelet (ARI) + * @version 4.1 (11/2014) + * @since 4.1 + * + * @see UWSParameters + */ +public interface RequestParser { + + /** + *

    Extract parameters from the given HTTP request.

    + * + *

    + * These parameters can be fetched from {@link HttpServletRequest#getParameterMap()} + * or directly from the full request content. In this last case, a parsing is necessary ; + * hence this function. + *

    + * + *

    + * In case a parameter is provided several times with the same time and the same case, + * the request parser can choose to keep only the last occurrence or all occurrences. + * If all occurrences are kept, this function MUST return an array of {@link Object}s + * (in which types may be mixed), otherwise a map value MUST be an elementary object. + *

    + * + *

    Note: + * A parameter item can be a simple value (e.g. String, integer, ...) + * or a more complex object (e.g. File, InputStream, ...). + *

    + * + *

    IMPORTANT: + * This function MUST NOT be used to check the parameters' value. + * It only aims to parse the given request in order to extract its embedded parameters. + *
    + * Consequently, if this function throws an exception, it could be only because the request + * can not be read, and not because a parameter format or value is incorrect. + *
    + * Parameter checks should be done in {@link UWSParameters} and more particularly by + * an {@link InputParamController}. + *

    + * + * @param request An HTTP request. + * + * @return A map listing all extracted parameters. Values are either an elementary object (whatever is the type), + * or an array of {@link Object}s (in which types can be mixed). + * + * @throws UWSException If any error provides this function to read the parameters. + */ + public Map parse(final HttpServletRequest request) throws UWSException; + +} diff --git a/src/uws/service/request/UWSRequestParser.java b/src/uws/service/request/UWSRequestParser.java new file mode 100644 index 0000000..89ea7d9 --- /dev/null +++ b/src/uws/service/request/UWSRequestParser.java @@ -0,0 +1,141 @@ +package uws.service.request; + +/* + * This file is part of UWSLibrary. + * + * UWSLibrary is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * UWSLibrary is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with UWSLibrary. If not, see . + * + * Copyright 2014 - Astronomisches Rechen Institut (ARI) + */ + +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + +import uws.UWSException; +import uws.UWSToolBox; +import uws.service.file.UWSFileManager; + +/** + *

    This parser adapts the request parser to use in function of the request content-type:

    + *
      + *
    • application/x-www-form-urlencoded: {@link FormEncodedParser}
    • + *
    • multipart/form-data: {@link MultipartParser}
    • + *
    • other: {@link NoEncodingParser} (the whole request body will be stored as one single parameter)
    • + *
    + * + *

    + * The request body size is limited for the multipart AND the no-encoding parsers. If you want to change this limit, + * you MUST do it for each of these parsers, setting the following static attributes: resp. {@link MultipartParser#SIZE_LIMIT} + * and {@link NoEncodingParser#SIZE_LIMIT}. + *

    + * + *

    Note: + * If you want to change the support other request parsing, you will have to write your own {@link RequestParser} implementation. + *

    + * + * @author Grégory Mantelet (ARI) + * @version 4.1 (12/2014) + * @since 4.1 + */ +public final class UWSRequestParser implements RequestParser { + + /** File manager to use to create {@link UploadFile} instances. + * It is required by this new object to execute open, move and delete operations whenever it could be asked. */ + private final UWSFileManager fileManager; + + /** {@link RequestParser} to use when a application/x-www-form-urlencoded request must be parsed. This attribute is set by {@link #parse(HttpServletRequest)} + * only when needed, by calling the function {@link #getFormParser()}. */ + private RequestParser formParser = null; + + /** {@link RequestParser} to use when a multipart/form-data request must be parsed. This attribute is set by {@link #parse(HttpServletRequest)} + * only when needed, by calling the function {@link #getMultipartParser()}. */ + private RequestParser multipartParser = null; + + /** {@link RequestParser} to use when none of the other parsers can be used ; it will then transform the whole request body in a parameter called "JDL" + * (Job Description Language). This attribute is set by {@link #parse(HttpServletRequest)} only when needed, by calling the function + * {@link #getNoEncodingParser()}. */ + private RequestParser noEncodingParser = null; + + /** + * Build a {@link RequestParser} able to choose the most appropriate {@link RequestParser} in function of the request content-type. + * + * @param fileManager The file manager to use in order to store any eventual upload. MUST NOT be NULL + */ + public UWSRequestParser(final UWSFileManager fileManager){ + if (fileManager == null) + throw new NullPointerException("Missing file manager => can not create a UWSRequestParser!"); + this.fileManager = fileManager; + } + + @Override + public Map parse(final HttpServletRequest req) throws UWSException{ + if (req == null) + return new HashMap(); + + // Get the method: + String method = (req.getMethod() == null) ? "" : req.getMethod().toLowerCase(); + + if (method.equals("post") || method.equals("put")){ + Map params = null; + + // Get the parameters: + if (FormEncodedParser.isFormEncodedRequest(req)) + params = getFormParser().parse(req); + else if (MultipartParser.isMultipartContent(req)) + params = getMultipartParser().parse(req); + else + params = getNoEncodingParser().parse(req); + + // Only for POST requests, the parameters specified in the URL must be added: + if (method.equals("post")) + params = UWSToolBox.addGETParameters(req, (params == null) ? new HashMap() : params); + + return params; + }else + return UWSToolBox.addGETParameters(req, new HashMap()); + } + + /** + * Get the {@link RequestParser} to use for application/x-www-form-urlencoded HTTP requests. + * This parser may be created if not already done. + * + * @return The {@link RequestParser} to use for application/x-www-form-urlencoded requests. Never NULL + */ + private synchronized final RequestParser getFormParser(){ + return (formParser == null) ? (formParser = new FormEncodedParser()) : formParser; + } + + /** + * Get the {@link RequestParser} to use for multipart/form-data HTTP requests. + * This parser may be created if not already done. + * + * @return The {@link RequestParser} to use for multipart/form-data requests. Never NULL + */ + private synchronized final RequestParser getMultipartParser(){ + return (multipartParser == null) ? (multipartParser = new MultipartParser(fileManager)) : multipartParser; + } + + /** + * Get the {@link RequestParser} to use for HTTP requests whose the content type is neither application/x-www-form-urlencoded nor multipart/form-data. + * This parser may be created if not already done. + * + * @return The {@link RequestParser} to use for requests whose the content-type is not supported. Never NULL + */ + private synchronized final RequestParser getNoEncodingParser(){ + return (noEncodingParser == null) ? (noEncodingParser = new NoEncodingParser(fileManager)) : noEncodingParser; + } + +} diff --git a/src/uws/service/request/UploadFile.java b/src/uws/service/request/UploadFile.java new file mode 100644 index 0000000..0ea800e --- /dev/null +++ b/src/uws/service/request/UploadFile.java @@ -0,0 +1,207 @@ +package uws.service.request; + +/* + * This file is part of UWSLibrary. + * + * UWSLibrary is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * UWSLibrary is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with UWSLibrary. If not, see . + * + * Copyright 2014 - Astronomisches Rechen Institut (ARI) + */ + +import java.io.IOException; +import java.io.InputStream; + +import uws.job.UWSJob; +import uws.job.parameters.UWSParameters; +import uws.service.file.UWSFileManager; + +/** + *

    This class lets represent a file submitted inline in an HTTP request.

    + * + *

    + * To read this special kind of parameter, an {@link InputStream} must be open. This class lets do it + * by its function {@link #open()}. + *

    + * + *

    + * When not used any more this file should be deleted, in order to save server disk space. + * This can be easily done thanks to {@link #deleteFile()}. This function actually just call the corresponding function + * of the file manager, which is the only one to known how to deal with this file on the server. Indeed, even if most + * of the time this file is stored on the local file system, it could also be stored on a distant server by a VOSpace. + * In this case, the way to proceed is different, hence the use of the file manager. + *

    + * + * @author Grégory Mantelet (ARI) + * @version 4.1 (11/2014) + * + * @see UWSParameters + * @see MultipartParser + */ +public class UploadFile { + /** Name of the parameter in which the file was submitted. */ + public final String paramName; + + /** File name. It is the name provided in the HTTP request. */ + public final String fileName; + + /** Location at which the content of this upload has been stored. + * It can be a local file path, but also any other path or ID allowing + * the {@link UWSFileManager} to access its content. */ + protected String location; + + /** Jobs that owns this uploaded file. */ + protected UWSJob owner = null; + + /** Indicate whether this file has been or is used by a UWSJob. + * In other words, it is true when an open, move or delete operation has been performed. + * An unused {@link UploadFile} instance shall be physically deleted from the file system. */ + protected boolean used = false; + + /** MIME type of the file. */ + public String mimeType = null; + + /** Length in bytes of the file. + * If negative, the length should be considered as unknown. */ + public long length = -1; + + /** File manager to use in order to open, move or delete this uploaded file. */ + protected final UWSFileManager fileManager; + + /** + * Build the description of an uploaded file. + * + * @param paramName Name of the HTTP request parameter in which the uploaded content was stored. MUST NOT be NULL + * @param location Location of the file on the server. This String is then used by the given file manager in order to open, + * move or delete the uploaded file. Thus, it can be a path, an ID or any other String meaningful to the file manager. + * @param fileManager File manager to use in order to open, move or delete this uploaded file from the server. + */ + public UploadFile(final String paramName, final String location, final UWSFileManager fileManager){ + this(paramName, null, location, fileManager); + } + + /** + * Build the description of an uploaded file. + * + * @param paramName Name of the HTTP request parameter in which the uploaded content was stored. MUST NOT be NULL + * @param fileName Filename as provided by the HTTP request. MAY be NULL + * @param location Location of the file on the server. This String is then used by the given file manager in order to open, + * move or delete the uploaded file. Thus, it can be a path, an ID or any other String meaningful to the file manager. + * @param fileManager File manager to use in order to open, move or delete this uploaded file from the server. + */ + public UploadFile(final String paramName, final String fileName, final String location, final UWSFileManager fileManager){ + if (paramName == null) + throw new NullPointerException("Missing name of the parameter in which the uploaded file content was => can not create UploadFile!"); + else if (location == null) + throw new NullPointerException("Missing server location of the uploaded file => can not create UploadFile!"); + else if (fileManager == null) + throw new NullPointerException("Missing file manager => can not create the UploadFile!"); + + this.paramName = paramName; + this.fileName = (fileName == null) ? "" : fileName; + this.location = location; + this.fileManager = fileManager; + } + + /** + *

    Get the location (e.g. URI, file path) of this file on the server.

    + * + *

    Important note: + * This function SHOULD be used only by the {@link UWSFileManager} when open, move and delete operations are executed. + * The {@link RequestParser} provided by the library set this location to the file URI (i.e. "file://{local-file-path}") + * since the default behavior is to store uploaded file on the system temporary directory. + *

    + * + * @return Location (e.g. URI) or ID or any other meaningful String used by the file manager to access to the uploaded file. + */ + public String getLocation(){ + return location; + } + + /** + * Get the job that uses this uploaded file. + * + * @return The owner of this file. + */ + public UWSJob getOwner(){ + return owner; + } + + /** + *

    Tell whether this uploaded file has been or will be used. + * That's to say, whether an open, delete or move operation has been executed (even if it failed) on this {@link UploadFile} instance.

    + * + * @return true if the file must be preserved, false otherwise. + */ + public final boolean isUsed(){ + return used; + } + + /** + * Open a stream toward this uploaded file. + * + * @return Stream toward this upload content. + * + * @throws IOException If an error occurs while opening the stream. + * + * @see UWSFileManager#getUploadInput(UploadFile) + */ + public InputStream open() throws IOException{ + used = true; + return fileManager.getUploadInput(this); + } + + /** + * Delete definitely this uploaded file from the server. + * + * @throws IOException If the delete operation can not be performed. + * + * @see UWSFileManager#deleteUpload(UploadFile) + */ + public void deleteFile() throws IOException{ + fileManager.deleteUpload(this); + used = true; + } + + /** + *

    Move this uploaded file in a location related to the given {@link UWSJob}. + * It is particularly useful if at reception of an HTTP request uploaded files are stored in a temporary + * directory (e.g. /tmp on Unix/Linux systems).

    + * + *

    + * This function calls {@link UWSFileManager#moveUpload(UploadFile, UWSJob)} to process to the physical + * moving of the file, but it then, it updates its location in this {@link UploadFile} instance. + * The file manager does NOT update this location! That's why it must not be called directly, but + * through {@link #move(UWSJob)}. + *

    + * + * @param destination The job by which this uploaded file will be exclusively used. + * + * @throws IOException If the move operation can not be performed. + * + * @see UWSFileManager#moveUpload(UploadFile, UWSJob) + */ + public void move(final UWSJob destination) throws IOException{ + if (destination == null) + throw new NullPointerException("Missing move destination (i.e. the job in which the uploaded file must be stored)!"); + + location = fileManager.moveUpload(this, destination); + used = true; + owner = destination; + } + + @Override + public String toString(){ + return (owner != null && owner.getJobList() != null && owner.getUrl() != null) ? owner.getUrl().jobParameter(owner.getJobList().getName(), owner.getJobId(), paramName).toString() : fileName; + } +} diff --git a/test/adql/SearchColumnListTest.java b/test/adql/SearchColumnListTest.java deleted file mode 100644 index 776a343..0000000 --- a/test/adql/SearchColumnListTest.java +++ /dev/null @@ -1,240 +0,0 @@ -package adql; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; - -import adql.db.DBColumn; -import adql.db.DBCommonColumn; -import adql.db.DBTable; -import adql.db.SearchColumnList; -import adql.db.exception.UnresolvedJoin; -import adql.parser.ParseException; -import adql.query.IdentifierField; -import adql.query.operand.ADQLColumn; -import tap.metadata.TAPColumn; -import tap.metadata.TAPSchema; -import tap.metadata.TAPTable; - -public class SearchColumnListTest { - - public static void main(String[] args) throws ParseException{ - - /* SET THE TABLES AND COLUMNS NEEDED FOR THE TEST */ - // Describe the available table: - TAPTable tableA = new TAPTable("A", "TABLE", "NATURAL JOIN Test table", null); - TAPTable tableB = new TAPTable("B", "TABLE", "NATURAL JOIN Test table", null); - TAPTable tableC = new TAPTable("C", "TABLE", "NATURAL JOIN Test table", null); - TAPTable tableD = new TAPTable("D", "TABLE", "NATURAL JOIN Test table", null); - - // Describe its columns: - tableA.addColumn(new TAPColumn("id", "Object ID")); - tableA.addColumn(new TAPColumn("txta", "Text of table A")); - tableB.addColumn(new TAPColumn("id", "Object ID")); - tableB.addColumn(new TAPColumn("txtb", "Text of table B")); - tableC.addColumn(new TAPColumn("Id", "Object ID")); - tableC.addColumn(new TAPColumn("txta", "Text of table A")); - tableC.addColumn(new TAPColumn("txtc", "Text of table C")); - tableD.addColumn(new TAPColumn("id", "Object ID")); - tableD.addColumn(new TAPColumn("txta", "Text of table A")); - tableD.addColumn(new TAPColumn("txtd", "Text of table D")); - - // List all available tables: - TAPSchema schema = new TAPSchema("public"); - schema.addTable(tableA); - schema.addTable(tableB); - schema.addTable(tableC); - schema.addTable(tableD); - - // Build the corresponding SearchColumnList: - SearchColumnList listA = new SearchColumnList(); - for(DBColumn col : tableA) - listA.add(col); - SearchColumnList listB = new SearchColumnList(); - for(DBColumn col : tableB) - listB.add(col); - SearchColumnList listC = new SearchColumnList(); - for(DBColumn col : tableC) - listC.add(col); - SearchColumnList listD = new SearchColumnList(); - for(DBColumn col : tableD) - listD.add(col); - - /* TEST OF NATURAL JOIN */ - System.out.println("### CROSS JOIN ###"); - SearchColumnList crossJoin = join(listA, listB, false, null); - - // DEBUG - for(DBColumn dbCol : crossJoin){ - if (dbCol instanceof DBCommonColumn){ - System.out.print("\t- " + dbCol.getADQLName() + " in " + ((dbCol.getTable() == null) ? "" : dbCol.getTable().getADQLName()) + " (= " + dbCol.getDBName() + " in "); - Iterator it = ((DBCommonColumn)dbCol).getCoveredTables(); - DBTable table; - while(it.hasNext()){ - table = it.next(); - System.out.print((table == null) ? "" : table.getDBName() + ", "); - } - System.out.println(")"); - }else - System.out.println("\t- " + dbCol.getADQLName() + " in " + ((dbCol.getTable() == null) ? "" : dbCol.getTable().getADQLName()) + " (= " + dbCol.getDBName() + " in " + ((dbCol.getTable() == null) ? "" : dbCol.getTable().getDBName()) + ")"); - } - System.out.println(); - - /* TEST OF NATURAL JOIN */ - System.out.println("### NATURAL JOIN ###"); - SearchColumnList join1 = join(listA, listB, true, null); - SearchColumnList join2 = join(listC, listD, true, null); - //SearchColumnList join3 = join(join1, join2, true, null); - - // DEBUG - for(DBColumn dbCol : join2){ - if (dbCol instanceof DBCommonColumn){ - System.out.print("\t- " + dbCol.getADQLName() + " in " + ((dbCol.getTable() == null) ? "" : dbCol.getTable().getADQLName()) + " (= " + dbCol.getDBName() + " in "); - Iterator it = ((DBCommonColumn)dbCol).getCoveredTables(); - DBTable table; - while(it.hasNext()){ - table = it.next(); - System.out.print((table == null) ? "" : table.getDBName() + ", "); - } - System.out.println(")"); - }else - System.out.println("\t- " + dbCol.getADQLName() + " in " + ((dbCol.getTable() == null) ? "" : dbCol.getTable().getADQLName()) + " (= " + dbCol.getDBName() + " in " + ((dbCol.getTable() == null) ? "" : dbCol.getTable().getDBName()) + ")"); - } - System.out.println(); - - /* TEST OF JOIN USING 1 */ - System.out.println("\n### USING JOIN 1 ###"); - ArrayList usingList = new ArrayList(); - usingList.add(new ADQLColumn("id")); - SearchColumnList joinUsing1 = join(join1, join2, false, usingList); - - // DEBUG - for(DBColumn dbCol : joinUsing1){ - if (dbCol instanceof DBCommonColumn){ - System.out.print("\t- " + dbCol.getADQLName() + " in " + ((dbCol.getTable() == null) ? "" : dbCol.getTable().getADQLName()) + " (= " + dbCol.getDBName() + " in "); - Iterator it = ((DBCommonColumn)dbCol).getCoveredTables(); - DBTable table; - while(it.hasNext()){ - table = it.next(); - System.out.print((table == null) ? "" : table.getDBName() + ", "); - } - System.out.println(")"); - }else - System.out.println("\t- " + dbCol.getADQLName() + " in " + ((dbCol.getTable() == null) ? "" : dbCol.getTable().getADQLName()) + " (= " + dbCol.getDBName() + " in " + ((dbCol.getTable() == null) ? "" : dbCol.getTable().getDBName()) + ")"); - } - System.out.println(); - - /* TEST OF JOIN USING 1 * - System.out.println("\n### USING JOIN 2 ###"); - usingList.clear(); - usingList.add(new TAPColumn("id")); - SearchColumnList joinUsing2 = joinUsing(listA, join3, usingList); - - // DEBUG - for(DBColumn dbCol : joinUsing2){ - System.out.println("\t- "+dbCol.getADQLName()+" in "+((dbCol.getTable()==null)?"":dbCol.getTable().getADQLName())+" (= "+dbCol.getDBName()+" in "+((dbCol.getTable()==null)?"":dbCol.getTable().getDBName())+")"); - } - System.out.println();*/ - - } - - public static final SearchColumnList join(final SearchColumnList leftList, final SearchColumnList rightList, final boolean natural, final ArrayList usingList) throws UnresolvedJoin{ - - SearchColumnList list = new SearchColumnList(); - /*SearchColumnList leftList = leftTable.getDBColumns(); - SearchColumnList rightList = rightTable.getDBColumns();*/ - - /* 1. Figure out duplicated columns */ - HashMap mapDuplicated = new HashMap(); - // CASE: NATURAL - if (natural){ - // Find duplicated items between the two lists and add one common column in mapDuplicated for each - DBColumn rightCol; - for(DBColumn leftCol : leftList){ - // search for at most one column with the same name in the RIGHT list - // and throw an exception is there are several matches: - rightCol = findAtMostOneColumn(leftCol.getADQLName(), (byte)0, rightList, false); - // if there is one... - if (rightCol != null){ - // ...check there is only one column with this name in the LEFT list, - // and throw an exception if it is not the case: - findExactlyOneColumn(leftCol.getADQLName(), (byte)0, leftList, true); - // ...create a common column: - mapDuplicated.put(leftCol.getADQLName().toLowerCase(), new DBCommonColumn(leftCol, rightCol)); - } - } - - } - // CASE: USING - else if (usingList != null && !usingList.isEmpty()){ - // For each columns of usingList, check there is in each list exactly one matching column, and then, add it in mapDuplicated - DBColumn leftCol, rightCol; - for(ADQLColumn usingCol : usingList){ - // search for exactly one column with the same name in the LEFT list - // and throw an exception if there is none, or if there are several matches: - leftCol = findExactlyOneColumn(usingCol.getColumnName(), usingCol.getCaseSensitive(), leftList, true); - // idem in the RIGHT list: - rightCol = findExactlyOneColumn(usingCol.getColumnName(), usingCol.getCaseSensitive(), rightList, false); - // create a common column: - mapDuplicated.put((usingCol.isCaseSensitive(IdentifierField.COLUMN) ? ("\"" + usingCol.getColumnName() + "\"") : usingCol.getColumnName().toLowerCase()), new DBCommonColumn(leftCol, rightCol)); - } - - } - // CASE: NO DUPLICATION TO FIGURE OUT - else{ - // Return the union of both lists: - list.addAll(leftList); - list.addAll(rightList); - return list; - } - - /* 2. Add all columns of the left list except the ones identified as duplications */ - addAllExcept(leftList, list, mapDuplicated); - - /* 3. Add all columns of the right list except the ones identified as duplications */ - addAllExcept(rightList, list, mapDuplicated); - - /* 4. Add all common columns of mapDuplicated */ - list.addAll(mapDuplicated.values()); - - return list; - - } - - public final static void addAllExcept(final SearchColumnList itemsToAdd, final SearchColumnList target, final Map exception){ - for(DBColumn col : itemsToAdd){ - if (!exception.containsKey(col.getADQLName().toLowerCase()) && !exception.containsKey("\"" + col.getADQLName() + "\"")) - target.add(col); - } - } - - public final static DBColumn findExactlyOneColumn(final String columnName, final byte caseSensitive, final SearchColumnList list, final boolean leftList) throws UnresolvedJoin{ - DBColumn result = findAtMostOneColumn(columnName, caseSensitive, list, leftList); - if (result == null) - throw new UnresolvedJoin("Column \"" + columnName + "\" specified in USING clause does not exist in " + (leftList ? "left" : "right") + " table!"); - else - return result; - } - - public final static DBColumn findAtMostOneColumn(final String columnName, final byte caseSensitive, final SearchColumnList list, final boolean leftList) throws UnresolvedJoin{ - ArrayList result = list.search(null, null, null, columnName, caseSensitive); - if (result.isEmpty()) - return null; - else if (result.size() > 1) - throw new UnresolvedJoin("Common column name \"" + columnName + "\" appears more than once in " + (leftList ? "left" : "right") + " table!"); - else - return result.get(0); - } - - /** - * Tells whether the given column is a common column (that's to say, a unification of several columns of the same name). - * - * @param col A DBColumn. - * @return true if the given column is a common column, false otherwise (particularly if col = null). - */ - public static final boolean isCommonColumn(final DBColumn col){ - return (col != null && col instanceof DBCommonColumn); - } - -} diff --git a/test/adql/SearchIterator.java b/test/adql/SearchIterator.java deleted file mode 100644 index c365882..0000000 --- a/test/adql/SearchIterator.java +++ /dev/null @@ -1,76 +0,0 @@ -package adql; - -/* - * This file is part of ADQLLibrary. - * - * ADQLLibrary is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ADQLLibrary is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ADQLLibrary. If not, see . - * - * Copyright 2011 - UDS/Centre de Données astronomiques de Strasbourg (CDS) - */ - -import java.util.Iterator; -import java.util.NoSuchElementException; -import java.util.Vector; - -/** - * Lets iterate on each "real" result ({@link SearchResult} objects whose the {@link SearchResult#isResult() isResult()} function returns true). - * - * @author Grégory Mantelet (CDS) - * @version 11/2010 - * - * @see SearchResult - */ -public class SearchIterator implements Iterator { - - /** List of the next SearchResult objects which has at least one result (themselves or included SearchResult). */ - protected Vector toExplore; - - public SearchIterator(SearchResult r){ - toExplore = new Vector(); - if (r != null && r.hasResult()) - toExplore.add(r); - } - - public boolean hasNext(){ - return !toExplore.isEmpty(); - } - - public SearchResult next() throws NoSuchElementException{ - SearchResult next = null; - - while(next == null && !toExplore.isEmpty()){ - SearchResult r = toExplore.remove(0); - if (!r.isLeaf()){ - Iterator children = r.getChildren(); - while(children.hasNext()){ - SearchResult child = children.next(); - if (child != null && child.hasResult()) - toExplore.add(child); - } - } - if (r.isResult()) - next = r; - } - - if (next == null) - throw new NoSuchElementException("No more search result !"); - - return next; - } - - public void remove() throws UnsupportedOperationException{ - throw new UnsupportedOperationException("The REMOVE operation is not possible in a search result !"); - } - -} diff --git a/test/adql/SearchResult.java b/test/adql/SearchResult.java deleted file mode 100644 index 3a373d8..0000000 --- a/test/adql/SearchResult.java +++ /dev/null @@ -1,251 +0,0 @@ -package adql; - -/* - * This file is part of ADQLLibrary. - * - * ADQLLibrary is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ADQLLibrary is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ADQLLibrary. If not, see . - * - * Copyright 2011 - UDS/Centre de Données astronomiques de Strasbourg (CDS) - */ - -import java.util.Iterator; -import java.util.Vector; - -import adql.query.ADQLObject; - -/** - *

    Results of a research in an ADQL query.

    - * - *

    This class is built as a tree. A node (leaf or not) corresponds to an item of a part of an ADQL query or merely of a whole ADQL query. - * It represents a step of the research. That means a node can represents a matched ADQL item and/or a list of other SearchResults (which are the results of the same research into the corresponding ADQL object). - * Thus it is possible to know the parent (into the ADQL query) of a matched ADQL item.

    - * - *

    Here are some useful functions of this class: - *

      - *
    • {@link SearchResult#isResult() isResult()}: indicates whether the current node corresponds to a matched ADQL item
    • - *
    • {@link SearchResult#getResult() getResult()}: returns the value of this node
    • - *
    • {@link SearchResult#getParent() getParent()}: returns the result (node) which encapsulates this result (node)
    • - *
    • {@link SearchResult#isLeaf() isLeaf()}: indicates whether this node encapsulates other results (nodes) or not
    • - *
    • {@link SearchResult#getChildren() getChildren()}: returns an iterator on all encapsulated results (nodes)
    • - *

    - * - *

    You have two different ways to navigate in a SearchResult object: - *

      - *
    1. As said previously a SearchResult is a hierarchical structure. So you can explore it as a tree with the functions {@link SearchResult#getResult() getResult()} (to get the node value), {@link SearchResult#getParent() getParent()} (to get the direct parent node), {@link SearchResult#getChildren() getChildren()} (to explore the children list) and {@link SearchResult#isLeaf() isLeaf()} (to determine if the current node is a leaf or not).
    2. - *
    3. However you can also iterate directly on each matched ADQL item (leaf or not) thanks to the {@link SearchResult#iterator() iterator()} function. All iterated object corresponds to a matched ADQL object (so {@link SearchResult#isResult() isResult()} always returns true for all iterated results).
    4. - *

    - * - *

    Important: Be aware that any SearchResult (leaf or not) may contain a matched ADQL object: to know that, use the function {@link SearchResult#isResult() isResult()}.

    - * - * @author Grégory Mantelet (CDS) - * @version 11/2010 - * - * @see SearchIterator - */ -public final class SearchResult implements Iterable { - - /** Parent node. */ - private SearchResult parent; - - /** Child nodes. */ - private final Vector children; - - /** Total number of results from this node (included). */ - private int nbResults = 0; - - /** The node value (may be the matched ADQL object). */ - private final ADQLObject value; - - /** Indicates whether this node corresponds to a matched ADQL object or not. */ - private final boolean result; - - /** If it is impossible to replace an ADQL object by another one, a SearchResult must be created (with result = true) and this field must contain an error description. */ - private String error = null; - - /** - *

    Builds a SearchResult (node) with its value (node value).

    - *

    Note: By using this constructor the created SearchResult will not correspond to a matched ADQL object.

    - * - * @param nodeValue Value (ADQL object) associated with this node. - */ - public SearchResult(ADQLObject nodeValue){ - this(nodeValue, false); - } - - /** - * Builds a SearchResult (node) with its value (node value) and an indication on its interpretation (~ "matched ADQL object ?"). - * - * @param nodeValue Value (ADQL object) associated with this node. - * @param isResult Indicates whether the given ADQL object is a match or not. - */ - public SearchResult(ADQLObject nodeValue, boolean isResult){ - this.parent = null; - children = new Vector(); - - value = nodeValue; - result = (nodeValue != null) && isResult; - if (result) - nbResults = 1; - } - - /** - * Gets the ADQL object associated with this node. - * It may be a matched ADQL item (it depends of what returns the {@link SearchResult#isResult() isResult()} function). - * - * @return The node value. - */ - public final ADQLObject getResult(){ - return value; - } - - /** - * Indicates whether the ADQL object (returned by {@link SearchResult#getResult() getResult()}) is a match or not. - * - * @return true if this SearchResult corresponds to a matched ADQL item, false otherwise. - */ - public final boolean isResult(){ - return result; - } - - /** - * Gets the error that occurs when replacing the matched item. - * - * @return Replacing error. - */ - public final String getError(){ - return error; - } - - /** - * Indicates whether there was an error during the replacement of the matched item. - * - * @return true if there was an error during the replacement, false else. - */ - public final boolean hasError(){ - return error != null; - } - - /** - * Sets the explanation of why the matched item has not been replaced. - * - * @param msg Error description. - */ - public final void setError(String msg){ - if (msg != null){ - msg = msg.trim(); - error = (msg.length() == 0) ? null : msg; - }else - error = null; - } - - /** - * Gets the parent node. - * - * @return Its parent node. - */ - public final SearchResult getParent(){ - return parent; - } - - /** - * Changes the parent node. - * - * @param newParent Its new parent node. - */ - private final void setParent(SearchResult newParent){ - parent = newParent; - } - - /** - * Gets an iterator on the children list of this SearchResult. - * - * @return An iterator on its children. - */ - public final Iterator getChildren(){ - return children.iterator(); - } - - /** - * Indicates whether this node is a leaf (that is to say if it has children). - * - * @return true if this node is a leaf, false otherwise. - */ - public final boolean isLeaf(){ - return children.isEmpty(); - } - - /** - * Lets adding a child to this node. - * - * @param result The SearchResult to add. - */ - public final void add(SearchResult result){ - if (result != null){ - // Add the given result: - children.add(result); - - // Set its parent: - result.setParent(this); - - // Update the total number of results from this node: - updateNbResults(); - } - } - - /** - * Counts exactly the total number of results from this node (included). - * Once the counting phase finished the direct parent node is notify that it must update its own number of results. - */ - private final void updateNbResults(){ - synchronized(this){ - // Count all results from this node: - nbResults = isResult() ? 1 : 0; - for(SearchResult r : children) - nbResults += r.getNbResults(); - } - - // Notify the direct parent node: - if (parent != null) - parent.updateNbResults(); - } - - /** - *

    Indicates whether this node is and/or contains some results (SearchResult objects whose the function isResult() returns true).

    - * - * @return true if this SearchResult is a result or if one of its children is a result, false otherwise. - */ - public final boolean hasResult(){ - return nbResults > 0; - } - - /** - *

    Tells exactly the number of SearchResult which are really results.

    - * - * @return The number of matched ADQL item. - */ - public final int getNbResults(){ - return nbResults; - } - - /** - * Lets iterating on all contained SearchResult objects (itself included) which are really a result (whose the function isResult() returns true). - * - * @see java.lang.Iterable#iterator() - * @see SearchIterator - */ - public final Iterator iterator(){ - return new SearchIterator(this); - } - -} diff --git a/test/adql/TestADQLQuery.java b/test/adql/TestADQLQuery.java index 7fac283..54a704e 100644 --- a/test/adql/TestADQLQuery.java +++ b/test/adql/TestADQLQuery.java @@ -1,6 +1,13 @@ package adql; +import static org.junit.Assert.assertEquals; + +import java.util.ArrayList; import java.util.Iterator; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; import adql.query.ADQLObject; import adql.query.ADQLOrder; @@ -9,13 +16,10 @@ import adql.query.ClauseADQL; import adql.query.ClauseConstraints; import adql.query.ClauseSelect; import adql.query.SelectItem; - import adql.query.constraint.Comparison; import adql.query.constraint.ComparisonOperator; import adql.query.constraint.ConstraintsGroup; - import adql.query.from.ADQLTable; - import adql.query.operand.ADQLColumn; import adql.query.operand.Concatenation; import adql.query.operand.NumericConstant; @@ -23,24 +27,45 @@ import adql.query.operand.Operation; import adql.query.operand.OperationType; import adql.query.operand.StringConstant; import adql.query.operand.WrappedOperand; - +import adql.search.IReplaceHandler; import adql.search.ISearchHandler; import adql.search.SearchColumnHandler; +import adql.search.SimpleReplaceHandler; public class TestADQLQuery { - public static final void main(String[] args) throws Exception{ - ADQLQuery query = new ADQLQuery(); + private ADQLQuery query = null; + private List columns = new ArrayList(8); + private List typeObjColumns = new ArrayList(3); + + @Before + public void setUp(){ + query = new ADQLQuery(); + columns.clear(); + typeObjColumns.clear(); + + columns.add(new ADQLColumn("O", "nameObj")); // 0 = O.nameObj + columns.add(new ADQLColumn("O", "typeObj")); // 1 = O.typeObj + columns.add(new ADQLColumn("O", "ra")); // 2 = O.ra + columns.add(new ADQLColumn("O", "dec")); // 3 = O.dec + columns.add(new ADQLColumn("ra")); // 4 = ra + columns.add(new ADQLColumn("dec")); // 5 = dec + columns.add(new ADQLColumn("typeObj")); // 6 = typeObj + columns.add(new ADQLColumn("typeObj")); // 7 = typeObj + + typeObjColumns.add(columns.get(1)); + typeObjColumns.add(columns.get(6)); + typeObjColumns.add(columns.get(7)); // SELECT: ClauseSelect select = query.getSelect(); Concatenation concatObj = new Concatenation(); - concatObj.add(new ADQLColumn("O", "nameObj")); + concatObj.add(columns.get(0)); // O.nameObj concatObj.add(new StringConstant(" (")); - concatObj.add(new ADQLColumn("O", "typeObj")); + concatObj.add(columns.get(1)); // O.typeObj concatObj.add(new StringConstant(")")); select.add(new SelectItem(new WrappedOperand(concatObj), "Nom objet")); - select.add(new ADQLColumn("O", "ra")); - select.add(new ADQLColumn("O", "dec")); + select.add(columns.get(2)); // O.ra + select.add(columns.get(3)); // O.dec // FROM: ADQLTable table = new ADQLTable("truc.ObsCore"); @@ -50,40 +75,53 @@ public class TestADQLQuery { // WHERE: ClauseConstraints where = query.getWhere(); - where.add(new Comparison(new Operation(new ADQLColumn("ra"), OperationType.DIV, new ADQLColumn("dec")), ComparisonOperator.GREATER_THAN, new NumericConstant("1"))); + // ra/dec > 1 + where.add(new Comparison(new Operation(columns.get(4), OperationType.DIV, columns.get(5)), ComparisonOperator.GREATER_THAN, new NumericConstant("1"))); ConstraintsGroup constOr = new ConstraintsGroup(); - constOr.add(new Comparison(new ADQLColumn("typeObj"), ComparisonOperator.EQUAL, new StringConstant("Star"))); - constOr.add("OR", new Comparison(new ADQLColumn("typeObj"), ComparisonOperator.LIKE, new StringConstant("Galaxy*"))); + // AND (typeObj == 'Star' + constOr.add(new Comparison(columns.get(6), ComparisonOperator.EQUAL, new StringConstant("Star"))); + // OR typeObj LIKE 'Galaxy*') + constOr.add("OR", new Comparison(columns.get(7), ComparisonOperator.LIKE, new StringConstant("Galaxy*"))); where.add("AND", constOr); // ORDER BY: ClauseADQL orderBy = query.getOrderBy(); orderBy.add(new ADQLOrder(1, true)); + } - System.out.println("*** QUERY ***\n" + query.toADQL()); + @Test + public void testADQLQuery(){ + assertEquals("SELECT (O.nameObj || ' (' || O.typeObj || ')') AS Nom objet , O.ra , O.dec\nFROM truc.ObsCore AS O\nWHERE ra/dec > 1 AND (typeObj = 'Star' OR typeObj LIKE 'Galaxy*')\nORDER BY 1 DESC", query.toADQL()); + } + @Test + public void testSearch(){ ISearchHandler sHandler = new SearchColumnHandler(false); Iterator results = query.search(sHandler); - // IReplaceHandler sHandler = new SimpleReplaceHandler(false, false) { - // - // @Override - // protected boolean match(ADQLObject obj) { - // return (obj instanceof ADQLColumn) && (((ADQLColumn)obj).getColumnName().equalsIgnoreCase("typeObj")); - // } - // - // @Override - // public ADQLObject getReplacer(ADQLObject objToReplace) throws UnsupportedOperationException { - // return new ADQLColumn("NewTypeObj"); - // } - // - // }; - // sHandler.searchAndReplace(query); - // System.out.println("INFO: "+sHandler.getNbReplacement()+"/"+sHandler.getNbMatch()+" replaced objects !"); - // Iterator results = sHandler.iterator(); - System.out.println("\n*** SEARCH ALL COLUMNS ***"); - while(results.hasNext()) - System.out.println("\t- " + results.next().toADQL()); - - System.out.println("\n*** QUERY ***\n" + query.toADQL()); + assertEquals(columns.size(), sHandler.getNbMatch()); + for(ADQLColumn expectedCol : columns) + assertEquals(expectedCol, results.next()); + } + + @Test + public void testReplace(){ + IReplaceHandler sHandler = new SimpleReplaceHandler(false, false){ + @Override + protected boolean match(ADQLObject obj){ + return (obj instanceof ADQLColumn) && (((ADQLColumn)obj).getColumnName().equalsIgnoreCase("typeObj")); + } + + @Override + public ADQLObject getReplacer(ADQLObject objToReplace) throws UnsupportedOperationException{ + return new ADQLColumn("NewTypeObj"); + } + }; + sHandler.searchAndReplace(query); + assertEquals(typeObjColumns.size(), sHandler.getNbMatch()); + assertEquals(sHandler.getNbMatch(), sHandler.getNbReplacement()); + Iterator results = sHandler.iterator(); + for(ADQLColumn expectedCol : typeObjColumns) + assertEquals(expectedCol, results.next()); + assertEquals("SELECT (O.nameObj || ' (' || NewTypeObj || ')') AS Nom objet , O.ra , O.dec\nFROM truc.ObsCore AS O\nWHERE ra/dec > 1 AND (NewTypeObj = 'Star' OR NewTypeObj LIKE 'Galaxy*')\nORDER BY 1 DESC", query.toADQL()); } } diff --git a/test/adql/TestGetPositionInAllADQLObject.java b/test/adql/TestGetPositionInAllADQLObject.java index 19744e9..5fd4fc5 100644 --- a/test/adql/TestGetPositionInAllADQLObject.java +++ b/test/adql/TestGetPositionInAllADQLObject.java @@ -11,13 +11,13 @@ public class TestGetPositionInAllADQLObject { public static void main(String[] args) throws Throwable{ ADQLParser parser = new ADQLParser(); - ADQLQuery query = parser.parseQuery("SELECT truc, bidule.machin FROM foo JOIN bidule USING(id) WHERE truc > 12.5 AND bidule.machin < 5"); + ADQLQuery query = parser.parseQuery("SELECT truc, bidule.machin FROM foo JOIN bidule USING(id) WHERE truc > 12.5 AND bidule.machin < 5 GROUP BY chose HAVING try > 0 ORDER BY chouetteAlors"); System.out.println("\nOBJECT WITH NO DEFINED POSITION:"); Iterator results = query.search(new SimpleSearchHandler(true){ @Override protected boolean match(ADQLObject obj){ - return obj.getPosition() == null; + return /*(obj instanceof ADQLList && ((ADQLList)obj).size() > 0) &&*/obj.getPosition() == null; } }); while(results.hasNext()) diff --git a/test/adql/TestIN.java b/test/adql/TestIN.java index 43ad6ca..d3ad2f2 100644 --- a/test/adql/TestIN.java +++ b/test/adql/TestIN.java @@ -1,32 +1,52 @@ package adql; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + import java.util.Iterator; +import org.junit.BeforeClass; +import org.junit.Test; + import adql.query.ADQLList; import adql.query.ADQLObject; import adql.query.ADQLOrder; import adql.query.ADQLQuery; import adql.query.ClauseSelect; - import adql.query.constraint.In; - import adql.query.from.ADQLTable; - import adql.query.operand.ADQLColumn; import adql.query.operand.ADQLOperand; import adql.query.operand.StringConstant; - import adql.search.IReplaceHandler; import adql.search.SimpleReplaceHandler; - +import adql.translator.ADQLTranslator; import adql.translator.PostgreSQLTranslator; public class TestIN { - public static void main(String[] args) throws Exception{ - In myIn = new In(new ADQLColumn("typeObj"), new ADQLOperand[]{new StringConstant("galaxy"),new StringConstant("star"),new StringConstant("planet"),new StringConstant("nebula")}, true); - System.out.println(myIn.getName() + ": " + myIn.toADQL()); + private static ADQLTranslator translator = null; + + @BeforeClass + public static void setUpBeforeClass(){ + translator = new PostgreSQLTranslator(); + } + @Test + public void testIN(){ + // Test with a simple list of values (here, string constants): + In myIn = new In(new ADQLColumn("typeObj"), new ADQLOperand[]{new StringConstant("galaxy"),new StringConstant("star"),new StringConstant("planet"),new StringConstant("nebula")}, true); + // check the ADQL: + assertEquals("typeObj NOT IN ('galaxy' , 'star' , 'planet' , 'nebula')", myIn.toADQL()); + // check the SQL translation: + try{ + assertEquals(myIn.toADQL(), translator.translate(myIn)); + }catch(Exception ex){ + ex.printStackTrace(); + fail("This test should have succeeded because the IN statement is correct and theoretically well supported by the POSTGRESQL translator!"); + } + + // Test with a sub-query: ADQLQuery subQuery = new ADQLQuery(); ClauseSelect select = subQuery.getSelect(); @@ -40,10 +60,17 @@ public class TestIN { orderBy.add(new ADQLOrder(1)); myIn.setSubQuery(subQuery); - System.out.println("\n*** " + myIn.getName().toUpperCase() + " ***\n" + myIn.toADQL()); - PostgreSQLTranslator translator = new PostgreSQLTranslator(); - System.out.println("\n*** SQL TRANSLATION ***\n" + translator.translate(myIn)); - + // check the ADQL: + assertEquals("typeObj NOT IN (SELECT DISTINCT TOP 10 typeObj\nFROM Objects\nORDER BY 1 ASC)", myIn.toADQL()); + // check the SQL translation: + try{ + assertEquals("typeObj NOT IN (SELECT DISTINCT typeObj AS \"typeObj\"\nFROM Objects\nORDER BY 1 ASC\nLimit 10)", translator.translate(myIn)); + }catch(Exception ex){ + ex.printStackTrace(); + fail("This test should have succeeded because the IN statement is correct and theoretically well supported by the POSTGRESQL translator!"); + } + + // Test after replacement inside this IN statement: IReplaceHandler sHandler = new SimpleReplaceHandler(true){ @Override @@ -57,13 +84,12 @@ public class TestIN { } }; sHandler.searchAndReplace(myIn); - System.out.println("INFO: " + sHandler.getNbReplacement() + "/" + sHandler.getNbMatch() + " replaced objects !"); + assertEquals(2, sHandler.getNbMatch()); + assertEquals(sHandler.getNbMatch(), sHandler.getNbReplacement()); Iterator results = sHandler.iterator(); - System.out.println("\n*** SEARCH RESULTS ***"); while(results.hasNext()) - System.out.println("\t- " + results.next()); - - System.out.println("\n*** AFTER REPLACEMENT ***\n" + myIn.toADQL()); + assertEquals("typeObj", results.next().toADQL()); + assertEquals("type NOT IN (SELECT DISTINCT TOP 10 type\nFROM Objects\nORDER BY 1 ASC)", myIn.toADQL()); } } diff --git a/test/adql/TestIdentifierField.java b/test/adql/TestIdentifierField.java new file mode 100644 index 0000000..c4c5fc2 --- /dev/null +++ b/test/adql/TestIdentifierField.java @@ -0,0 +1,25 @@ +package adql; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import adql.query.IdentifierField; + +public class TestIdentifierField { + + @Test + public void testIsCaseSensitive(){ + byte b = 0x00; + assertFalse(IdentifierField.SCHEMA.isCaseSensitive(b)); + b = IdentifierField.SCHEMA.setCaseSensitive(b, true); + assertTrue(IdentifierField.SCHEMA.isCaseSensitive(b)); + } + + /*@Test + public void testSetCaseSensitive(){ + fail("Not yet implemented"); + }*/ + +} diff --git a/test/adql/db/TestDBChecker.java b/test/adql/db/TestDBChecker.java new file mode 100644 index 0000000..3f9924a --- /dev/null +++ b/test/adql/db/TestDBChecker.java @@ -0,0 +1,740 @@ +package adql.db; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import adql.db.DBType.DBDatatype; +import adql.db.FunctionDef.FunctionParam; +import adql.db.exception.UnresolvedIdentifiersException; +import adql.parser.ADQLParser; +import adql.parser.ParseException; +import adql.query.ADQLObject; +import adql.query.ADQLQuery; +import adql.query.operand.ADQLColumn; +import adql.query.operand.ADQLOperand; +import adql.query.operand.StringConstant; +import adql.query.operand.function.DefaultUDF; +import adql.query.operand.function.UserDefinedFunction; +import adql.search.SimpleSearchHandler; +import adql.translator.ADQLTranslator; +import adql.translator.TranslationException; + +public class TestDBChecker { + + private static List tables; + + @BeforeClass + public static void setUpBeforeClass() throws Exception{ + tables = new ArrayList(); + + DefaultDBTable fooTable = new DefaultDBTable("foo"); + DBColumn col = new DefaultDBColumn("colS", new DBType(DBDatatype.VARCHAR), fooTable); + fooTable.addColumn(col); + col = new DefaultDBColumn("colI", new DBType(DBDatatype.INTEGER), fooTable); + fooTable.addColumn(col); + col = new DefaultDBColumn("colG", new DBType(DBDatatype.POINT), fooTable); + fooTable.addColumn(col); + + tables.add(fooTable); + } + + @AfterClass + public static void tearDownAfterClass() throws Exception{} + + @Before + public void setUp() throws Exception{} + + @After + public void tearDown() throws Exception{} + + @Test + public void testNumericOrStringValueExpressionPrimary(){ + ADQLParser parser = new ADQLParser(); + try{ + assertNotNull(parser.parseQuery("SELECT 'toto' FROM foo;")); + assertNotNull(parser.parseQuery("SELECT ('toto') FROM foo;")); + assertNotNull(parser.parseQuery("SELECT (('toto')) FROM foo;")); + assertNotNull(parser.parseQuery("SELECT 'toto' || 'blabla' FROM foo;")); + assertNotNull(parser.parseQuery("SELECT ('toto' || 'blabla') FROM foo;")); + assertNotNull(parser.parseQuery("SELECT (('toto' || 'blabla')) FROM foo;")); + assertNotNull(parser.parseQuery("SELECT (('toto') || (('blabla'))) FROM foo;")); + assertNotNull(parser.parseQuery("SELECT 3 FROM foo;")); + assertNotNull(parser.parseQuery("SELECT ((2+3)*5) FROM foo;")); + assertNotNull(parser.parseQuery("SELECT ABS(-123) FROM foo;")); + assertNotNull(parser.parseQuery("SELECT ABS(2*-1+5) FROM foo;")); + assertNotNull(parser.parseQuery("SELECT ABS(COUNT(*)) FROM foo;")); + assertNotNull(parser.parseQuery("SELECT toto FROM foo;")); + assertNotNull(parser.parseQuery("SELECT toto * 3 FROM foo;")); + assertNotNull(parser.parseQuery("SELECT toto || 'blabla' FROM foo;")); + }catch(ParseException pe){ + pe.printStackTrace(); + fail(); + } + try{ + parser.parseQuery("SELECT ABS('toto') FROM foo;"); + fail(); + }catch(ParseException pe){} + try{ + parser.parseQuery("SELECT ABS(('toto' || 'blabla')) FROM foo;"); + fail(); + }catch(ParseException pe){} + try{ + parser.parseQuery("SELECT 'toto' || 1 FROM foo;"); + fail(); + }catch(ParseException pe){} + try{ + parser.parseQuery("SELECT 1 || 'toto' FROM foo;"); + fail(); + }catch(ParseException pe){} + try{ + parser.parseQuery("SELECT 'toto' * 3 FROM foo;"); + fail(); + }catch(ParseException pe){} + } + + @Test + public void testUDFManagement(){ + // UNKNOWN FUNCTIONS ARE NOT ALLOWED: + ADQLParser parser = new ADQLParser(new DBChecker(tables, new ArrayList(0))); + + // Test with a simple ADQL query without unknown or user defined function: + try{ + assertNotNull(parser.parseQuery("SELECT * FROM foo;")); + }catch(ParseException e){ + e.printStackTrace(); + fail("A simple and basic query should not be a problem for the parser!"); + } + + // Test with an ADQL query containing one not declared UDF: + try{ + parser.parseQuery("SELECT toto() FROM foo;"); + fail("This query contains a UDF while it's not allowed: this test should have failed!"); + }catch(ParseException e){ + assertTrue(e instanceof UnresolvedIdentifiersException); + UnresolvedIdentifiersException ex = (UnresolvedIdentifiersException)e; + assertEquals(1, ex.getNbErrors()); + assertEquals("Unresolved function: \"toto()\"! No UDF has been defined or found with the signature: toto().", ex.getErrors().next().getMessage()); + } + + // DECLARE THE UDFs: + FunctionDef[] udfs = new FunctionDef[]{new FunctionDef("toto", new DBType(DBDatatype.VARCHAR)),new FunctionDef("tata", new DBType(DBDatatype.INTEGER))}; + parser = new ADQLParser(new DBChecker(tables, Arrays.asList(udfs))); + + // Test again: + try{ + assertNotNull(parser.parseQuery("SELECT toto() FROM foo;")); + assertNotNull(parser.parseQuery("SELECT tata() FROM foo;")); + }catch(ParseException e){ + e.printStackTrace(); + fail("This query contains a DECLARED UDF: this test should have succeeded!"); + } + + // Test but with at least one parameter: + try{ + parser.parseQuery("SELECT toto('blabla') FROM foo;"); + fail("This query contains an unknown UDF signature (the fct toto is declared with no parameter): this test should have failed!"); + }catch(ParseException e){ + assertTrue(e instanceof UnresolvedIdentifiersException); + UnresolvedIdentifiersException ex = (UnresolvedIdentifiersException)e; + assertEquals(1, ex.getNbErrors()); + assertEquals("Unresolved function: \"toto('blabla')\"! No UDF has been defined or found with the signature: toto(STRING).", ex.getErrors().next().getMessage()); + } + + // Test with a UDF whose the class is specified ; the corresponding object in the ADQL tree must be replace by an instance of this class: + udfs = new FunctionDef[]{new FunctionDef("toto", new DBType(DBDatatype.VARCHAR), new FunctionParam[]{new FunctionParam("txt", new DBType(DBDatatype.VARCHAR))})}; + udfs[0].setUDFClass(UDFToto.class); + parser = new ADQLParser(new DBChecker(tables, Arrays.asList(udfs))); + try{ + ADQLQuery query = parser.parseQuery("SELECT toto('blabla') FROM foo;"); + assertNotNull(query); + Iterator it = query.search(new SimpleSearchHandler(){ + @Override + protected boolean match(ADQLObject obj){ + return (obj instanceof UserDefinedFunction) && ((UserDefinedFunction)obj).getName().equals("toto"); + } + }); + assertTrue(it.hasNext()); + assertEquals(UDFToto.class.getName(), it.next().getClass().getName()); + assertFalse(it.hasNext()); + }catch(Exception e){ + e.printStackTrace(); + fail("This query contains a DECLARED UDF with a valid UserDefinedFunction class: this test should have succeeded!"); + } + + // Test with a wrong parameter type: + try{ + parser.parseQuery("SELECT toto(123) FROM foo;"); + fail("This query contains an unknown UDF signature (the fct toto is declared with one parameter of type STRING...here it is a numeric): this test should have failed!"); + }catch(Exception e){ + assertTrue(e instanceof UnresolvedIdentifiersException); + UnresolvedIdentifiersException ex = (UnresolvedIdentifiersException)e; + assertEquals(1, ex.getNbErrors()); + assertEquals("Unresolved function: \"toto(123)\"! No UDF has been defined or found with the signature: toto(NUMERIC).", ex.getErrors().next().getMessage()); + } + + // Test with UDF class constructor throwing an exception: + udfs = new FunctionDef[]{new FunctionDef("toto", new DBType(DBDatatype.VARCHAR), new FunctionParam[]{new FunctionParam("txt", new DBType(DBDatatype.VARCHAR))})}; + udfs[0].setUDFClass(WrongUDFToto.class); + parser = new ADQLParser(new DBChecker(tables, Arrays.asList(udfs))); + try{ + parser.parseQuery("SELECT toto('blabla') FROM foo;"); + fail("The set UDF class constructor has throw an error: this test should have failed!"); + }catch(Exception e){ + assertTrue(e instanceof UnresolvedIdentifiersException); + UnresolvedIdentifiersException ex = (UnresolvedIdentifiersException)e; + assertEquals(1, ex.getNbErrors()); + assertEquals("Impossible to represent the function \"toto\": the following error occured while creating this representation: \"[Exception] Systematic error!\"", ex.getErrors().next().getMessage()); + } + } + + @Test + public void testGeometry(){ + // DECLARE A SIMPLE PARSER where all geometries are allowed by default: + ADQLParser parser = new ADQLParser(new DBChecker(tables)); + + // Test with several geometries while all are allowed: + try{ + assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(POINT('', 12.3, 45.6), CIRCLE('', 1.2, 2.3, 5)) = 1;")); + }catch(ParseException pe){ + pe.printStackTrace(); + fail("This query contains several geometries, and all are theoretically allowed: this test should have succeeded!"); + } + + // Test with several geometries while only the allowed ones: + try{ + parser = new ADQLParser(new DBChecker(tables, new ArrayList(0), Arrays.asList(new String[]{"CONTAINS","POINT","CIRCLE"}), null)); + assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(POINT('', 12.3, 45.6), CIRCLE('', 1.2, 2.3, 5)) = 1;")); + }catch(ParseException pe){ + pe.printStackTrace(); + fail("This query contains several geometries, and all are theoretically allowed: this test should have succeeded!"); + } + try{ + parser.parseQuery("SELECT * FROM foo WHERE INTERSECTS(POINT('', 12.3, 45.6), CIRCLE('', 1.2, 2.3, 5)) = 1;"); + fail("This query contains a not-allowed geometry function (INTERSECTS): this test should have failed!"); + }catch(ParseException pe){ + assertTrue(pe instanceof UnresolvedIdentifiersException); + UnresolvedIdentifiersException ex = (UnresolvedIdentifiersException)pe; + assertEquals(1, ex.getNbErrors()); + assertEquals("The geometrical function \"INTERSECTS\" is not available in this implementation!", ex.getErrors().next().getMessage()); + } + + // Test by adding REGION: + try{ + parser = new ADQLParser(new DBChecker(tables, new ArrayList(0), Arrays.asList(new String[]{"CONTAINS","POINT","CIRCLE","REGION"}), null)); + assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(REGION('Position 12.3 45.6'), REGION('circle 1.2 2.3 5')) = 1;")); + }catch(ParseException pe){ + pe.printStackTrace(); + fail("This query contains several geometries, and all are theoretically allowed: this test should have succeeded!"); + } + try{ + parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(REGION('Position 12.3 45.6'), REGION('BOX 1.2 2.3 5 9')) = 1;"); + fail("This query contains a not-allowed geometry function (BOX): this test should have failed!"); + }catch(ParseException pe){ + assertTrue(pe instanceof UnresolvedIdentifiersException); + UnresolvedIdentifiersException ex = (UnresolvedIdentifiersException)pe; + assertEquals(1, ex.getNbErrors()); + assertEquals("The geometrical function \"BOX\" is not available in this implementation!", ex.getErrors().next().getMessage()); + } + + // Test with several geometries while none geometry is allowed: + try{ + parser = new ADQLParser(new DBChecker(tables, new ArrayList(0), new ArrayList(0), null)); + parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(POINT('', 12.3, 45.6), CIRCLE('', 1.2, 2.3, 5)) = 1;"); + fail("This query contains geometries while they are all forbidden: this test should have failed!"); + }catch(ParseException pe){ + assertTrue(pe instanceof UnresolvedIdentifiersException); + UnresolvedIdentifiersException ex = (UnresolvedIdentifiersException)pe; + assertEquals(3, ex.getNbErrors()); + Iterator itErrors = ex.getErrors(); + assertEquals("The geometrical function \"CONTAINS\" is not available in this implementation!", itErrors.next().getMessage()); + assertEquals("The geometrical function \"POINT\" is not available in this implementation!", itErrors.next().getMessage()); + assertEquals("The geometrical function \"CIRCLE\" is not available in this implementation!", itErrors.next().getMessage()); + } + } + + @Test + public void testCoordSys(){ + // DECLARE A SIMPLE PARSER where all coordinate systems are allowed by default: + ADQLParser parser = new ADQLParser(new DBChecker(tables)); + + // Test with several coordinate systems while all are allowed: + try{ + assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(POINT('', 12.3, 45.6), CIRCLE('', 1.2, 2.3, 5)) = 1;")); + assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(POINT('icrs', 12.3, 45.6), CIRCLE('cartesian2', 1.2, 2.3, 5)) = 1;")); + assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(POINT('lsr', 12.3, 45.6), CIRCLE('galactic heliocenter', 1.2, 2.3, 5)) = 1;")); + assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(POINT('unknownframe', 12.3, 45.6), CIRCLE('galactic unknownrefpos spherical2', 1.2, 2.3, 5)) = 1;")); + assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(REGION('position icrs lsr 12.3 45.6'), REGION('circle fk5 1.2 2.3 5')) = 1;")); + assertNotNull(parser.parseQuery("SELECT Region('not(position 1 2)') FROM foo;")); + }catch(ParseException pe){ + pe.printStackTrace(); + fail("This query contains several valid coordinate systems, and all are theoretically allowed: this test should have succeeded!"); + } + + // Concatenation as coordinate systems not checked: + try{ + assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(POINT('From ' || 'here', 12.3, 45.6), CIRCLE('', 1.2, 2.3, 5)) = 1;")); + }catch(ParseException pe){ + pe.printStackTrace(); + fail("This query contains a concatenation as coordinate systems (but only string constants are checked): this test should have succeeded!"); + } + + // Test with several coordinate systems while only some allowed: + try{ + parser = new ADQLParser(new DBChecker(tables, new ArrayList(0), null, Arrays.asList(new String[]{"icrs * *","fk4 geocenter *","galactic * spherical2"}))); + assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(POINT('', 12.3, 45.6), CIRCLE('', 1.2, 2.3, 5)) = 1;")); + assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(POINT('icrs', 12.3, 45.6), CIRCLE('cartesian3', 1.2, 2.3, 5)) = 1;")); + assertNotNull(parser.parseQuery("SELECT POINT('fk4', 12.3, 45.6) FROM foo;")); + assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(POINT('fk4 geocenter', 12.3, 45.6), CIRCLE('cartesian2', 1.2, 2.3, 5)) = 1;")); + assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(POINT('galactic', 12.3, 45.6), CIRCLE('galactic spherical2', 1.2, 2.3, 5)) = 1;")); + assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(POINT('galactic geocenter', 12.3, 45.6), CIRCLE('galactic lsr spherical2', 1.2, 2.3, 5)) = 1;")); + assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(REGION('position galactic lsr 12.3 45.6'), REGION('circle icrs 1.2 2.3 5')) = 1;")); + assertNotNull(parser.parseQuery("SELECT Region('not(position 1 2)') FROM foo;")); + }catch(ParseException pe){ + pe.printStackTrace(); + fail("This query contains several valid coordinate systems, and all are theoretically allowed: this test should have succeeded!"); + } + try{ + parser.parseQuery("SELECT POINT('fk5 geocenter', 12.3, 45.6) FROM foo;"); + fail("This query contains a not-allowed coordinate system ('fk5' is not allowed): this test should have failed!"); + }catch(ParseException pe){ + assertTrue(pe instanceof UnresolvedIdentifiersException); + UnresolvedIdentifiersException ex = (UnresolvedIdentifiersException)pe; + assertEquals(1, ex.getNbErrors()); + assertEquals("Coordinate system \"fk5 geocenter\" (= \"FK5 GEOCENTER SPHERICAL2\") not allowed in this implementation.", ex.getErrors().next().getMessage()); + } + try{ + parser.parseQuery("SELECT Region('not(position fk5 heliocenter 1 2)') FROM foo;"); + fail("This query contains a not-allowed coordinate system ('fk5' is not allowed): this test should have failed!"); + }catch(ParseException pe){ + assertTrue(pe instanceof UnresolvedIdentifiersException); + UnresolvedIdentifiersException ex = (UnresolvedIdentifiersException)pe; + assertEquals(1, ex.getNbErrors()); + assertEquals("Coordinate system \"FK5 HELIOCENTER\" (= \"FK5 HELIOCENTER SPHERICAL2\") not allowed in this implementation.", ex.getErrors().next().getMessage()); + } + + // Test with a coordinate system while none is allowed: + try{ + parser = new ADQLParser(new DBChecker(tables, new ArrayList(0), null, new ArrayList(0))); + assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(POINT('', 12.3, 45.6), CIRCLE('', 1.2, 2.3, 5)) = 1;")); + assertNotNull(parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(REGION('position 12.3 45.6'), REGION('circle 1.2 2.3 5')) = 1;")); + assertNotNull(parser.parseQuery("SELECT Region('not(position 1 2)') FROM foo;")); + }catch(ParseException pe){ + pe.printStackTrace(); + fail("This query specifies none coordinate system: this test should have succeeded!"); + } + try{ + parser.parseQuery("SELECT * FROM foo WHERE CONTAINS(POINT('ICRS SPHERICAL2', 12.3, 45.6), CIRCLE('icrs', 1.2, 2.3, 5)) = 1;"); + fail("This query specifies coordinate systems while they are all forbidden: this test should have failed!"); + }catch(ParseException pe){ + assertTrue(pe instanceof UnresolvedIdentifiersException); + UnresolvedIdentifiersException ex = (UnresolvedIdentifiersException)pe; + assertEquals(2, ex.getNbErrors()); + Iterator itErrors = ex.getErrors(); + assertEquals("Coordinate system \"ICRS SPHERICAL2\" (= \"ICRS UNKNOWNREFPOS SPHERICAL2\") not allowed in this implementation.", itErrors.next().getMessage()); + assertEquals("Coordinate system \"icrs\" (= \"ICRS UNKNOWNREFPOS SPHERICAL2\") not allowed in this implementation.", itErrors.next().getMessage()); + } + try{ + parser.parseQuery("SELECT Region('not(position fk4 1 2)') FROM foo;"); + fail("This query specifies coordinate systems while they are all forbidden: this test should have failed!"); + }catch(ParseException pe){ + assertTrue(pe instanceof UnresolvedIdentifiersException); + UnresolvedIdentifiersException ex = (UnresolvedIdentifiersException)pe; + assertEquals(1, ex.getNbErrors()); + assertEquals("Coordinate system \"FK4\" (= \"FK4 UNKNOWNREFPOS SPHERICAL2\") not allowed in this implementation.", ex.getErrors().next().getMessage()); + } + } + + @Test + public void testTypesChecking(){ + // DECLARE A SIMPLE PARSER: + ADQLParser parser = new ADQLParser(new DBChecker(tables)); + + // Test the type of columns generated by the parser: + try{ + ADQLQuery query = parser.parseQuery("SELECT colS, colI, colG FROM foo;"); + ADQLOperand colS = query.getSelect().get(0).getOperand(); + ADQLOperand colI = query.getSelect().get(1).getOperand(); + ADQLOperand colG = query.getSelect().get(2).getOperand(); + // test string column: + assertTrue(colS instanceof ADQLColumn); + assertTrue(colS.isString()); + assertFalse(colS.isNumeric()); + assertFalse(colS.isGeometry()); + // test integer column: + assertTrue(colI instanceof ADQLColumn); + assertFalse(colI.isString()); + assertTrue(colI.isNumeric()); + assertFalse(colI.isGeometry()); + // test geometry column: + assertTrue(colG instanceof ADQLColumn); + assertFalse(colG.isString()); + assertFalse(colG.isNumeric()); + assertTrue(colG.isGeometry()); + }catch(ParseException e1){ + if (e1 instanceof UnresolvedIdentifiersException) + ((UnresolvedIdentifiersException)e1).getErrors().next().printStackTrace(); + else + e1.printStackTrace(); + fail("This query contains known columns: this test should have succeeded!"); + } + + // Test the expected type - NUMERIC - generated by the parser: + try{ + assertNotNull(parser.parseQuery("SELECT colI * 3 FROM foo;")); + }catch(ParseException e){ + e.printStackTrace(); + fail("This query contains a product between 2 numerics: this test should have succeeded!"); + } + try{ + parser.parseQuery("SELECT colS * 3 FROM foo;"); + fail("This query contains a product between a string and an integer: this test should have failed!"); + }catch(ParseException e){ + assertTrue(e instanceof UnresolvedIdentifiersException); + UnresolvedIdentifiersException ex = (UnresolvedIdentifiersException)e; + assertEquals(1, ex.getNbErrors()); + assertEquals("Type mismatch! A numeric value was expected instead of \"colS\".", ex.getErrors().next().getMessage()); + } + try{ + parser.parseQuery("SELECT colG * 3 FROM foo;"); + fail("This query contains a product between a geometry and an integer: this test should have failed!"); + }catch(ParseException e){ + assertTrue(e instanceof UnresolvedIdentifiersException); + UnresolvedIdentifiersException ex = (UnresolvedIdentifiersException)e; + assertEquals(1, ex.getNbErrors()); + assertEquals("Type mismatch! A numeric value was expected instead of \"colG\".", ex.getErrors().next().getMessage()); + } + + // Test the expected type - STRING - generated by the parser: + try{ + assertNotNull(parser.parseQuery("SELECT colS || 'blabla' FROM foo;")); + }catch(ParseException e){ + e.printStackTrace(); + fail("This query contains a concatenation between 2 strings: this test should have succeeded!"); + } + try{ + parser.parseQuery("SELECT colI || 'blabla' FROM foo;"); + fail("This query contains a concatenation between an integer and a string: this test should have failed!"); + }catch(ParseException e){ + assertTrue(e instanceof UnresolvedIdentifiersException); + UnresolvedIdentifiersException ex = (UnresolvedIdentifiersException)e; + assertEquals(1, ex.getNbErrors()); + assertEquals("Type mismatch! A string value was expected instead of \"colI\".", ex.getErrors().next().getMessage()); + } + try{ + parser.parseQuery("SELECT colG || 'blabla' FROM foo;"); + fail("This query contains a concatenation between a geometry and a string: this test should have failed!"); + }catch(ParseException e){ + assertTrue(e instanceof UnresolvedIdentifiersException); + UnresolvedIdentifiersException ex = (UnresolvedIdentifiersException)e; + assertEquals(1, ex.getNbErrors()); + assertEquals("Type mismatch! A string value was expected instead of \"colG\".", ex.getErrors().next().getMessage()); + } + + // Test the expected type - GEOMETRY - generated by the parser: + try{ + assertNotNull(parser.parseQuery("SELECT CONTAINS(colG, CIRCLE('', 1, 2, 5)) FROM foo;")); + }catch(ParseException e){ + e.printStackTrace(); + fail("This query contains a geometrical predicate between 2 geometries: this test should have succeeded!"); + } + try{ + parser.parseQuery("SELECT CONTAINS(colI, CIRCLE('', 1, 2, 5)) FROM foo;"); + fail("This query contains a geometrical predicate between an integer and a geometry: this test should have failed!"); + }catch(ParseException e){ + assertTrue(e instanceof UnresolvedIdentifiersException); + UnresolvedIdentifiersException ex = (UnresolvedIdentifiersException)e; + assertEquals(1, ex.getNbErrors()); + assertEquals("Type mismatch! A geometry was expected instead of \"colI\".", ex.getErrors().next().getMessage()); + } + try{ + parser.parseQuery("SELECT CONTAINS(colS, CIRCLE('', 1, 2, 5)) FROM foo;"); + fail("This query contains a geometrical predicate between a string and a geometry: this test should have failed!"); + }catch(ParseException e){ + assertTrue(e instanceof UnresolvedIdentifiersException); + UnresolvedIdentifiersException ex = (UnresolvedIdentifiersException)e; + assertEquals(1, ex.getNbErrors()); + assertEquals("Type mismatch! A geometry was expected instead of \"colS\".", ex.getErrors().next().getMessage()); + } + + // DECLARE SOME UDFs: + FunctionDef[] udfs = new FunctionDef[]{new FunctionDef("toto", new DBType(DBDatatype.VARCHAR)),new FunctionDef("tata", new DBType(DBDatatype.INTEGER)),new FunctionDef("titi", new DBType(DBDatatype.REGION))}; + parser = new ADQLParser(new DBChecker(tables, Arrays.asList(udfs))); + + // Test the return type of the function TOTO generated by the parser: + try{ + ADQLQuery query = parser.parseQuery("SELECT toto() FROM foo;"); + ADQLOperand fct = query.getSelect().get(0).getOperand(); + assertTrue(fct instanceof DefaultUDF); + assertNotNull(((DefaultUDF)fct).getDefinition()); + assertTrue(fct.isString()); + assertFalse(fct.isNumeric()); + assertFalse(fct.isGeometry()); + }catch(ParseException e1){ + e1.printStackTrace(); + fail("This query contains a DECLARED UDF: this test should have succeeded!"); + } + + // Test the return type checking inside a whole query: + try{ + assertNotNull(parser.parseQuery("SELECT toto() || 'Blabla ' AS \"SuperText\" FROM foo;")); + }catch(ParseException e1){ + e1.printStackTrace(); + fail("This query contains a DECLARED UDF concatenated to a String: this test should have succeeded!"); + } + try{ + parser.parseQuery("SELECT toto()*3 AS \"SuperError\" FROM foo;"); + fail("This query contains a DECLARED UDF BUT used as numeric...which is here not possible: this test should have failed!"); + }catch(ParseException e1){ + assertTrue(e1 instanceof UnresolvedIdentifiersException); + UnresolvedIdentifiersException ex = (UnresolvedIdentifiersException)e1; + assertEquals(1, ex.getNbErrors()); + assertEquals("Type mismatch! A numeric value was expected instead of \"toto()\".", ex.getErrors().next().getMessage()); + } + + // Test the return type of the function TATA generated by the parser: + try{ + ADQLQuery query = parser.parseQuery("SELECT tata() FROM foo;"); + ADQLOperand fct = query.getSelect().get(0).getOperand(); + assertTrue(fct instanceof DefaultUDF); + assertNotNull(((DefaultUDF)fct).getDefinition()); + assertFalse(fct.isString()); + assertTrue(fct.isNumeric()); + assertFalse(fct.isGeometry()); + }catch(ParseException e1){ + e1.printStackTrace(); + fail("This query contains a DECLARED UDF: this test should have succeeded!"); + } + + // Test the return type checking inside a whole query: + try{ + assertNotNull(parser.parseQuery("SELECT tata()*3 AS \"aNumeric\" FROM foo;")); + }catch(ParseException e1){ + e1.printStackTrace(); + fail("This query contains a DECLARED UDF multiplicated by 3: this test should have succeeded!"); + } + try{ + parser.parseQuery("SELECT 'Blabla ' || tata() AS \"SuperError\" FROM foo;"); + fail("This query contains a DECLARED UDF BUT used as string...which is here not possible: this test should have failed!"); + }catch(ParseException e1){ + assertTrue(e1 instanceof UnresolvedIdentifiersException); + UnresolvedIdentifiersException ex = (UnresolvedIdentifiersException)e1; + assertEquals(1, ex.getNbErrors()); + assertEquals("Type mismatch! A string value was expected instead of \"tata()\".", ex.getErrors().next().getMessage()); + } + try{ + parser.parseQuery("SELECT tata() || 'Blabla ' AS \"SuperError\" FROM foo;"); + fail("This query contains a DECLARED UDF BUT used as string...which is here not possible: this test should have failed!"); + }catch(ParseException e1){ + assertTrue(e1 instanceof UnresolvedIdentifiersException); + UnresolvedIdentifiersException ex = (UnresolvedIdentifiersException)e1; + assertEquals(1, ex.getNbErrors()); + assertEquals("Type mismatch! A string value was expected instead of \"tata()\".", ex.getErrors().next().getMessage()); + } + + // Test the return type of the function TITI generated by the parser: + try{ + ADQLQuery query = parser.parseQuery("SELECT titi() FROM foo;"); + ADQLOperand fct = query.getSelect().get(0).getOperand(); + assertTrue(fct instanceof DefaultUDF); + assertNotNull(((DefaultUDF)fct).getDefinition()); + assertFalse(fct.isString()); + assertFalse(fct.isNumeric()); + assertTrue(fct.isGeometry()); + }catch(ParseException e1){ + e1.printStackTrace(); + fail("This query contains a DECLARED UDF: this test should have succeeded!"); + } + + // Test the return type checking inside a whole query: + try{ + parser.parseQuery("SELECT CONTAINS(colG, titi()) ' AS \"Super\" FROM foo;"); + fail("Geometrical UDFs are not allowed for the moment in the ADQL language: this test should have failed!"); + }catch(ParseException e1){ + assertTrue(e1 instanceof ParseException); + assertEquals(" Encountered \"(\". Was expecting one of: \")\" \".\" \".\" \")\" ", e1.getMessage()); + } + try{ + parser.parseQuery("SELECT titi()*3 AS \"SuperError\" FROM foo;"); + fail("This query contains a DECLARED UDF BUT used as numeric...which is here not possible: this test should have failed!"); + }catch(ParseException e1){ + assertTrue(e1 instanceof UnresolvedIdentifiersException); + UnresolvedIdentifiersException ex = (UnresolvedIdentifiersException)e1; + assertEquals(1, ex.getNbErrors()); + assertEquals("Type mismatch! A numeric value was expected instead of \"titi()\".", ex.getErrors().next().getMessage()); + } + + // CLEAR ALL UDFs AND ALLOW UNKNOWN FUNCTION: + parser = new ADQLParser(new DBChecker(tables, null)); + + // Test again: + try{ + assertNotNull(parser.parseQuery("SELECT toto() FROM foo;")); + }catch(ParseException e){ + e.printStackTrace(); + fail("The parser allow ANY unknown function: this test should have succeeded!"); + } + + // Test the return type of the function generated by the parser: + try{ + ADQLQuery query = parser.parseQuery("SELECT toto() FROM foo;"); + ADQLOperand fct = query.getSelect().get(0).getOperand(); + assertTrue(fct instanceof DefaultUDF); + assertNull(((DefaultUDF)fct).getDefinition()); + assertTrue(fct.isString()); + assertTrue(fct.isNumeric()); + }catch(ParseException e1){ + e1.printStackTrace(); + fail("The parser allow ANY unknown function: this test should have succeeded!"); + } + + // DECLARE THE UDF (while unknown functions are allowed): + parser = new ADQLParser(new DBChecker(tables, Arrays.asList(new FunctionDef[]{new FunctionDef("toto", new DBType(DBDatatype.VARCHAR))}))); + + // Test the return type of the function generated by the parser: + try{ + ADQLQuery query = parser.parseQuery("SELECT toto() FROM foo;"); + ADQLOperand fct = query.getSelect().get(0).getOperand(); + assertTrue(fct instanceof DefaultUDF); + assertNotNull(((DefaultUDF)fct).getDefinition()); + assertTrue(fct.isString()); + assertFalse(fct.isNumeric()); + }catch(ParseException e1){ + e1.printStackTrace(); + fail("The parser allow ANY unknown function: this test should have succeeded!"); + } + + // DECLARE UDFs WITH SAME NAMES BUT DIFFERENT TYPE OF ARGUMENT: + udfs = new FunctionDef[]{new FunctionDef("toto", new DBType(DBDatatype.VARCHAR), new FunctionParam[]{new FunctionParam("attr", new DBType(DBDatatype.VARCHAR))}),new FunctionDef("toto", new DBType(DBDatatype.INTEGER), new FunctionParam[]{new FunctionParam("attr", new DBType(DBDatatype.INTEGER))}),new FunctionDef("toto", new DBType(DBDatatype.INTEGER), new FunctionParam[]{new FunctionParam("attr", new DBType(DBDatatype.POINT))})}; + parser = new ADQLParser(new DBChecker(tables, Arrays.asList(udfs))); + + // Test the return type in function of the parameter: + try{ + assertNotNull(parser.parseQuery("SELECT toto('blabla') AS toto1, toto(123) AS toto2, toto(POINT('', 1, 2)) AS toto3 FROM foo;")); + }catch(ParseException e1){ + e1.printStackTrace(); + fail("This query contains two DECLARED UDFs used here: this test should have succeeded!"); + } + try{ + parser.parseQuery("SELECT toto('blabla') * 123 AS \"SuperError\" FROM foo;"); + fail("This query contains a DECLARED UDF BUT used as numeric...which is here not possible: this test should have failed!"); + }catch(ParseException e){ + assertTrue(e instanceof UnresolvedIdentifiersException); + UnresolvedIdentifiersException ex = (UnresolvedIdentifiersException)e; + assertEquals(1, ex.getNbErrors()); + assertEquals("Type mismatch! A numeric value was expected instead of \"toto('blabla')\".", ex.getErrors().next().getMessage()); + } + try{ + parser.parseQuery("SELECT toto(123) || 'blabla' AS \"SuperError\" FROM foo;"); + fail("This query contains a DECLARED UDF BUT used as string...which is here not possible: this test should have failed!"); + }catch(ParseException e){ + assertTrue(e instanceof UnresolvedIdentifiersException); + UnresolvedIdentifiersException ex = (UnresolvedIdentifiersException)e; + assertEquals(1, ex.getNbErrors()); + assertEquals("Type mismatch! A string value was expected instead of \"toto(123)\".", ex.getErrors().next().getMessage()); + } + try{ + parser.parseQuery("SELECT toto(POINT('', 1, 2)) || 'blabla' AS \"SuperError\" FROM foo;"); + fail("This query contains a DECLARED UDF BUT used as string...which is here not possible: this test should have failed!"); + }catch(ParseException e){ + assertTrue(e instanceof UnresolvedIdentifiersException); + UnresolvedIdentifiersException ex = (UnresolvedIdentifiersException)e; + assertEquals(1, ex.getNbErrors()); + assertEquals("Type mismatch! A string value was expected instead of \"toto(POINT('', 1, 2))\".", ex.getErrors().next().getMessage()); + } + } + + private static class WrongUDFToto extends UDFToto { + public WrongUDFToto(final ADQLOperand[] params) throws Exception{ + super(params); + throw new Exception("Systematic error!"); + } + } + + public static class UDFToto extends UserDefinedFunction { + protected StringConstant fakeParam; + + public UDFToto(final ADQLOperand[] params) throws Exception{ + if (params == null || params.length == 0) + throw new Exception("Missing parameter for the user defined function \"toto\"!"); + else if (params.length > 1) + throw new Exception("Too many parameters for the function \"toto\"! Only one is required."); + else if (!(params[0] instanceof StringConstant)) + throw new Exception("Wrong parameter type! The parameter of the UDF \"toto\" must be a string constant."); + fakeParam = (StringConstant)params[0]; + } + + @Override + public final boolean isNumeric(){ + return false; + } + + @Override + public final boolean isString(){ + return true; + } + + @Override + public final boolean isGeometry(){ + return false; + } + + @Override + public ADQLObject getCopy() throws Exception{ + ADQLOperand[] params = new ADQLOperand[]{(StringConstant)fakeParam.getCopy()}; + return new UDFToto(params); + } + + @Override + public final String getName(){ + return "toto"; + } + + @Override + public final ADQLOperand[] getParameters(){ + return new ADQLOperand[]{fakeParam}; + } + + @Override + public final int getNbParameters(){ + return 1; + } + + @Override + public final ADQLOperand getParameter(int index) throws ArrayIndexOutOfBoundsException{ + if (index != 0) + throw new ArrayIndexOutOfBoundsException("Incorrect parameter index: " + index + "! The function \"toto\" has only one parameter."); + return fakeParam; + } + + @Override + public ADQLOperand setParameter(int index, ADQLOperand replacer) throws ArrayIndexOutOfBoundsException, NullPointerException, Exception{ + if (index != 0) + throw new ArrayIndexOutOfBoundsException("Incorrect parameter index: " + index + "! The function \"toto\" has only one parameter."); + else if (!(replacer instanceof StringConstant)) + throw new Exception("Wrong parameter type! The parameter of the UDF \"toto\" must be a string constant."); + return (fakeParam = (StringConstant)replacer); + } + + @Override + public String translate(final ADQLTranslator caller) throws TranslationException{ + /* Note: Since this function is totally fake, this function will be replaced in SQL by its parameter (the string). */ + return caller.translate(fakeParam); + } + } + +} diff --git a/test/adql/db/TestFunctionDef.java b/test/adql/db/TestFunctionDef.java new file mode 100644 index 0000000..c3d738a --- /dev/null +++ b/test/adql/db/TestFunctionDef.java @@ -0,0 +1,312 @@ +package adql.db; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.junit.Test; + +import adql.db.DBType.DBDatatype; +import adql.db.FunctionDef.FunctionParam; +import adql.parser.ParseException; +import adql.query.operand.ADQLOperand; +import adql.query.operand.NumericConstant; +import adql.query.operand.StringConstant; +import adql.query.operand.function.ADQLFunction; +import adql.query.operand.function.DefaultUDF; +import adql.query.operand.function.geometry.PointFunction; + +public class TestFunctionDef { + + @Test + public void testIsString(){ + for(DBDatatype type : DBDatatype.values()){ + switch(type){ + case CHAR: + case VARCHAR: + case TIMESTAMP: + case CLOB: + assertTrue(new FunctionDef("foo", new DBType(type)).isString); + break; + default: + assertFalse(new FunctionDef("foo", new DBType(type)).isString); + } + } + } + + @Test + public void testIsGeometry(){ + for(DBDatatype type : DBDatatype.values()){ + switch(type){ + case POINT: + case REGION: + assertTrue(new FunctionDef("foo", new DBType(type)).isGeometry); + break; + default: + assertFalse(new FunctionDef("foo", new DBType(type)).isGeometry); + } + } + } + + @Test + public void testIsNumeric(){ + for(DBDatatype type : DBDatatype.values()){ + switch(type){ + case CHAR: + case VARCHAR: + case TIMESTAMP: + case POINT: + case REGION: + case CLOB: + assertFalse(new FunctionDef("foo", new DBType(type)).isNumeric); + break; + default: + assertTrue(new FunctionDef("foo", new DBType(type)).isNumeric); + } + } + } + + @Test + public void testToString(){ + assertEquals("fct1()", new FunctionDef("fct1").toString()); + assertEquals("fct1() -> VARCHAR", new FunctionDef("fct1", new DBType(DBDatatype.VARCHAR)).toString()); + assertEquals("fct1(foo DOUBLE) -> VARCHAR", new FunctionDef("fct1", new DBType(DBDatatype.VARCHAR), new FunctionParam[]{new FunctionParam("foo", new DBType(DBDatatype.DOUBLE))}).toString()); + assertEquals("fct1(foo DOUBLE)", new FunctionDef("fct1", new FunctionParam[]{new FunctionParam("foo", new DBType(DBDatatype.DOUBLE))}).toString()); + assertEquals("fct1(foo DOUBLE, pt POINT) -> VARCHAR", new FunctionDef("fct1", new DBType(DBDatatype.VARCHAR), new FunctionParam[]{new FunctionParam("foo", new DBType(DBDatatype.DOUBLE)),new FunctionParam("pt", new DBType(DBDatatype.POINT))}).toString()); + assertEquals("fct1(foo DOUBLE, pt POINT)", new FunctionDef("fct1", null, new FunctionParam[]{new FunctionParam("foo", new DBType(DBDatatype.DOUBLE)),new FunctionParam("pt", new DBType(DBDatatype.POINT))}).toString()); + } + + @Test + public void testParse(){ + final String WRONG_FULL_SYNTAX = "Wrong function definition syntax! Expected syntax: \"(?) ?\", where =\"[a-zA-Z]+[a-zA-Z0-9_]*\", =\" -> \", =\"( (, )*)\", should be one of the types described in the UPLOAD section of the TAP documentation. Examples of good syntax: \"foo()\", \"foo() -> VARCHAR\", \"foo(param INTEGER)\", \"foo(param1 INTEGER, param2 DOUBLE) -> DOUBLE\""; + final String WRONG_PARAM_SYNTAX = "Wrong parameters syntax! Expected syntax: \"( (, )*)\", where =\"[a-zA-Z]+[a-zA-Z0-9_]*\", should be one of the types described in the UPLOAD section of the TAP documentation. Examples of good syntax: \"()\", \"(param INTEGER)\", \"(param1 INTEGER, param2 DOUBLE)\""; + + // NULL test: + try{ + FunctionDef.parse(null); + fail("A NULL string is not valide!"); + }catch(Exception ex){ + assertTrue(ex instanceof NullPointerException); + assertEquals("Missing string definition to build a FunctionDef!", ex.getMessage()); + } + + // EMPTY STRING test: + try{ + FunctionDef.parse(""); + fail("An empty string is not valide!"); + }catch(Exception ex){ + assertTrue(ex instanceof ParseException); + assertEquals(WRONG_FULL_SYNTAX, ex.getMessage()); + } + + // CORRECT string definitions: + try{ + assertEquals("foo()", FunctionDef.parse("foo()").toString()); + assertEquals("foo() -> VARCHAR", FunctionDef.parse("foo() -> string").toString()); + assertEquals("foo() -> VARCHAR", FunctionDef.parse("foo()->string").toString()); + assertEquals("foo(toto VARCHAR) -> SMALLINT", FunctionDef.parse("foo(toto varchar) -> boolean").toString()); + assertEquals("foo(param1 DOUBLE, param2 INTEGER) -> DOUBLE", FunctionDef.parse(" foo ( param1 numeric, param2 int ) -> DOUBLE ").toString()); + assertEquals("foo_ALTernative2first(p POINT, d TIMESTAMP) -> TIMESTAMP", FunctionDef.parse("foo_ALTernative2first (p POINT,d date) -> time").toString()); + assertEquals("blabla_123(toto INTEGER, bla SMALLINT, truc CLOB, bidule CHAR, smurph POINT, date TIMESTAMP) -> SMALLINT", FunctionDef.parse("blabla_123(toto int4, bla bool, truc text, bidule character, smurph point, date timestamp) -> BOOLEAN").toString()); + }catch(Exception ex){ + ex.printStackTrace(System.err); + fail("All this string definitions are correct."); + } + + // TYPE PARAMETER test: + try{ + for(DBDatatype t : DBDatatype.values()){ + switch(t){ + case CHAR: + case VARCHAR: + case BINARY: + case VARBINARY: + assertEquals("foo() -> " + t.toString() + "(10)", FunctionDef.parse("foo() -> " + t.toString() + "(10)").toString()); + break; + default: + assertEquals("foo() -> " + t.toString(), FunctionDef.parse("foo() -> " + t.toString() + "(10)").toString()); + } + } + }catch(Exception ex){ + ex.printStackTrace(System.err); + fail("Wrong type parsing!"); + } + + // WRONG string definitions: + try{ + FunctionDef.parse("123()"); + fail("No number is allowed as first character of a function name!"); + }catch(Exception ex){ + assertTrue(ex instanceof ParseException); + assertEquals(WRONG_FULL_SYNTAX, ex.getMessage()); + } + try{ + FunctionDef.parse("1foo()"); + fail("No number is allowed as first character of a function name!"); + }catch(Exception ex){ + assertTrue(ex instanceof ParseException); + assertEquals(WRONG_FULL_SYNTAX, ex.getMessage()); + } + try{ + FunctionDef.parse("foo,truc()"); + fail("No other character than [a-zA-Z0-9_] is allowed after a first character [a-zA-Z] in a function name!"); + }catch(Exception ex){ + assertTrue(ex instanceof ParseException); + assertEquals(WRONG_FULL_SYNTAX, ex.getMessage()); + } + try{ + FunctionDef.parse("foo"); + fail("A function definition must contain at list parenthesis even if there is no parameter."); + }catch(Exception ex){ + assertTrue(ex instanceof ParseException); + assertEquals(WRONG_FULL_SYNTAX, ex.getMessage()); + } + try{ + FunctionDef.parse("foo(param)"); + fail("A parameter must always have a type!"); + }catch(Exception ex){ + assertTrue(ex instanceof ParseException); + assertEquals("Wrong syntax for the 1-th parameter: \"param\"! Expected syntax: \"( (, )*)\", where =\"[a-zA-Z]+[a-zA-Z0-9_]*\", should be one of the types described in the UPLOAD section of the TAP documentation. Examples of good syntax: \"()\", \"(param INTEGER)\", \"(param1 INTEGER, param2 DOUBLE)\"", ex.getMessage()); + } + try{ + FunctionDef.parse("foo()->aType"); + fail("Wrong (return) type!"); + }catch(Exception ex){ + assertTrue(ex instanceof ParseException); + assertEquals("Unknown return type: \"aType\"!", ex.getMessage()); + } + try{ + FunctionDef.parse("foo()->aType(10)"); + fail("Wrong (return) type!"); + }catch(Exception ex){ + assertTrue(ex instanceof ParseException); + assertEquals("Unknown return type: \"aType(10)\"!", ex.getMessage()); + } + try{ + FunctionDef.parse("foo() -> "); + fail("The return type is missing!"); + }catch(Exception ex){ + assertTrue(ex instanceof ParseException); + assertEquals(WRONG_FULL_SYNTAX, ex.getMessage()); + } + try{ + FunctionDef.parse("foo(,)"); + fail("Missing parameter definition!"); + }catch(Exception ex){ + assertTrue(ex instanceof ParseException); + assertEquals(WRONG_PARAM_SYNTAX, ex.getMessage()); + } + try{ + FunctionDef.parse("foo(param1 int,)"); + fail("Missing parameter definition!"); + }catch(Exception ex){ + assertTrue(ex instanceof ParseException); + assertEquals(WRONG_PARAM_SYNTAX, ex.getMessage()); + } + try{ + FunctionDef.parse("foo(param1 aType)"); + fail("Wrong parameter type!"); + }catch(Exception ex){ + assertTrue(ex instanceof ParseException); + assertEquals("Unknown type for the parameter \"param1\": \"aType\"!", ex.getMessage()); + } + try{ + FunctionDef.parse("foo(param1 aType(10))"); + fail("Wrong parameter type!"); + }catch(Exception ex){ + assertTrue(ex instanceof ParseException); + assertEquals("Unknown type for the parameter \"param1\": \"aType(10)\"!", ex.getMessage()); + } + } + + @Test + public void testCompareToFunctionDef(){ + // DEFINITION 1 :: fct1() -> VARCHAR + FunctionDef def1 = new FunctionDef("fct1", new DBType(DBDatatype.VARCHAR)); + + // TEST :: Identity test (def1 with def1): [EQUAL] + assertEquals(0, def1.compareTo(def1)); + + // TEST :: With a function having a different name and also no parameter: [GREATER] + assertEquals(1, def1.compareTo(new FunctionDef("fct0", new DBType(DBDatatype.VARCHAR)))); + + // TEST :: With a function having the same name, but a different return type: [EQUAL} + assertEquals(0, def1.compareTo(new FunctionDef("fct1", new DBType(DBDatatype.INTEGER)))); + + // TEST :: With a function having the same name, but 2 parameters: [LESS (4 characters: ø against 1010)] + assertEquals(-6, def1.compareTo(new FunctionDef("fct1", new DBType(DBDatatype.INTEGER), new FunctionParam[]{new FunctionParam("foo", new DBType(DBDatatype.INTEGER)),new FunctionParam("foo", new DBType(DBDatatype.INTEGER))}))); + + // DEFINITION 1 :: fct1(foo1 CHAR(12), foo2 DOUBLE) -> VARCHAR + def1 = new FunctionDef("fct1", new DBType(DBDatatype.VARCHAR), new FunctionParam[]{new FunctionParam("foo1", new DBType(DBDatatype.CHAR, 12)),new FunctionParam("foo2", new DBType(DBDatatype.DOUBLE))}); + + // TEST :: Identity test (def1 with def1): [EQUAL] + assertEquals(0, def1.compareTo(def1)); + + // DEFINITION 2 :: fct1(foo1 CHAR(12), foo2 VARCHAR) -> VARCHAR + FunctionDef def2 = new FunctionDef("fct1", new DBType(DBDatatype.VARCHAR), new FunctionParam[]{new FunctionParam("foo1", new DBType(DBDatatype.CHAR, 12)),new FunctionParam("foo2", new DBType(DBDatatype.VARCHAR))}); + + // TEST :: Identity test (def2 with def2): [EQUAL] + assertEquals(0, def2.compareTo(def2)); + + // TEST :: Same name, but different type for the last parameter only: [GREATER (because Numeric = 10 > String = 01)] + assertEquals(1, def1.compareTo(def2)); + + // DEFINITION 2 :: fct2(foo1 CHAR(12), foo2 DOUBLE) -> VARCHAR + def2 = new FunctionDef("fct2", new DBType(DBDatatype.VARCHAR), new FunctionParam[]{new FunctionParam("foo1", new DBType(DBDatatype.CHAR, 12)),new FunctionParam("foo2", new DBType(DBDatatype.DOUBLE))}); + + // TEST :: Identity test (def2 with def2): [EQUAL] + assertEquals(0, def2.compareTo(def2)); + + // TEST :: Different name but same parameters: [LESS] + assertEquals(-1, def1.compareTo(def2)); + + // DEFINITION 2 :: fct1(foo1 CHAR(12), foo2 POINT) -> VARCHAR + def2 = new FunctionDef("fct1", new DBType(DBDatatype.VARCHAR), new FunctionParam[]{new FunctionParam("foo1", new DBType(DBDatatype.CHAR, 12)),new FunctionParam("foo2", new DBType(DBDatatype.POINT))}); + + // TEST :: Identity test (def2 with def2): [EQUAL] + assertEquals(0, def2.compareTo(def2)); + + // TEST :: Same name, but different type for the last parameter only: [GREATER] + assertEquals(1, def1.compareTo(def2)); + } + + @Test + public void testCompareToADQLFunction(){ + // DEFINITION :: fct1() -> VARCHAR + FunctionDef def = new FunctionDef("fct1", new DBType(DBDatatype.VARCHAR)); + + // TEST :: NULL: + try{ + def.compareTo((ADQLFunction)null); + fail("Missing ADQL function for comparison with FunctionDef!"); + }catch(Exception e){ + assertTrue(e instanceof NullPointerException); + assertEquals("Missing ADQL function with which comparing this function definition!", e.getMessage()); + } + + // TEST :: "fct1()": [EQUAL] + assertEquals(0, def.compareTo(new DefaultUDF("fct1", null))); + + // TEST :: "fct0()": [GREATER] + assertEquals(1, def.compareTo(new DefaultUDF("fct0", null))); + + // TEST :: "fct1(12.3, 3.14)": [LESS (of 2 params)] + assertEquals(-2, def.compareTo(new DefaultUDF("fct1", new ADQLOperand[]{new NumericConstant(12.3),new NumericConstant(3.14)}))); + + // DEFINITION :: fct1(foo1 CHAR(12), foo2 DOUBLE) -> VARCHAR + def = new FunctionDef("fct1", new DBType(DBDatatype.VARCHAR), new FunctionParam[]{new FunctionParam("foo1", new DBType(DBDatatype.CHAR, 12)),new FunctionParam("foo2", new DBType(DBDatatype.DOUBLE))}); + + // TEST :: "fct1('blabla', 'blabla2')": [GREATER (because the second param is numeric and Numeric = 10 > String = 01)] + assertEquals(1, def.compareTo(new DefaultUDF("fct1", new ADQLOperand[]{new StringConstant("blabla"),new StringConstant("blabla2")}))); + + // TEST :: "fct1('blabla', POINT('COORDSYS', 1.2, 3.4))": [GREATER (same reason ; POINT is considered as a String)] + try{ + assertEquals(1, def.compareTo(new DefaultUDF("fct1", new ADQLOperand[]{new StringConstant("blabla"),new PointFunction(new StringConstant("COORDSYS"), new NumericConstant(1.2), new NumericConstant(3.4))}))); + }catch(Exception e){ + e.printStackTrace(); + fail(); + } + } + +} diff --git a/test/adql/db/TestSTCS.java b/test/adql/db/TestSTCS.java new file mode 100644 index 0000000..8ff7646 --- /dev/null +++ b/test/adql/db/TestSTCS.java @@ -0,0 +1,536 @@ +package adql.db; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.StringBufferInputStream; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import adql.db.STCS.CoordSys; +import adql.db.STCS.Flavor; +import adql.db.STCS.Frame; +import adql.db.STCS.RefPos; +import adql.db.STCS.Region; +import adql.db.STCS.RegionType; +import adql.parser.ADQLParser; +import adql.parser.ParseException; +import adql.query.operand.ADQLColumn; +import adql.query.operand.ADQLOperand; +import adql.query.operand.NegativeOperand; +import adql.query.operand.NumericConstant; +import adql.query.operand.Operation; +import adql.query.operand.OperationType; +import adql.query.operand.StringConstant; +import adql.query.operand.function.geometry.BoxFunction; +import adql.query.operand.function.geometry.CircleFunction; +import adql.query.operand.function.geometry.ContainsFunction; +import adql.query.operand.function.geometry.GeometryFunction; +import adql.query.operand.function.geometry.GeometryFunction.GeometryValue; +import adql.query.operand.function.geometry.PointFunction; +import adql.query.operand.function.geometry.PolygonFunction; +import adql.query.operand.function.geometry.RegionFunction; + +@SuppressWarnings("deprecation") +public class TestSTCS { + + @BeforeClass + public static void setUpBeforeClass() throws Exception{} + + @AfterClass + public static void tearDownAfterClass() throws Exception{} + + @Before + public void setUp() throws Exception{} + + @After + public void tearDown() throws Exception{} + + @Test + public void buildRegion(){ + // Special values: + try{ + new Region((GeometryFunction)null); + fail(); + }catch(Exception e){ + assertTrue(e instanceof NullPointerException); + assertEquals("Missing geometry to convert into STCS.Region!", e.getMessage()); + } + + try{ + new Region((Region)null); + fail(); + }catch(Exception e){ + assertTrue(e instanceof NullPointerException); + assertEquals("Missing region to NOT select!", e.getMessage()); + } + + try{ + new Region(new ContainsFunction(new GeometryValue(new RegionFunction(new StringConstant("position 1 2"))), new GeometryValue(new RegionFunction(new StringConstant("circle 0 1 4"))))); + fail(); + }catch(Exception e){ + assertTrue(e instanceof IllegalArgumentException); + assertEquals("Unknown region type! Only geometrical function PointFunction, CircleFunction, BoxFunction, PolygonFunction and RegionFunction are allowed.", e.getMessage()); + } + + // Allowed values (1 test for each type of region): + try{ + Region r = new Region(new PointFunction(new StringConstant(""), new NumericConstant(1.2), new NegativeOperand(new NumericConstant(2.3)))); + assertEquals(RegionType.POSITION, r.type); + assertEquals("", r.coordSys.toSTCS()); + assertEquals(1, r.coordinates.length); + assertEquals(2, r.coordinates[0].length); + assertEquals(1.2, r.coordinates[0][0], 0); + assertEquals(-2.3, r.coordinates[0][1], 0); + assertEquals(Double.NaN, r.radius, 0); + assertEquals(Double.NaN, r.width, 0); + assertEquals(Double.NaN, r.height, 0); + assertNull(r.regions); + assertEquals("POSITION 1.2 -2.3", r.toSTCS()); + + r = new Region(new CircleFunction(new StringConstant("ICRS"), new NumericConstant(1.2), new NegativeOperand(new NumericConstant(2.3)), new NumericConstant(5))); + assertEquals(RegionType.CIRCLE, r.type); + assertEquals("ICRS", r.coordSys.toSTCS()); + assertEquals(1, r.coordinates.length); + assertEquals(2, r.coordinates[0].length); + assertEquals(1.2, r.coordinates[0][0], 0); + assertEquals(-2.3, r.coordinates[0][1], 0); + assertEquals(5, r.radius, 0); + assertEquals(Double.NaN, r.width, 0); + assertEquals(Double.NaN, r.height, 0); + assertNull(r.regions); + assertEquals("CIRCLE ICRS 1.2 -2.3 5.0", r.toSTCS()); + + r = new Region(new BoxFunction(new StringConstant("ICRS heliocenter"), new NumericConstant(1.2), new NegativeOperand(new NumericConstant(2.3)), new NumericConstant(5), new NumericConstant(4.6))); + assertEquals(RegionType.BOX, r.type); + assertEquals("ICRS HELIOCENTER", r.coordSys.toSTCS()); + assertEquals(1, r.coordinates.length); + assertEquals(2, r.coordinates[0].length); + assertEquals(1.2, r.coordinates[0][0], 0); + assertEquals(-2.3, r.coordinates[0][1], 0); + assertEquals(Double.NaN, r.radius, 0); + assertEquals(5, r.width, 0); + assertEquals(4.6, r.height, 0); + assertNull(r.regions); + assertEquals("BOX ICRS HELIOCENTER 1.2 -2.3 5.0 4.6", r.toSTCS()); + + r = new Region(new PolygonFunction(new StringConstant("cartesian2"), new ADQLOperand[]{new NumericConstant(1.2),new NegativeOperand(new NumericConstant(2.3)),new NumericConstant(5),new NumericConstant(4.6),new NegativeOperand(new NumericConstant(.89)),new NumericConstant(1)})); + assertEquals(RegionType.POLYGON, r.type); + assertEquals("CARTESIAN2", r.coordSys.toSTCS()); + assertEquals(3, r.coordinates.length); + assertEquals(2, r.coordinates[0].length); + assertEquals(1.2, r.coordinates[0][0], 0); + assertEquals(-2.3, r.coordinates[0][1], 0); + assertEquals(5, r.coordinates[1][0], 0); + assertEquals(4.6, r.coordinates[1][1], 0); + assertEquals(-0.89, r.coordinates[2][0], 0); + assertEquals(1, r.coordinates[2][1], 0); + assertEquals(Double.NaN, r.radius, 0); + assertEquals(Double.NaN, r.width, 0); + assertEquals(Double.NaN, r.height, 0); + assertNull(r.regions); + assertEquals("POLYGON CARTESIAN2 1.2 -2.3 5.0 4.6 -0.89 1.0", r.toSTCS()); + + r = new Region(new RegionFunction(new StringConstant("position ICrs 1.2 -2.3"))); + assertEquals(RegionType.POSITION, r.type); + assertEquals("ICRS", r.coordSys.toSTCS()); + assertEquals(1, r.coordinates.length); + assertEquals(2, r.coordinates[0].length); + assertEquals(1.2, r.coordinates[0][0], 0); + assertEquals(-2.3, r.coordinates[0][1], 0); + assertEquals(Double.NaN, r.radius, 0); + assertEquals(Double.NaN, r.width, 0); + assertEquals(Double.NaN, r.height, 0); + assertNull(r.regions); + assertEquals("POSITION ICRS 1.2 -2.3", r.toSTCS()); + + r = new Region(new RegionFunction(new StringConstant("Union ICRS (Polygon 1 4 2 4 2 5 1 5 Polygon 3 4 4 4 4 5 3 5)"))); + assertEquals(RegionType.UNION, r.type); + assertEquals("ICRS", r.coordSys.toSTCS()); + assertNull(r.coordinates); + assertEquals(Double.NaN, r.radius, 0); + assertEquals(Double.NaN, r.width, 0); + assertEquals(Double.NaN, r.height, 0); + assertEquals(2, r.regions.length); + assertEquals("UNION ICRS (POLYGON 1.0 4.0 2.0 4.0 2.0 5.0 1.0 5.0 POLYGON 3.0 4.0 4.0 4.0 4.0 5.0 3.0 5.0)", r.toString()); + // inner region 1 + Region innerR = r.regions[0]; + assertEquals(RegionType.POLYGON, innerR.type); + assertEquals("", innerR.coordSys.toSTCS()); + assertEquals(4, innerR.coordinates.length); + assertEquals(2, innerR.coordinates[0].length); + assertEquals(1, innerR.coordinates[0][0], 0); + assertEquals(4, innerR.coordinates[0][1], 0); + assertEquals(2, innerR.coordinates[1][0], 0); + assertEquals(4, innerR.coordinates[1][1], 0); + assertEquals(2, innerR.coordinates[2][0], 0); + assertEquals(5, innerR.coordinates[2][1], 0); + assertEquals(1, innerR.coordinates[3][0], 0); + assertEquals(5, innerR.coordinates[3][1], 0); + assertEquals(Double.NaN, innerR.radius, 0); + assertEquals(Double.NaN, innerR.width, 0); + assertEquals(Double.NaN, innerR.height, 0); + assertNull(innerR.regions); + assertEquals("POLYGON 1.0 4.0 2.0 4.0 2.0 5.0 1.0 5.0", innerR.toSTCS()); + // inner region 2 + innerR = r.regions[1]; + assertEquals(RegionType.POLYGON, innerR.type); + assertEquals("", innerR.coordSys.toSTCS()); + assertEquals(4, innerR.coordinates.length); + assertEquals(2, innerR.coordinates[0].length); + assertEquals(3, innerR.coordinates[0][0], 0); + assertEquals(4, innerR.coordinates[0][1], 0); + assertEquals(4, innerR.coordinates[1][0], 0); + assertEquals(4, innerR.coordinates[1][1], 0); + assertEquals(4, innerR.coordinates[2][0], 0); + assertEquals(5, innerR.coordinates[2][1], 0); + assertEquals(3, innerR.coordinates[3][0], 0); + assertEquals(5, innerR.coordinates[3][1], 0); + assertEquals(Double.NaN, innerR.radius, 0); + assertEquals(Double.NaN, innerR.width, 0); + assertEquals(Double.NaN, innerR.height, 0); + assertNull(innerR.regions); + assertEquals("POLYGON 3.0 4.0 4.0 4.0 4.0 5.0 3.0 5.0", innerR.toSTCS()); + + r = new Region(new RegionFunction(new StringConstant("NOT(CIRCLE ICRS 1.2 -2.3 5)"))); + assertEquals(RegionType.NOT, r.type); + assertNull(r.coordSys); + assertNull(r.coordinates); + assertEquals(Double.NaN, r.radius, 0); + assertEquals(Double.NaN, r.width, 0); + assertEquals(Double.NaN, r.height, 0); + assertEquals(1, r.regions.length); + assertEquals("NOT(CIRCLE ICRS 1.2 -2.3 5.0)", r.toSTCS()); + // inner region + innerR = r.regions[0]; + assertEquals(RegionType.CIRCLE, innerR.type); + assertEquals("ICRS", innerR.coordSys.toSTCS()); + assertEquals(1, innerR.coordinates.length); + assertEquals(2, innerR.coordinates[0].length); + assertEquals(1.2, innerR.coordinates[0][0], 0); + assertEquals(-2.3, innerR.coordinates[0][1], 0); + assertEquals(5, innerR.radius, 0); + assertEquals(Double.NaN, innerR.width, 0); + assertEquals(Double.NaN, innerR.height, 0); + assertNull(innerR.regions); + assertEquals("CIRCLE ICRS 1.2 -2.3 5.0", innerR.toSTCS()); + }catch(Exception e){ + e.printStackTrace(System.err); + fail(); + } + + // Test with incorrect syntaxes: + try{ + new Region(new PointFunction(new StringConstant(""), new StringConstant("1.2"), new NegativeOperand(new NumericConstant(2.3)))); + fail("The first coordinate is a StringConstant rather than a NumericConstant!"); + }catch(Exception e){ + assertTrue(e instanceof ParseException); + assertEquals("Can not convert into STC-S a non numeric argument (including ADQLColumn and Operation)!", e.getMessage()); + } + try{ + new Region(new PointFunction(new NumericConstant(.65), new NumericConstant(1.2), new NegativeOperand(new NumericConstant(2.3)))); + fail("The coordinate system is a NumericConstant rather than a StringConstant!"); + }catch(Exception e){ + assertTrue(e instanceof ParseException); + assertEquals("A coordinate system must be a string literal: \"0.65\" is not a string operand!", e.getMessage()); + } + try{ + new Region(new PointFunction(new StringConstant(""), null, new NegativeOperand(new NumericConstant(2.3)))); + fail("The first coordinate is missing!"); + }catch(Exception e){ + assertTrue(e instanceof NullPointerException); + assertEquals("The POINT function must have non-null coordinates!", e.getMessage()); + } + try{ + new Region(new RegionFunction(new StringConstant(""))); + fail("Missing STC-S expression!"); + }catch(Exception e){ + assertTrue(e instanceof ParseException); + assertEquals("Missing STC-S expression to parse!", e.getMessage()); + } + try{ + new Region(new RegionFunction(new StringConstant("MyRegion HERE 1.2"))); + fail("Totally incorrect region type!"); + }catch(Exception e){ + assertTrue(e instanceof ParseException); + assertEquals("Unknown STC region type: \"MYREGION\"!", e.getMessage()); + } + try{ + new Region(new RegionFunction((new ADQLParser(new StringBufferInputStream("'POSITION ' || coordinateSys || ' ' || ra || ' ' || dec"))).StringExpression())); + fail("String concatenation can not be managed!"); + }catch(Exception e){ + assertTrue(e instanceof ParseException); + assertEquals("Can not convert into STC-S a non string argument (including ADQLColumn and Concatenation)!", e.getMessage()); + } + try{ + new Region(new PointFunction(new ADQLColumn("coordSys"), new NumericConstant(1), new NumericConstant(2))); + fail("Columns can not be managed!"); + }catch(Exception e){ + assertTrue(e instanceof ParseException); + assertEquals("Can not convert into STC-S a non string argument (including ADQLColumn and Concatenation)!", e.getMessage()); + } + try{ + new Region(new PointFunction(new StringConstant("ICRS"), new Operation(new NumericConstant(2), OperationType.MULT, new NumericConstant(5)), new NumericConstant(2))); + fail("Operations can not be managed!"); + }catch(Exception e){ + assertTrue(e instanceof ParseException); + assertEquals("Can not convert into STC-S a non numeric argument (including ADQLColumn and Operation)!", e.getMessage()); + } + } + + @Test + public void parseCoordSys(){ + // GOOD SYNTAXES: + try{ + CoordSys p; + + // Default coordinate system (should be then interpreted as local coordinate system): + for(String s : new String[]{null,""," "}){ + p = STCS.parseCoordSys(s); + assertEquals(Frame.UNKNOWNFRAME, p.frame); + assertEquals(RefPos.UNKNOWNREFPOS, p.refpos); + assertEquals(Flavor.SPHERICAL2, p.flavor); + assertTrue(p.isDefault()); + } + + // Just a frame: + p = STCS.parseCoordSys("ICRS"); + assertEquals(Frame.ICRS, p.frame); + assertEquals(RefPos.UNKNOWNREFPOS, p.refpos); + assertEquals(Flavor.SPHERICAL2, p.flavor); + assertFalse(p.isDefault()); + + // Just a reference position: + p = STCS.parseCoordSys("LSR"); + assertEquals(Frame.UNKNOWNFRAME, p.frame); + assertEquals(RefPos.LSR, p.refpos); + assertEquals(Flavor.SPHERICAL2, p.flavor); + assertFalse(p.isDefault()); + + // Just a flavor: + p = STCS.parseCoordSys("CARTESIAN2"); + assertEquals(Frame.UNKNOWNFRAME, p.frame); + assertEquals(RefPos.UNKNOWNREFPOS, p.refpos); + assertEquals(Flavor.CARTESIAN2, p.flavor); + assertFalse(p.isDefault()); + + // Frame + RefPos: + p = STCS.parseCoordSys("ICRS LSR"); + assertEquals(Frame.ICRS, p.frame); + assertEquals(RefPos.LSR, p.refpos); + assertEquals(Flavor.SPHERICAL2, p.flavor); + assertFalse(p.isDefault()); + + // Frame + Flavor: + p = STCS.parseCoordSys("ICRS SPHERICAL2"); + assertEquals(Frame.ICRS, p.frame); + assertEquals(RefPos.UNKNOWNREFPOS, p.refpos); + assertEquals(Flavor.SPHERICAL2, p.flavor); + assertFalse(p.isDefault()); + + // RefPos + Flavor: + p = STCS.parseCoordSys("HELIOCENTER SPHERICAL2"); + assertEquals(Frame.UNKNOWNFRAME, p.frame); + assertEquals(RefPos.HELIOCENTER, p.refpos); + assertEquals(Flavor.SPHERICAL2, p.flavor); + assertFalse(p.isDefault()); + + // Frame + RefPos + Flavor + p = STCS.parseCoordSys("ICRS GEOCENTER SPHERICAL2"); + assertEquals(Frame.ICRS, p.frame); + assertEquals(RefPos.GEOCENTER, p.refpos); + assertEquals(Flavor.SPHERICAL2, p.flavor); + assertFalse(p.isDefault()); + + // Lets try in a different case: + p = STCS.parseCoordSys("icrs Geocenter SpheriCAL2"); + assertEquals(Frame.ICRS, p.frame); + assertEquals(RefPos.GEOCENTER, p.refpos); + assertEquals(Flavor.SPHERICAL2, p.flavor); + assertFalse(p.isDefault()); + }catch(Exception e){ + e.printStackTrace(System.err); + fail(); + } + + // WRONG SYNTAXES: + try{ + STCS.parseCoordSys("HOME"); + fail(); + }catch(Exception e){ + assertTrue(e instanceof ParseException); + assertEquals("Incorrect syntax: \"HOME\" was unexpected! Expected syntax: \"[(ECLIPTIC|FK4|FK5|GALACTIC|ICRS|UNKNOWNFRAME)] [(BARYCENTER|GEOCENTER|HELIOCENTER|LSR|TOPOCENTER|RELOCATABLE|UNKNOWNREFPOS)] [(CARTESIAN2|CARTESIAN3|SPHERICAL2)]\" ; an empty string is also allowed and will be interpreted as the coordinate system locally used.", e.getMessage()); + } + + // With wrong reference position: + try{ + STCS.parseCoordSys("ICRS HOME SPHERICAL2"); + fail(); + }catch(Exception e){ + assertTrue(e instanceof ParseException); + assertEquals("Incorrect syntax: \"HOME SPHERICAL2\" was unexpected! Expected syntax: \"[(ECLIPTIC|FK4|FK5|GALACTIC|ICRS|UNKNOWNFRAME)] [(BARYCENTER|GEOCENTER|HELIOCENTER|LSR|TOPOCENTER|RELOCATABLE|UNKNOWNREFPOS)] [(CARTESIAN2|CARTESIAN3|SPHERICAL2)]\" ; an empty string is also allowed and will be interpreted as the coordinate system locally used.", e.getMessage()); + } + + // With a cartesian flavor: + try{ + STCS.parseCoordSys("ICRS CARTESIAN2"); + fail(); + }catch(Exception e){ + assertTrue(e instanceof ParseException); + assertEquals("a coordinate system expressed with a cartesian flavor MUST have an UNKNOWNFRAME and UNKNOWNREFPOS!", e.getMessage()); + } + try{ + STCS.parseCoordSys("LSR CARTESIAN3"); + fail(); + }catch(Exception e){ + assertTrue(e instanceof ParseException); + assertEquals("a coordinate system expressed with a cartesian flavor MUST have an UNKNOWNFRAME and UNKNOWNREFPOS!", e.getMessage()); + } + try{ + CoordSys p = STCS.parseCoordSys("CARTESIAN2"); + assertEquals(Frame.UNKNOWNFRAME, p.frame); + assertEquals(RefPos.UNKNOWNREFPOS, p.refpos); + assertEquals(Flavor.CARTESIAN2, p.flavor); + + p = STCS.parseCoordSys("CARTESIAN3"); + assertEquals(Frame.UNKNOWNFRAME, p.frame); + assertEquals(RefPos.UNKNOWNREFPOS, p.refpos); + assertEquals(Flavor.CARTESIAN3, p.flavor); + }catch(Exception e){ + e.printStackTrace(System.err); + fail(); + } + + // Without spaces: + try{ + STCS.parseCoordSys("icrsGeocentercarteSIAN2"); + fail(); + }catch(Exception e){ + assertTrue(e instanceof ParseException); + assertEquals("Incorrect syntax: \"icrsGeocentercarteSIAN2\" was unexpected! Expected syntax: \"[(ECLIPTIC|FK4|FK5|GALACTIC|ICRS|UNKNOWNFRAME)] [(BARYCENTER|GEOCENTER|HELIOCENTER|LSR|TOPOCENTER|RELOCATABLE|UNKNOWNREFPOS)] [(CARTESIAN2|CARTESIAN3|SPHERICAL2)]\" ; an empty string is also allowed and will be interpreted as the coordinate system locally used.", e.getMessage()); + } + } + + @Test + public void serializeCoordSys(){ + try{ + assertEquals("", STCS.toSTCS((CoordSys)null)); + + assertEquals("", STCS.toSTCS(new CoordSys())); + + assertEquals("", STCS.toSTCS(new CoordSys(null, null, null))); + assertEquals("", STCS.toSTCS(new CoordSys(Frame.DEFAULT, RefPos.DEFAULT, Flavor.DEFAULT))); + assertEquals("", STCS.toSTCS(new CoordSys(Frame.UNKNOWNFRAME, RefPos.UNKNOWNREFPOS, Flavor.SPHERICAL2))); + + assertEquals("", STCS.toSTCS(new CoordSys(null))); + assertEquals("", STCS.toSTCS(new CoordSys(""))); + assertEquals("", STCS.toSTCS(new CoordSys(" \n\r"))); + + assertEquals("ICRS", STCS.toSTCS(new CoordSys(Frame.ICRS, null, null))); + assertEquals("ICRS", STCS.toSTCS(new CoordSys(Frame.ICRS, RefPos.DEFAULT, Flavor.DEFAULT))); + assertEquals("ICRS", STCS.toSTCS(new CoordSys(Frame.ICRS, RefPos.UNKNOWNREFPOS, Flavor.SPHERICAL2))); + + assertEquals("GEOCENTER", STCS.toSTCS(new CoordSys(null, RefPos.GEOCENTER, null))); + assertEquals("GEOCENTER", STCS.toSTCS(new CoordSys(Frame.DEFAULT, RefPos.GEOCENTER, Flavor.DEFAULT))); + assertEquals("GEOCENTER", STCS.toSTCS(new CoordSys(Frame.UNKNOWNFRAME, RefPos.GEOCENTER, Flavor.SPHERICAL2))); + + assertEquals("CARTESIAN3", STCS.toSTCS(new CoordSys(null, null, Flavor.CARTESIAN3))); + assertEquals("CARTESIAN3", STCS.toSTCS(new CoordSys(Frame.DEFAULT, RefPos.UNKNOWNREFPOS, Flavor.CARTESIAN3))); + assertEquals("CARTESIAN3", STCS.toSTCS(new CoordSys(Frame.UNKNOWNFRAME, RefPos.UNKNOWNREFPOS, Flavor.CARTESIAN3))); + + assertEquals("ICRS GEOCENTER", STCS.toSTCS(new CoordSys(Frame.ICRS, RefPos.GEOCENTER, null))); + assertEquals("ICRS GEOCENTER", STCS.toSTCS(new CoordSys(Frame.ICRS, RefPos.GEOCENTER, Flavor.DEFAULT))); + + assertEquals("UNKNOWNFRAME UNKNOWNREFPOS SPHERICAL2", new CoordSys().toFullSTCS()); + assertEquals("UNKNOWNFRAME UNKNOWNREFPOS SPHERICAL2", new CoordSys("").toFullSTCS()); + assertEquals("UNKNOWNFRAME UNKNOWNREFPOS SPHERICAL2", new CoordSys(null).toFullSTCS()); + assertEquals("UNKNOWNFRAME UNKNOWNREFPOS SPHERICAL2", new CoordSys(" \n\t").toFullSTCS()); + assertEquals("UNKNOWNFRAME UNKNOWNREFPOS SPHERICAL2", new CoordSys(null, null, null).toFullSTCS()); + assertEquals("UNKNOWNFRAME UNKNOWNREFPOS SPHERICAL2", new CoordSys(Frame.DEFAULT, RefPos.DEFAULT, Flavor.DEFAULT).toFullSTCS()); + assertEquals("ICRS UNKNOWNREFPOS SPHERICAL2", new CoordSys(Frame.ICRS, null, null).toFullSTCS()); + assertEquals("ICRS UNKNOWNREFPOS SPHERICAL2", new CoordSys(Frame.ICRS, RefPos.DEFAULT, Flavor.DEFAULT).toFullSTCS()); + assertEquals("UNKNOWNFRAME GEOCENTER SPHERICAL2", new CoordSys(Frame.UNKNOWNFRAME, RefPos.GEOCENTER, Flavor.DEFAULT).toFullSTCS()); + assertEquals("UNKNOWNFRAME UNKNOWNREFPOS CARTESIAN3", new CoordSys(Frame.DEFAULT, RefPos.DEFAULT, Flavor.CARTESIAN3).toFullSTCS()); + assertEquals("ICRS GEOCENTER SPHERICAL2", new CoordSys(Frame.ICRS, RefPos.GEOCENTER, Flavor.DEFAULT).toFullSTCS()); + }catch(ParseException pe){ + pe.printStackTrace(System.err); + fail(); + } + } + + @Test + public void parseRegion(){ + // TESTS WITH NO STC-S: + try{ + STCS.parseRegion(null); + fail(); + }catch(Exception e){ + assertTrue(e instanceof ParseException); + assertEquals("Missing STC-S expression to parse!", e.getMessage()); + } + try{ + STCS.parseRegion(""); + fail(); + }catch(Exception e){ + assertTrue(e instanceof ParseException); + assertEquals("Missing STC-S expression to parse!", e.getMessage()); + } + try{ + STCS.parseRegion(" \n\r"); + fail(); + }catch(Exception e){ + assertTrue(e instanceof ParseException); + assertEquals("Missing STC-S expression to parse!", e.getMessage()); + } + + // TESTS WITH A VALID EXPRESSION, EACH OF A DIFFERENT REGION TYPE: + String[] expressions = new String[]{" Position GALACTIC 10 20","Circle ICRS GEOCENTER 10 20 0.5 ","BOX cartesian2 3 3 2 2","Polygon 1 4 2 4 2 5 1 5","Union ICRS (Polygon 1 4 2 4 2 5 1 5 Polygon 3 4 4 4 4 5 3 5)","INTERSECTION ICRS (Polygon 1 4 2 4 2 5 1 5 Polygon 3 4 4 4 4 5 3 5)","NOT(Circle ICRS GEOCENTER 10 20 0.5)"}; + try{ + for(String e : expressions) + STCS.parseRegion(e); + }catch(Exception ex){ + ex.printStackTrace(System.err); + fail(); + } + + // TEST WITH A MISSING PARAMETER: + expressions = new String[]{" Position GALACTIC 10 ","BOX cartesian2 3 3 2","NOT()"}; + for(String e : expressions){ + try{ + STCS.parseRegion(e); + fail(); + }catch(Exception ex){ + assertTrue(ex instanceof ParseException); + assertTrue(ex.getMessage().startsWith("Unexpected End Of Expression! Expected syntax: \"")); + } + } + + // TEST WITH A WRONG COORDINATE SYSTEM (since it is optional in all these expressions, it will be considered as a coordinate...which is of course, not the case): + try{ + STCS.parseRegion("Circle HERE 10 20 0.5 "); + fail(); + }catch(Exception ex){ + assertTrue(ex instanceof ParseException); + assertTrue(ex.getMessage().startsWith("Incorrect syntax: a coordinates pair (2 numerics separated by one or more spaces) was expected! Expected syntax: \"CIRCLE \", where coordPair=\" \", radius=\"\" and coordSys=\"[(ECLIPTIC|FK4|FK5|GALACTIC|ICRS|UNKNOWNFRAME)] [(BARYCENTER|GEOCENTER|HELIOCENTER|LSR|TOPOCENTER|RELOCATABLE|UNKNOWNREFPOS)] [(CARTESIAN2|CARTESIAN3|SPHERICAL2)]\" ; an empty string is also allowed and will be interpreted as the coordinate system locally used.")); + } + + // TEST WITH EITHER A WRONG NUMERIC (L in lower case instead of 1) OR A MISSING OPENING PARENTHESIS: + expressions = new String[]{"Polygon 1 4 2 4 2 5 l 5","Union ICRS Polygon 1 4 2 4 2 5 1 5 Polygon 3 4 4 4 4 5 3 5)"}; + for(String e : expressions){ + try{ + STCS.parseRegion(e); + fail(); + }catch(Exception ex){ + assertTrue(ex instanceof ParseException); + assertTrue(ex.getMessage().startsWith("Incorrect syntax: ")); + } + } + } +} diff --git a/test/adql/parser/TestADQLParser.java b/test/adql/parser/TestADQLParser.java new file mode 100644 index 0000000..10837d5 --- /dev/null +++ b/test/adql/parser/TestADQLParser.java @@ -0,0 +1,43 @@ +package adql.parser; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import adql.query.ADQLQuery; +import adql.query.operand.StringConstant; + +public class TestADQLParser { + + @BeforeClass + public static void setUpBeforeClass() throws Exception{} + + @AfterClass + public static void tearDownAfterClass() throws Exception{} + + @Before + public void setUp() throws Exception{} + + @After + public void tearDown() throws Exception{} + + @Test + public void test(){ + ADQLParser parser = new ADQLParser(); + try{ + ADQLQuery query = parser.parseQuery("SELECT 'truc''machin' 'bidule' -- why not a comment now ^^\n'FIN' FROM foo;"); + assertNotNull(query); + assertEquals("truc'machinbiduleFIN", ((StringConstant)(query.getSelect().get(0).getOperand())).getValue()); + assertEquals("'truc''machinbiduleFIN'", query.getSelect().get(0).getOperand().toADQL()); + }catch(Exception ex){ + fail("String litteral concatenation is perfectly legal according to the ADQL standard."); + } + } + +} diff --git a/test/adql/query/from/TestCrossJoin.java b/test/adql/query/from/TestCrossJoin.java new file mode 100644 index 0000000..ce440bc --- /dev/null +++ b/test/adql/query/from/TestCrossJoin.java @@ -0,0 +1,95 @@ +package adql.query.from; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +import java.util.List; + +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; + +import adql.db.DBColumn; +import adql.db.DBType; +import adql.db.DBType.DBDatatype; +import adql.db.DefaultDBColumn; +import adql.db.DefaultDBTable; +import adql.db.SearchColumnList; +import adql.query.IdentifierField; + +public class TestCrossJoin { + + private ADQLTable tableA, tableB; + + @AfterClass + public static void tearDownAfterClass() throws Exception{} + + @Before + public void setUp() throws Exception{ + /* SET THE TABLES AND COLUMNS NEEDED FOR THE TEST */ + // Describe the available table: + DefaultDBTable metaTableA = new DefaultDBTable("A"); + metaTableA.setADQLSchemaName("public"); + DefaultDBTable metaTableB = new DefaultDBTable("B"); + metaTableB.setADQLSchemaName("public"); + + // Describe its columns: + metaTableA.addColumn(new DefaultDBColumn("id", new DBType(DBDatatype.VARCHAR), metaTableA)); + metaTableA.addColumn(new DefaultDBColumn("txta", new DBType(DBDatatype.VARCHAR), metaTableA)); + metaTableB.addColumn(new DefaultDBColumn("id", new DBType(DBDatatype.VARCHAR), metaTableB)); + metaTableB.addColumn(new DefaultDBColumn("txtb", new DBType(DBDatatype.VARCHAR), metaTableB)); + + // Build the ADQL tables: + tableA = new ADQLTable("A"); + tableA.setDBLink(metaTableA); + tableB = new ADQLTable("B"); + tableB.setDBLink(metaTableB); + } + + @Test + public void testGetDBColumns(){ + try{ + ADQLJoin join = new CrossJoin(tableA, tableB); + SearchColumnList joinColumns = join.getDBColumns(); + assertEquals(4, joinColumns.size()); + + // check column A.id and B.id + List lstFound = joinColumns.search(null, null, null, "id", IdentifierField.getFullCaseSensitive(true)); + assertEquals(2, lstFound.size()); + // A.id + assertNotNull(lstFound.get(0).getTable()); + assertEquals("A", lstFound.get(0).getTable().getADQLName()); + assertEquals("public", lstFound.get(0).getTable().getADQLSchemaName()); + assertEquals(1, joinColumns.search(null, "public", "A", "id", IdentifierField.getFullCaseSensitive(true)).size()); + // B.id + assertNotNull(lstFound.get(1).getTable()); + assertEquals("B", lstFound.get(1).getTable().getADQLName()); + assertEquals("public", lstFound.get(1).getTable().getADQLSchemaName()); + assertEquals(1, joinColumns.search(null, "public", "B", "id", IdentifierField.getFullCaseSensitive(true)).size()); + assertEquals(0, joinColumns.search(null, "public", "C", "id", IdentifierField.getFullCaseSensitive(true)).size()); + + // check column A.txta + lstFound = joinColumns.search(null, null, null, "txta", IdentifierField.getFullCaseSensitive(true)); + assertEquals(1, lstFound.size()); + assertNotNull(lstFound.get(0).getTable()); + assertEquals("A", lstFound.get(0).getTable().getADQLName()); + assertEquals("public", lstFound.get(0).getTable().getADQLSchemaName()); + assertEquals(1, joinColumns.search(null, "public", "A", "txta", IdentifierField.getFullCaseSensitive(true)).size()); + assertEquals(0, joinColumns.search(null, "public", "B", "txta", IdentifierField.getFullCaseSensitive(true)).size()); + + // check column B.txtb + lstFound = joinColumns.search(null, null, null, "txtb", IdentifierField.getFullCaseSensitive(true)); + assertEquals(1, lstFound.size()); + assertNotNull(lstFound.get(0).getTable()); + assertEquals("B", lstFound.get(0).getTable().getADQLName()); + assertEquals("public", lstFound.get(0).getTable().getADQLSchemaName()); + assertEquals(1, joinColumns.search(null, "public", "B", "txtb", IdentifierField.getFullCaseSensitive(true)).size()); + assertEquals(0, joinColumns.search(null, "public", "A", "txtb", IdentifierField.getFullCaseSensitive(true)).size()); + + }catch(Exception ex){ + ex.printStackTrace(); + fail("This test should have succeeded!"); + } + } +} diff --git a/test/adql/query/from/TestInnerJoin.java b/test/adql/query/from/TestInnerJoin.java new file mode 100644 index 0000000..57c4f87 --- /dev/null +++ b/test/adql/query/from/TestInnerJoin.java @@ -0,0 +1,158 @@ +package adql.query.from; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +import adql.db.DBColumn; +import adql.db.DBCommonColumn; +import adql.db.DBType; +import adql.db.DBType.DBDatatype; +import adql.db.DefaultDBColumn; +import adql.db.DefaultDBTable; +import adql.db.SearchColumnList; +import adql.query.IdentifierField; +import adql.query.operand.ADQLColumn; + +public class TestInnerJoin { + + private ADQLTable tableA, tableB, tableC; + + @Before + public void setUp() throws Exception{ + /* SET THE TABLES AND COLUMNS NEEDED FOR THE TEST */ + // Describe the available table: + DefaultDBTable metaTableA = new DefaultDBTable("A"); + metaTableA.setADQLSchemaName("public"); + DefaultDBTable metaTableB = new DefaultDBTable("B"); + metaTableB.setADQLSchemaName("public"); + DefaultDBTable metaTableC = new DefaultDBTable("C"); + metaTableC.setADQLSchemaName("public"); + + // Describe its columns: + metaTableA.addColumn(new DefaultDBColumn("id", new DBType(DBDatatype.VARCHAR), metaTableA)); + metaTableA.addColumn(new DefaultDBColumn("txta", new DBType(DBDatatype.VARCHAR), metaTableA)); + metaTableB.addColumn(new DefaultDBColumn("id", new DBType(DBDatatype.VARCHAR), metaTableB)); + metaTableB.addColumn(new DefaultDBColumn("txtb", new DBType(DBDatatype.VARCHAR), metaTableB)); + metaTableC.addColumn(new DefaultDBColumn("Id", new DBType(DBDatatype.VARCHAR), metaTableC)); + metaTableC.addColumn(new DefaultDBColumn("txta", new DBType(DBDatatype.VARCHAR), metaTableC)); + metaTableC.addColumn(new DefaultDBColumn("txtc", new DBType(DBDatatype.VARCHAR), metaTableC)); + + // Build the ADQL tables: + tableA = new ADQLTable("A"); + tableA.setDBLink(metaTableA); + tableB = new ADQLTable("B"); + tableB.setDBLink(metaTableB); + tableC = new ADQLTable("C"); + tableC.setDBLink(metaTableC); + } + + @Test + public void testGetDBColumns(){ + // Test NATURAL JOIN 1: + try{ + ADQLJoin join = new InnerJoin(tableA, tableB); + SearchColumnList joinColumns = join.getDBColumns(); + assertEquals(3, joinColumns.size()); + List lstFound = joinColumns.search(null, null, null, "id", IdentifierField.getFullCaseSensitive(true)); + assertEquals(1, lstFound.size()); + assertEquals(DBCommonColumn.class, lstFound.get(0).getClass()); + assertEquals(1, joinColumns.search(null, "public", "A", "id", IdentifierField.getFullCaseSensitive(true)).size()); + assertEquals(1, joinColumns.search(null, "public", "B", "id", IdentifierField.getFullCaseSensitive(true)).size()); + assertEquals(0, joinColumns.search(null, "public", "C", "id", IdentifierField.getFullCaseSensitive(true)).size()); + lstFound = joinColumns.search(null, "public", "A", "txta", IdentifierField.getFullCaseSensitive(true)); + assertEquals(1, lstFound.size()); + lstFound = joinColumns.search(null, "public", "B", "txtb", IdentifierField.getFullCaseSensitive(true)); + assertEquals(1, lstFound.size()); + }catch(Exception ex){ + ex.printStackTrace(); + fail("This test should have succeeded!"); + } + + // Test NATURAL JOIN 2: + try{ + ADQLJoin join = new InnerJoin(tableA, tableC); + SearchColumnList joinColumns = join.getDBColumns(); + assertEquals(3, joinColumns.size()); + + // check id (column common to table A and C only): + List lstFound = joinColumns.search(null, null, null, "id", IdentifierField.getFullCaseSensitive(true)); + assertEquals(1, lstFound.size()); + assertEquals(DBCommonColumn.class, lstFound.get(0).getClass()); + assertEquals(1, joinColumns.search(null, "public", "A", "id", IdentifierField.getFullCaseSensitive(true)).size()); + assertEquals(1, joinColumns.search(null, "public", "C", "id", IdentifierField.getFullCaseSensitive(true)).size()); + assertEquals(0, joinColumns.search(null, "public", "B", "id", IdentifierField.getFullCaseSensitive(true)).size()); + + // check txta (column common to table A and C only): + lstFound = joinColumns.search(null, null, null, "txta", IdentifierField.getFullCaseSensitive(true)); + assertEquals(1, lstFound.size()); + assertEquals(DBCommonColumn.class, lstFound.get(0).getClass()); + assertEquals(1, joinColumns.search(null, "public", "A", "txta", IdentifierField.getFullCaseSensitive(true)).size()); + assertEquals(1, joinColumns.search(null, "public", "C", "txta", IdentifierField.getFullCaseSensitive(true)).size()); + assertEquals(0, joinColumns.search(null, "public", "B", "id", IdentifierField.getFullCaseSensitive(true)).size()); + + // check txtc (only for table C) + lstFound = joinColumns.search(null, null, null, "txtc", IdentifierField.getFullCaseSensitive(true)); + assertEquals(1, lstFound.size()); + assertNotNull(lstFound.get(0).getTable()); + assertEquals("C", lstFound.get(0).getTable().getADQLName()); + assertEquals("public", lstFound.get(0).getTable().getADQLSchemaName()); + + }catch(Exception ex){ + ex.printStackTrace(); + fail("This test should have succeeded!"); + } + + // Test with a USING("id"): + try{ + List usingList = new ArrayList(1); + usingList.add(new ADQLColumn("id")); + ADQLJoin join = new InnerJoin(tableA, tableC, usingList); + SearchColumnList joinColumns = join.getDBColumns(); + assertEquals(4, joinColumns.size()); + + // check id (column common to table A and C only): + List lstFound = joinColumns.search(null, null, null, "id", IdentifierField.getFullCaseSensitive(true)); + assertEquals(1, lstFound.size()); + assertEquals(DBCommonColumn.class, lstFound.get(0).getClass()); + assertEquals(1, joinColumns.search(null, "public", "A", "id", IdentifierField.getFullCaseSensitive(true)).size()); + assertEquals(1, joinColumns.search(null, "public", "C", "id", IdentifierField.getFullCaseSensitive(true)).size()); + assertEquals(0, joinColumns.search(null, "public", "B", "id", IdentifierField.getFullCaseSensitive(true)).size()); + + // check A.txta and C.txta: + lstFound = joinColumns.search(null, null, null, "txta", IdentifierField.getFullCaseSensitive(true)); + assertEquals(2, lstFound.size()); + // A.txta + assertNotNull(lstFound.get(0).getTable()); + assertEquals("A", lstFound.get(0).getTable().getADQLName()); + assertEquals("public", lstFound.get(0).getTable().getADQLSchemaName()); + assertEquals(1, joinColumns.search(null, "public", "A", "txta", IdentifierField.getFullCaseSensitive(true)).size()); + // C.txta + assertNotNull(lstFound.get(1).getTable()); + assertEquals("C", lstFound.get(1).getTable().getADQLName()); + assertEquals("public", lstFound.get(1).getTable().getADQLSchemaName()); + assertEquals(1, joinColumns.search(null, "public", "C", "txta", IdentifierField.getFullCaseSensitive(true)).size()); + assertEquals(0, joinColumns.search(null, "public", "B", "txta", IdentifierField.getFullCaseSensitive(true)).size()); + + // check txtc (only for table C): + lstFound = joinColumns.search(null, null, null, "txtc", IdentifierField.getFullCaseSensitive(true)); + assertEquals(1, lstFound.size()); + assertNotNull(lstFound.get(0).getTable()); + assertEquals("C", lstFound.get(0).getTable().getADQLName()); + assertEquals("public", lstFound.get(0).getTable().getADQLSchemaName()); + assertEquals(1, joinColumns.search(null, "public", "C", "txtc", IdentifierField.getFullCaseSensitive(true)).size()); + assertEquals(0, joinColumns.search(null, "public", "A", "txtc", IdentifierField.getFullCaseSensitive(true)).size()); + + }catch(Exception ex){ + ex.printStackTrace(); + fail("This test should have succeeded!"); + } + } + +} diff --git a/test/adql/translator/TestPgSphereTranslator.java b/test/adql/translator/TestPgSphereTranslator.java new file mode 100644 index 0000000..2f34471 --- /dev/null +++ b/test/adql/translator/TestPgSphereTranslator.java @@ -0,0 +1,332 @@ +package adql.translator; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.sql.Types; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.postgresql.util.PGobject; + +import adql.db.DBType; +import adql.db.DBType.DBDatatype; +import adql.db.STCS.Region; +import adql.parser.ParseException; + +public class TestPgSphereTranslator { + + @BeforeClass + public static void setUpBeforeClass() throws Exception{} + + @AfterClass + public static void tearDownAfterClass() throws Exception{} + + @Before + public void setUp() throws Exception{} + + @After + public void tearDown() throws Exception{} + + @Test + public void testConvertTypeFromDB(){ + PgSphereTranslator translator = new PgSphereTranslator(); + + // POINT + DBType type = translator.convertTypeFromDB(Types.OTHER, "spoint", "spoint", null); + assertNotNull(type); + assertEquals(DBDatatype.POINT, type.type); + assertEquals(DBType.NO_LENGTH, type.length); + + // CIRCLE + type = translator.convertTypeFromDB(Types.OTHER, "scircle", "scircle", null); + assertNotNull(type); + assertEquals(DBDatatype.REGION, type.type); + assertEquals(DBType.NO_LENGTH, type.length); + + // BOX + type = translator.convertTypeFromDB(Types.OTHER, "sbox", "sbox", null); + assertNotNull(type); + assertEquals(DBDatatype.REGION, type.type); + assertEquals(DBType.NO_LENGTH, type.length); + + // POLYGON + type = translator.convertTypeFromDB(Types.OTHER, "spoly", "spoly", null); + assertNotNull(type); + assertEquals(DBDatatype.REGION, type.type); + assertEquals(DBType.NO_LENGTH, type.length); + } + + @Test + public void testConvertTypeToDB(){ + PgSphereTranslator translator = new PgSphereTranslator(); + + // NULL + assertEquals("VARCHAR", translator.convertTypeToDB(null)); + + // POINT + assertEquals("spoint", translator.convertTypeToDB(new DBType(DBDatatype.POINT))); + + // REGION (any other region is transformed into a polygon) + assertEquals("spoly", translator.convertTypeToDB(new DBType(DBDatatype.REGION))); + } + + @Test + public void testTranslateGeometryFromDB(){ + PgSphereTranslator translator = new PgSphereTranslator(); + PGobject pgo = new PGobject(); + + // NULL + try{ + assertNull(translator.translateGeometryFromDB(null)); + }catch(Throwable t){ + t.printStackTrace(); + fail(t.getMessage()); + } + + // SPOINT + try{ + pgo.setType("spoint"); + pgo.setValue("(0.1 , 0.2)"); + Region r = translator.translateGeometryFromDB(pgo); + assertEquals(5.72957, r.coordinates[0][0], 1e-5); + assertEquals(11.45915, r.coordinates[0][1], 1e-5); + + pgo.setValue("(5.72957d , 11.45915d)"); + r = translator.translateGeometryFromDB(pgo); + assertEquals(5.72957, r.coordinates[0][0], 1e-5); + assertEquals(11.45915, r.coordinates[0][1], 1e-5); + + pgo.setValue("( 5d 43m 46.480625s , +11d 27m 32.961249s)"); + r = translator.translateGeometryFromDB(pgo); + assertEquals(5.72957, r.coordinates[0][0], 1e-5); + assertEquals(11.45915, r.coordinates[0][1], 1e-5); + + pgo.setValue("( 0h 22m 55.098708s , +11d 27m 32.961249s)"); + r = translator.translateGeometryFromDB(pgo); + assertEquals(5.72957, r.coordinates[0][0], 1e-5); + assertEquals(11.45915, r.coordinates[0][1], 1e-5); + }catch(Throwable t){ + t.printStackTrace(); + fail(t.getMessage()); + } + + // SCIRCLE + try{ + pgo.setType("scircle"); + pgo.setValue("<(0.1,-0.2),1>"); + Region r = translator.translateGeometryFromDB(pgo); + assertEquals(5.72957, r.coordinates[0][0], 1e-5); + assertEquals(-11.45915, r.coordinates[0][1], 1e-5); + assertEquals(57.29577, r.radius, 1e-5); + + pgo.setValue("<(5.72957d , -11.45915d) , 57.29577d>"); + r = translator.translateGeometryFromDB(pgo); + assertEquals(5.72957, r.coordinates[0][0], 1e-5); + assertEquals(-11.45915, r.coordinates[0][1], 1e-5); + assertEquals(57.29577, r.radius, 1e-5); + + pgo.setValue("<( 5d 43m 46.452s , -11d 27m 32.94s) , 57d 17m 44.772s>"); + r = translator.translateGeometryFromDB(pgo); + assertEquals(5.72957, r.coordinates[0][0], 1e-5); + assertEquals(-11.45915, r.coordinates[0][1], 1e-5); + assertEquals(57.29577, r.radius, 1e-5); + + pgo.setValue("<( 0h 22m 55.0968s , -11d 27m 32.94s) , 57d 17m 44.772s>"); + r = translator.translateGeometryFromDB(pgo); + assertEquals(5.72957, r.coordinates[0][0], 1e-5); + assertEquals(-11.45915, r.coordinates[0][1], 1e-5); + assertEquals(57.29577, r.radius, 1e-5); + }catch(Throwable t){ + t.printStackTrace(); + fail(t.getMessage()); + } + + // SBOX + try{ + pgo.setType("sbox"); + pgo.setValue("((0.1,0.2),(0.5,0.5))"); + Region r = translator.translateGeometryFromDB(pgo); + assertEquals(17.18873, r.coordinates[0][0], 1e-5); + assertEquals(20.05352, r.coordinates[0][1], 1e-5); + assertEquals(22.91831, r.width, 1e-5); + assertEquals(17.18873, r.height, 1e-5); + + pgo.setValue("((5.72957795130823d , 11.4591559026165d), (28.6478897565412d , 28.6478897565412d))"); + r = translator.translateGeometryFromDB(pgo); + assertEquals(17.18873, r.coordinates[0][0], 1e-5); + assertEquals(20.05352, r.coordinates[0][1], 1e-5); + assertEquals(22.91831, r.width, 1e-5); + assertEquals(17.18873, r.height, 1e-5); + + pgo.setValue("(( 5d 43m 46.480625s , +11d 27m 32.961249s), ( 28d 38m 52.403124s , +28d 38m 52.403124s))"); + r = translator.translateGeometryFromDB(pgo); + assertEquals(17.18873, r.coordinates[0][0], 1e-5); + assertEquals(20.05352, r.coordinates[0][1], 1e-5); + assertEquals(22.91831, r.width, 1e-5); + assertEquals(17.18873, r.height, 1e-5); + + pgo.setValue("(( 0h 22m 55.098708s , +11d 27m 32.961249s), ( 1h 54m 35.493542s , +28d 38m 52.403124s))"); + r = translator.translateGeometryFromDB(pgo); + assertEquals(17.18873, r.coordinates[0][0], 1e-5); + assertEquals(20.05352, r.coordinates[0][1], 1e-5); + assertEquals(22.91831, r.width, 1e-5); + assertEquals(17.18873, r.height, 1e-5); + }catch(Throwable t){ + t.printStackTrace(); + fail(t.getMessage()); + } + + // SPOLY + try{ + pgo.setType("spoly"); + pgo.setValue("{(0.789761486527434 , 0.00436332312998582),(0.789761486527434 , 0.00872664625997165),(0.785398163397448 , 0.00872664625997165),(0.785398163397448 , 0.00436332312998582),(0.781034840267463 , 0.00436332312998582),(0.781034840267463 , 0),(0.785398163397448 , 0)}"); + Region r = translator.translateGeometryFromDB(pgo); + assertEquals(45.25, r.coordinates[0][0], 1e-2); + assertEquals(0.25, r.coordinates[0][1], 1e-2); + assertEquals(45.25, r.coordinates[1][0], 1e-2); + assertEquals(0.5, r.coordinates[1][1], 1e-2); + assertEquals(45, r.coordinates[2][0], 1e-2); + assertEquals(0.5, r.coordinates[2][1], 1e-2); + assertEquals(45, r.coordinates[3][0], 1e-2); + assertEquals(0.25, r.coordinates[3][1], 1e-2); + assertEquals(44.75, r.coordinates[4][0], 1e-2); + assertEquals(0.25, r.coordinates[4][1], 1e-2); + assertEquals(44.75, r.coordinates[5][0], 1e-2); + assertEquals(0, r.coordinates[5][1], 1e-2); + assertEquals(45, r.coordinates[6][0], 1e-2); + assertEquals(0, r.coordinates[6][1], 1e-2); + + pgo.setValue("{(45.25d , 0.25d), (45.25d , 0.5d), (45d , 0.5d), (45d , 0.25d), (44.75d , 0.25d), (44.75d , 0d), (45d , 0d)}"); + r = translator.translateGeometryFromDB(pgo); + assertEquals(45.25, r.coordinates[0][0], 1e-2); + assertEquals(0.25, r.coordinates[0][1], 1e-2); + assertEquals(45.25, r.coordinates[1][0], 1e-2); + assertEquals(0.5, r.coordinates[1][1], 1e-2); + assertEquals(45, r.coordinates[2][0], 1e-2); + assertEquals(0.5, r.coordinates[2][1], 1e-2); + assertEquals(45, r.coordinates[3][0], 1e-2); + assertEquals(0.25, r.coordinates[3][1], 1e-2); + assertEquals(44.75, r.coordinates[4][0], 1e-2); + assertEquals(0.25, r.coordinates[4][1], 1e-2); + assertEquals(44.75, r.coordinates[5][0], 1e-2); + assertEquals(0, r.coordinates[5][1], 1e-2); + assertEquals(45, r.coordinates[6][0], 1e-2); + assertEquals(0, r.coordinates[6][1], 1e-2); + + pgo.setValue("{( 45d 15m 0s , + 0d 15m 0s),( 45d 15m 0s , + 0d 30m 0s),( 45d 0m 0s , + 0d 30m 0s),( 45d 0m 0s , + 0d 15m 0s),( 44d 45m 0s , + 0d 15m 0s),( 44d 45m 0s , + 0d 0m 0s),( 45d 0m 0s , + 0d 0m 0s)}"); + r = translator.translateGeometryFromDB(pgo); + assertEquals(45.25, r.coordinates[0][0], 1e-2); + assertEquals(0.25, r.coordinates[0][1], 1e-2); + assertEquals(45.25, r.coordinates[1][0], 1e-2); + assertEquals(0.5, r.coordinates[1][1], 1e-2); + assertEquals(45, r.coordinates[2][0], 1e-2); + assertEquals(0.5, r.coordinates[2][1], 1e-2); + assertEquals(45, r.coordinates[3][0], 1e-2); + assertEquals(0.25, r.coordinates[3][1], 1e-2); + assertEquals(44.75, r.coordinates[4][0], 1e-2); + assertEquals(0.25, r.coordinates[4][1], 1e-2); + assertEquals(44.75, r.coordinates[5][0], 1e-2); + assertEquals(0, r.coordinates[5][1], 1e-2); + assertEquals(45, r.coordinates[6][0], 1e-2); + assertEquals(0, r.coordinates[6][1], 1e-2); + + pgo.setValue("{( 3h 1m 0s , + 0d 15m 0s),( 3h 1m 0s , + 0d 30m 0s),( 3h 0m 0s , + 0d 30m 0s),( 3h 0m 0s , + 0d 15m 0s),( 2h 59m 0s , + 0d 15m 0s),( 2h 59m 0s , + 0d 0m 0s),( 3h 0m 0s , + 0d 0m 0s)}"); + r = translator.translateGeometryFromDB(pgo); + assertEquals(45.25, r.coordinates[0][0], 1e-2); + assertEquals(0.25, r.coordinates[0][1], 1e-2); + assertEquals(45.25, r.coordinates[1][0], 1e-2); + assertEquals(0.5, r.coordinates[1][1], 1e-2); + assertEquals(45, r.coordinates[2][0], 1e-2); + assertEquals(0.5, r.coordinates[2][1], 1e-2); + assertEquals(45, r.coordinates[3][0], 1e-2); + assertEquals(0.25, r.coordinates[3][1], 1e-2); + assertEquals(44.75, r.coordinates[4][0], 1e-2); + assertEquals(0.25, r.coordinates[4][1], 1e-2); + assertEquals(44.75, r.coordinates[5][0], 1e-2); + assertEquals(0, r.coordinates[5][1], 1e-2); + assertEquals(45, r.coordinates[6][0], 1e-2); + assertEquals(0, r.coordinates[6][1], 1e-2); + }catch(Throwable t){ + t.printStackTrace(); + fail(t.getMessage()); + } + + // OTHER + try{ + translator.translateGeometryFromDB(new Double(12.3)); + fail("The translation of a Double as a geometry is not supported!"); + }catch(Throwable t){ + assertTrue(t instanceof ParseException); + assertEquals("Incompatible type! The column value \"12.3\" was supposed to be a geometrical object.", t.getMessage()); + } + try{ + pgo.setType("sline"); + pgo.setValue("( -90d, -20d, 200d, XYZ ), 30d "); + translator.translateGeometryFromDB(pgo); + fail("The translation of a sline is not supported!"); + }catch(Throwable t){ + assertTrue(t instanceof ParseException); + assertEquals("Unsupported PgSphere type: \"sline\"! Impossible to convert the column value \"( -90d, -20d, 200d, XYZ ), 30d \" into a Region.", t.getMessage()); + } + } + + @Test + public void testTranslateGeometryToDB(){ + PgSphereTranslator translator = new PgSphereTranslator(); + + try{ + // NULL + assertNull(translator.translateGeometryToDB(null)); + + // POSITION + Region r = new Region(null, new double[]{45,0}); + PGobject pgo = (PGobject)translator.translateGeometryToDB(r); + assertNotNull(pgo); + assertEquals("spoint", pgo.getType()); + assertEquals("(45.0d,0.0d)", pgo.getValue()); + + // CIRCLE + r = new Region(null, new double[]{45,0}, 1.2); + pgo = (PGobject)translator.translateGeometryToDB(r); + assertNotNull(pgo); + assertEquals("spoly", pgo.getType()); + assertEquals("{(46.2d,0.0d),(46.176942336483876d,0.2341083864193539d),(46.108655439013546d,0.4592201188381077d),(45.99776353476305d,0.6666842796235226d),(45.848528137423855d,0.8485281374238569d),(45.666684279623524d,0.9977635347630542d),(45.45922011883811d,1.1086554390135441d),(45.23410838641935d,1.1769423364838765d),(45.0d,1.2d),(44.76589161358065d,1.1769423364838765d),(44.54077988116189d,1.1086554390135441d),(44.333315720376476d,0.9977635347630543d),(44.151471862576145d,0.848528137423857d),(44.00223646523695d,0.6666842796235226d),(43.891344560986454d,0.4592201188381073d),(43.823057663516124d,0.23410838641935325d),(43.8d,-9.188564877424678E-16d),(43.823057663516124d,-0.23410838641935505d),(43.891344560986454d,-0.45922011883810904d),(44.00223646523695d,-0.6666842796235241d),(44.151471862576145d,-0.8485281374238584d),(44.333315720376476d,-0.9977635347630555d),(44.540779881161896d,-1.108655439013545d),(44.76589161358065d,-1.176942336483877d),(45.0d,-1.2d),(45.23410838641936d,-1.1769423364838758d),(45.45922011883811d,-1.1086554390135428d),(45.666684279623524d,-0.9977635347630521d),(45.84852813742386d,-0.8485281374238541d),(45.99776353476306d,-0.6666842796235192d),(46.108655439013546d,-0.45922011883810354d),(46.176942336483876d,-0.23410838641934922d)}", pgo.getValue()); + + // BOX + r = new Region(null, new double[]{45,0}, 1.2, 5); + pgo = (PGobject)translator.translateGeometryToDB(r); + assertNotNull(pgo); + assertEquals("spoly", pgo.getType()); + assertEquals("{(44.4d,-2.5d),(44.4d,2.5d),(45.6d,2.5d),(45.6d,-2.5d)}", pgo.getValue()); + + // POLYGON + r = new Region(null, new double[][]{new double[]{45.25,0.25},new double[]{45.25,0.5},new double[]{45,0.5},new double[]{45,0.25},new double[]{44.75,0.25},new double[]{44.75,0},new double[]{45,0}}); + pgo = (PGobject)translator.translateGeometryToDB(r); + assertNotNull(pgo); + assertEquals("spoly", pgo.getType()); + assertEquals("{(45.25d,0.25d),(45.25d,0.5d),(45.0d,0.5d),(45.0d,0.25d),(44.75d,0.25d),(44.75d,0.0d),(45.0d,0.0d)}", pgo.getValue()); + + // OTHER + try{ + r = new Region(new Region(null, new double[]{45,0})); + translator.translateGeometryToDB(r); + fail("The translation of a STC Not region is not supported!"); + }catch(Throwable ex){ + assertTrue(ex instanceof ParseException); + assertEquals("Unsupported geometrical region: \"" + r.type + "\"!", ex.getMessage()); + } + + }catch(ParseException t){ + t.printStackTrace(); + fail(t.getMessage()); + } + } + +} diff --git a/test/tap/config/AllTAPConfigTests.java b/test/tap/config/AllTAPConfigTests.java new file mode 100644 index 0000000..c752a88 --- /dev/null +++ b/test/tap/config/AllTAPConfigTests.java @@ -0,0 +1,29 @@ +package tap.config; + +import java.util.Properties; + +import org.junit.runner.RunWith; +import org.junit.runners.Suite; +import org.junit.runners.Suite.SuiteClasses; + +import tap.parameters.TestMaxRecController; + +@RunWith(Suite.class) +@SuiteClasses({TestTAPConfiguration.class,TestConfigurableServiceConnection.class,TestConfigurableTAPFactory.class,TestMaxRecController.class}) +public class AllTAPConfigTests { + + public final static Properties getValidProperties(){ + Properties validProp = new Properties(); + validProp.setProperty("database_access", "jdbc"); + validProp.setProperty("jdbc_url", "jdbc:postgresql:gmantele"); + validProp.setProperty("jdbc_driver", "org.postgresql.Driver"); + validProp.setProperty("db_username", "gmantele"); + validProp.setProperty("db_password", "pwd"); + validProp.setProperty("sql_translator", "postgres"); + validProp.setProperty("metadata", "db"); + validProp.setProperty("file_manager", "local"); + validProp.setProperty("file_root_path", "bin/ext/test/tap"); + return validProp; + } + +} diff --git a/test/tap/config/TestConfigurableServiceConnection.java b/test/tap/config/TestConfigurableServiceConnection.java new file mode 100644 index 0000000..f2058cf --- /dev/null +++ b/test/tap/config/TestConfigurableServiceConnection.java @@ -0,0 +1,1200 @@ +package tap.config; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static tap.config.TAPConfiguration.DEFAULT_ASYNC_FETCH_SIZE; +import static tap.config.TAPConfiguration.DEFAULT_MAX_ASYNC_JOBS; +import static tap.config.TAPConfiguration.DEFAULT_SYNC_FETCH_SIZE; +import static tap.config.TAPConfiguration.KEY_ASYNC_FETCH_SIZE; +import static tap.config.TAPConfiguration.KEY_COORD_SYS; +import static tap.config.TAPConfiguration.KEY_DEFAULT_OUTPUT_LIMIT; +import static tap.config.TAPConfiguration.KEY_FILE_MANAGER; +import static tap.config.TAPConfiguration.KEY_GEOMETRIES; +import static tap.config.TAPConfiguration.KEY_LOG_ROTATION; +import static tap.config.TAPConfiguration.KEY_MAX_ASYNC_JOBS; +import static tap.config.TAPConfiguration.KEY_MAX_OUTPUT_LIMIT; +import static tap.config.TAPConfiguration.KEY_METADATA; +import static tap.config.TAPConfiguration.KEY_METADATA_FILE; +import static tap.config.TAPConfiguration.KEY_MIN_LOG_LEVEL; +import static tap.config.TAPConfiguration.KEY_OUTPUT_FORMATS; +import static tap.config.TAPConfiguration.KEY_SYNC_FETCH_SIZE; +import static tap.config.TAPConfiguration.KEY_TAP_FACTORY; +import static tap.config.TAPConfiguration.KEY_UDFS; +import static tap.config.TAPConfiguration.KEY_USER_IDENTIFIER; +import static tap.config.TAPConfiguration.VALUE_ANY; +import static tap.config.TAPConfiguration.VALUE_CSV; +import static tap.config.TAPConfiguration.VALUE_DB; +import static tap.config.TAPConfiguration.VALUE_FITS; +import static tap.config.TAPConfiguration.VALUE_JSON; +import static tap.config.TAPConfiguration.VALUE_LOCAL; +import static tap.config.TAPConfiguration.VALUE_NONE; +import static tap.config.TAPConfiguration.VALUE_SV; +import static tap.config.TAPConfiguration.VALUE_TEXT; +import static tap.config.TAPConfiguration.VALUE_TSV; +import static tap.config.TAPConfiguration.VALUE_VOTABLE; +import static tap.config.TAPConfiguration.VALUE_XML; + +import java.io.File; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.Map; +import java.util.Properties; + +import javax.servlet.http.HttpServletRequest; + +import org.junit.BeforeClass; +import org.junit.Test; + +import tap.AbstractTAPFactory; +import tap.ServiceConnection; +import tap.ServiceConnection.LimitUnit; +import tap.TAPException; +import tap.db.DBConnection; +import tap.db.DBException; +import tap.db.JDBCConnection; +import tap.formatter.OutputFormat; +import tap.formatter.VOTableFormat; +import uk.ac.starlink.votable.DataFormat; +import uk.ac.starlink.votable.VOTableVersion; +import uws.UWSException; +import uws.job.user.DefaultJobOwner; +import uws.job.user.JobOwner; +import uws.service.UWSUrl; +import uws.service.UserIdentifier; +import uws.service.file.LocalUWSFileManager; +import uws.service.log.DefaultUWSLog; +import uws.service.log.UWSLog.LogLevel; +import adql.db.FunctionDef; +import adql.db.STCS.Flavor; +import adql.db.STCS.Frame; +import adql.db.STCS.RefPos; +import adql.db.TestDBChecker.UDFToto; +import adql.translator.PostgreSQLTranslator; + +public class TestConfigurableServiceConnection { + + private final static String XML_FILE = "test/tap/config/tables.xml"; + + private static Properties validProp, noFmProp, fmClassNameProp, + incorrectFmProp, correctLogProp, incorrectLogLevelProp, + incorrectLogRotationProp, xmlMetaProp, wrongManualMetaProp, + missingMetaProp, missingMetaFileProp, wrongMetaProp, + wrongMetaFileProp, validFormatsProp, validVOTableFormatsProp, + badSVFormat1Prop, badSVFormat2Prop, badVotFormat1Prop, + badVotFormat2Prop, badVotFormat3Prop, badVotFormat4Prop, + badVotFormat5Prop, badVotFormat6Prop, unknownFormatProp, + maxAsyncProp, negativeMaxAsyncProp, notIntMaxAsyncProp, + defaultOutputLimitProp, maxOutputLimitProp, + bothOutputLimitGoodProp, bothOutputLimitBadProp, syncFetchSizeProp, + notIntSyncFetchSizeProp, negativeSyncFetchSizeProp, + notIntAsyncFetchSizeProp, negativeAsyncFetchSizeProp, + asyncFetchSizeProp, userIdentProp, notClassPathUserIdentProp, + coordSysProp, noneCoordSysProp, anyCoordSysProp, + noneInsideCoordSysProp, unknownCoordSysProp, geometriesProp, + noneGeomProp, anyGeomProp, noneInsideGeomProp, unknownGeomProp, + anyUdfsProp, noneUdfsProp, udfsProp, udfsWithClassNameProp, + udfsListWithNONEorANYProp, udfsWithWrongParamLengthProp, + udfsWithMissingBracketsProp, udfsWithMissingDefProp1, + udfsWithMissingDefProp2, emptyUdfItemProp1, emptyUdfItemProp2, + udfWithMissingEndBracketProp, customFactoryProp, + badCustomFactoryProp; + + @BeforeClass + public static void setUp() throws Exception{ + // LOAD ALL PROPERTIES FILES NEEDED FOR ALL THE TESTS: + validProp = AllTAPConfigTests.getValidProperties(); + + noFmProp = (Properties)validProp.clone(); + noFmProp.setProperty(KEY_FILE_MANAGER, ""); + + fmClassNameProp = (Properties)validProp.clone(); + fmClassNameProp.setProperty(KEY_FILE_MANAGER, "{tap.config.TestConfigurableServiceConnection$FileManagerTest}"); + + incorrectFmProp = (Properties)validProp.clone(); + incorrectFmProp.setProperty(KEY_FILE_MANAGER, "foo"); + + correctLogProp = (Properties)validProp.clone(); + correctLogProp.setProperty(KEY_LOG_ROTATION, " M 5 6 03 "); + correctLogProp.setProperty(KEY_MIN_LOG_LEVEL, " WARNing "); + + incorrectLogLevelProp = (Properties)validProp.clone(); + incorrectLogLevelProp.setProperty(KEY_MIN_LOG_LEVEL, "foo"); + + incorrectLogRotationProp = (Properties)validProp.clone(); + incorrectLogRotationProp.setProperty(KEY_LOG_ROTATION, "foo"); + + xmlMetaProp = (Properties)validProp.clone(); + xmlMetaProp.setProperty(KEY_METADATA, VALUE_XML); + xmlMetaProp.setProperty(KEY_METADATA_FILE, XML_FILE); + + wrongManualMetaProp = (Properties)validProp.clone(); + wrongManualMetaProp.setProperty(KEY_METADATA, "{tap.metadata.TAPMetadata}"); + + missingMetaProp = (Properties)validProp.clone(); + missingMetaProp.remove(KEY_METADATA); + + wrongMetaProp = (Properties)validProp.clone(); + wrongMetaProp.setProperty(KEY_METADATA, "foo"); + + wrongMetaFileProp = (Properties)validProp.clone(); + wrongMetaFileProp.setProperty(KEY_METADATA, VALUE_XML); + wrongMetaFileProp.setProperty(KEY_METADATA_FILE, "foo"); + + missingMetaFileProp = (Properties)validProp.clone(); + missingMetaFileProp.setProperty(KEY_METADATA, VALUE_XML); + missingMetaFileProp.remove(KEY_METADATA_FILE); + + validFormatsProp = (Properties)validProp.clone(); + validFormatsProp.setProperty(KEY_OUTPUT_FORMATS, VALUE_FITS + "," + VALUE_TEXT + "," + VALUE_JSON + "," + VALUE_CSV + " , " + VALUE_TSV + ",, , " + VALUE_SV + "([])" + ", " + VALUE_SV + "(|):text/psv:psv" + ", " + VALUE_SV + "($)::test" + ", \t " + VALUE_SV + "(@):text/arobase:" + ", {tap.formatter.HTMLFormat}"); + + validVOTableFormatsProp = (Properties)validProp.clone(); + validVOTableFormatsProp.setProperty(KEY_OUTPUT_FORMATS, "votable, votable()::, vot(), vot::, votable:, votable(Td, 1.0), vot(TableData), votable(,1.2), vot(Fits):application/fits:supervot"); + + badSVFormat1Prop = (Properties)validProp.clone(); + badSVFormat1Prop.setProperty(KEY_OUTPUT_FORMATS, VALUE_SV); + + badSVFormat2Prop = (Properties)validProp.clone(); + badSVFormat2Prop.setProperty(KEY_OUTPUT_FORMATS, VALUE_SV + "()"); + + badVotFormat1Prop = (Properties)validProp.clone(); + badVotFormat1Prop.setProperty(KEY_OUTPUT_FORMATS, "votable(foo)"); + + badVotFormat2Prop = (Properties)validProp.clone(); + badVotFormat2Prop.setProperty(KEY_OUTPUT_FORMATS, "vot(,foo)"); + + badVotFormat3Prop = (Properties)validProp.clone(); + badVotFormat3Prop.setProperty(KEY_OUTPUT_FORMATS, "text, vot(TD"); + + badVotFormat4Prop = (Properties)validProp.clone(); + badVotFormat4Prop.setProperty(KEY_OUTPUT_FORMATS, "vot(TD, text"); + + badVotFormat5Prop = (Properties)validProp.clone(); + badVotFormat5Prop.setProperty(KEY_OUTPUT_FORMATS, "vot(TD, 1.0, foo)"); + + badVotFormat6Prop = (Properties)validProp.clone(); + badVotFormat6Prop.setProperty(KEY_OUTPUT_FORMATS, "vot:application/xml:votable:foo"); + + unknownFormatProp = (Properties)validProp.clone(); + unknownFormatProp.setProperty(KEY_OUTPUT_FORMATS, "foo"); + + maxAsyncProp = (Properties)validProp.clone(); + maxAsyncProp.setProperty(KEY_MAX_ASYNC_JOBS, "10"); + + negativeMaxAsyncProp = (Properties)validProp.clone(); + negativeMaxAsyncProp.setProperty(KEY_MAX_ASYNC_JOBS, "-2"); + + notIntMaxAsyncProp = (Properties)validProp.clone(); + notIntMaxAsyncProp.setProperty(KEY_MAX_ASYNC_JOBS, "foo"); + + defaultOutputLimitProp = (Properties)validProp.clone(); + defaultOutputLimitProp.setProperty(KEY_DEFAULT_OUTPUT_LIMIT, "100"); + + maxOutputLimitProp = (Properties)validProp.clone(); + maxOutputLimitProp.setProperty(KEY_MAX_OUTPUT_LIMIT, "1000R"); + + bothOutputLimitGoodProp = (Properties)validProp.clone(); + bothOutputLimitGoodProp.setProperty(KEY_DEFAULT_OUTPUT_LIMIT, "100R"); + bothOutputLimitGoodProp.setProperty(KEY_MAX_OUTPUT_LIMIT, "1000"); + + bothOutputLimitBadProp = (Properties)validProp.clone(); + bothOutputLimitBadProp.setProperty(KEY_DEFAULT_OUTPUT_LIMIT, "1000"); + bothOutputLimitBadProp.setProperty(KEY_MAX_OUTPUT_LIMIT, "100"); + + syncFetchSizeProp = (Properties)validProp.clone(); + syncFetchSizeProp.setProperty(KEY_SYNC_FETCH_SIZE, "50"); + + notIntSyncFetchSizeProp = (Properties)validProp.clone(); + notIntSyncFetchSizeProp.setProperty(KEY_SYNC_FETCH_SIZE, "foo"); + + negativeSyncFetchSizeProp = (Properties)validProp.clone(); + negativeSyncFetchSizeProp.setProperty(KEY_SYNC_FETCH_SIZE, "-3"); + + asyncFetchSizeProp = (Properties)validProp.clone(); + asyncFetchSizeProp.setProperty(KEY_ASYNC_FETCH_SIZE, "50"); + + notIntAsyncFetchSizeProp = (Properties)validProp.clone(); + notIntAsyncFetchSizeProp.setProperty(KEY_ASYNC_FETCH_SIZE, "foo"); + + negativeAsyncFetchSizeProp = (Properties)validProp.clone(); + negativeAsyncFetchSizeProp.setProperty(KEY_ASYNC_FETCH_SIZE, "-3"); + + userIdentProp = (Properties)validProp.clone(); + userIdentProp.setProperty(KEY_USER_IDENTIFIER, "{tap.config.TestConfigurableServiceConnection$UserIdentifierTest}"); + + notClassPathUserIdentProp = (Properties)validProp.clone(); + notClassPathUserIdentProp.setProperty(KEY_USER_IDENTIFIER, "foo"); + + coordSysProp = (Properties)validProp.clone(); + coordSysProp.setProperty(KEY_COORD_SYS, "icrs * *, ICrs * (Spherical2| CARTEsian2)"); + + noneCoordSysProp = (Properties)validProp.clone(); + noneCoordSysProp.setProperty(KEY_COORD_SYS, VALUE_NONE); + + anyCoordSysProp = (Properties)validProp.clone(); + anyCoordSysProp.setProperty(KEY_COORD_SYS, VALUE_ANY); + + noneInsideCoordSysProp = (Properties)validProp.clone(); + noneInsideCoordSysProp.setProperty(KEY_COORD_SYS, " ICRS * *, none, FK4 (GEOCENTER|heliocenter) *"); + + unknownCoordSysProp = (Properties)validProp.clone(); + unknownCoordSysProp.setProperty(KEY_COORD_SYS, "ICRS foo *"); + + geometriesProp = (Properties)validProp.clone(); + geometriesProp.setProperty(KEY_GEOMETRIES, "point, CIRCle , cONTAins,intersECTS"); + + noneGeomProp = (Properties)validProp.clone(); + noneGeomProp.setProperty(KEY_GEOMETRIES, VALUE_NONE); + + anyGeomProp = (Properties)validProp.clone(); + anyGeomProp.setProperty(KEY_GEOMETRIES, VALUE_ANY); + + noneInsideGeomProp = (Properties)validProp.clone(); + noneInsideGeomProp.setProperty(KEY_GEOMETRIES, "POINT, Box, none, circle"); + + unknownGeomProp = (Properties)validProp.clone(); + unknownGeomProp.setProperty(KEY_GEOMETRIES, "POINT, Contains, foo, circle,Polygon"); + + anyUdfsProp = (Properties)validProp.clone(); + anyUdfsProp.setProperty(KEY_UDFS, VALUE_ANY); + + noneUdfsProp = (Properties)validProp.clone(); + noneUdfsProp.setProperty(KEY_UDFS, VALUE_NONE); + + udfsProp = (Properties)validProp.clone(); + udfsProp.setProperty(KEY_UDFS, "[toto(a string)] , [ titi(b REAL) -> double ]"); + + udfsWithClassNameProp = (Properties)validProp.clone(); + udfsWithClassNameProp.setProperty(KEY_UDFS, "[toto(a string)->VARCHAR, {adql.db.TestDBChecker$UDFToto}]"); + + udfsListWithNONEorANYProp = (Properties)validProp.clone(); + udfsListWithNONEorANYProp.setProperty(KEY_UDFS, "[toto(a string)->VARCHAR],ANY"); + + udfsWithWrongParamLengthProp = (Properties)validProp.clone(); + udfsWithWrongParamLengthProp.setProperty(KEY_UDFS, "[toto(a string)->VARCHAR, {adql.db.TestDBChecker$UDFToto}, foo]"); + + udfsWithMissingBracketsProp = (Properties)validProp.clone(); + udfsWithMissingBracketsProp.setProperty(KEY_UDFS, "toto(a string)->VARCHAR"); + + udfsWithMissingDefProp1 = (Properties)validProp.clone(); + udfsWithMissingDefProp1.setProperty(KEY_UDFS, "[{adql.db.TestDBChecker$UDFToto}]"); + + udfsWithMissingDefProp2 = (Properties)validProp.clone(); + udfsWithMissingDefProp2.setProperty(KEY_UDFS, "[,{adql.db.TestDBChecker$UDFToto}]"); + + emptyUdfItemProp1 = (Properties)validProp.clone(); + emptyUdfItemProp1.setProperty(KEY_UDFS, "[ ]"); + + emptyUdfItemProp2 = (Properties)validProp.clone(); + emptyUdfItemProp2.setProperty(KEY_UDFS, "[ , ]"); + + udfWithMissingEndBracketProp = (Properties)validProp.clone(); + udfWithMissingEndBracketProp.setProperty(KEY_UDFS, "[toto(a string)->VARCHAR"); + + customFactoryProp = (Properties)validProp.clone(); + customFactoryProp.setProperty(KEY_TAP_FACTORY, "{tap.config.TestConfigurableServiceConnection$CustomTAPFactory}"); + + badCustomFactoryProp = (Properties)validProp.clone(); + badCustomFactoryProp.setProperty(KEY_TAP_FACTORY, "{tap.config.TestConfigurableServiceConnection$BadCustomTAPFactory}"); + } + + /** + * CONSTRUCTOR TESTS + * * In general: + * - A valid configuration file builds successfully a fully functional ServiceConnection object. + * + * * Over the file manager: + * - If no TAPFileManager is provided, an exception must be thrown. + * - If a class name toward a valid TAPFileManager is provided, a functional DefaultServiceConnection must be successfully built. + * - An incorrect file manager value in the configuration file must generate an exception. + * + * * Over the output format: + * - If a SV format is badly expressed (test with "sv" and "sv()"), an exception must be thrown. + * - If an unknown output format is provided an exception must be thrown. + * + * Note: the good configuration of the TAPFactory built by the DefaultServiceConnection is tested in {@link TestConfigurableTAPFactory}. + * + * @see ConfigurableServiceConnection#DefaultServiceConnection(Properties) + */ + @Test + public void testDefaultServiceConnectionProperties(){ + // Valid Configuration File: + PrintWriter writer = null; + int nbSchemas = -1, nbTables = -1; + try{ + // build the ServiceConnection: + ServiceConnection connection = new ConfigurableServiceConnection(validProp); + + // tests: + assertNotNull(connection.getLogger()); + assertEquals(LogLevel.DEBUG, ((DefaultUWSLog)connection.getLogger()).getMinLogLevel()); + assertNotNull(connection.getFileManager()); + assertEquals("daily at 00:00", ((LocalUWSFileManager)connection.getFileManager()).getLogRotationFreq()); + assertNotNull(connection.getFactory()); + assertNotNull(connection.getTAPMetadata()); + assertTrue(connection.getTAPMetadata().getNbSchemas() >= 1); + assertTrue(connection.getTAPMetadata().getNbTables() >= 5); + assertFalse(connection.isAvailable()); + assertEquals(DEFAULT_MAX_ASYNC_JOBS, connection.getNbMaxAsyncJobs()); + assertTrue(connection.getRetentionPeriod()[0] <= connection.getRetentionPeriod()[1]); + assertTrue(connection.getExecutionDuration()[0] <= connection.getExecutionDuration()[1]); + assertNull(connection.getUserIdentifier()); + assertNull(connection.getGeometries()); + assertEquals(0, connection.getUDFs().size()); + assertNotNull(connection.getFetchSize()); + assertEquals(2, connection.getFetchSize().length); + assertEquals(DEFAULT_ASYNC_FETCH_SIZE, connection.getFetchSize()[0]); + assertEquals(DEFAULT_SYNC_FETCH_SIZE, connection.getFetchSize()[1]); + + // finally, save metadata in an XML file for the other tests: + writer = new PrintWriter(new File(XML_FILE)); + connection.getTAPMetadata().write(writer); + nbSchemas = connection.getTAPMetadata().getNbSchemas(); + nbTables = connection.getTAPMetadata().getNbTables(); + + }catch(Exception e){ + fail("This MUST have succeeded because the property file is valid! \nCaught exception: " + getPertinentMessage(e)); + }finally{ + if (writer != null) + writer.close(); + } + + // Valid XML metadata: + try{ + ServiceConnection connection = new ConfigurableServiceConnection(xmlMetaProp); + assertNotNull(connection.getLogger()); + assertEquals(LogLevel.DEBUG, ((DefaultUWSLog)connection.getLogger()).getMinLogLevel()); + assertNotNull(connection.getFileManager()); + assertEquals("daily at 00:00", ((LocalUWSFileManager)connection.getFileManager()).getLogRotationFreq()); + assertNotNull(connection.getFactory()); + assertNotNull(connection.getTAPMetadata()); + assertEquals(nbSchemas, connection.getTAPMetadata().getNbSchemas()); + assertEquals(nbTables, connection.getTAPMetadata().getNbTables()); + assertFalse(connection.isAvailable()); + assertEquals(DEFAULT_MAX_ASYNC_JOBS, connection.getNbMaxAsyncJobs()); + assertTrue(connection.getRetentionPeriod()[0] <= connection.getRetentionPeriod()[1]); + assertTrue(connection.getExecutionDuration()[0] <= connection.getExecutionDuration()[1]); + assertNull(connection.getUserIdentifier()); + assertNull(connection.getGeometries()); + assertEquals(0, connection.getUDFs().size()); + assertNotNull(connection.getFetchSize()); + assertEquals(2, connection.getFetchSize().length); + assertEquals(DEFAULT_ASYNC_FETCH_SIZE, connection.getFetchSize()[0]); + assertEquals(DEFAULT_SYNC_FETCH_SIZE, connection.getFetchSize()[1]); + }catch(Exception e){ + fail("This MUST have succeeded because the property file is valid! \nCaught exception: " + getPertinentMessage(e)); + } + + // Missing metadata property: + try{ + new ConfigurableServiceConnection(missingMetaProp); + fail("This MUST have failed because the property 'metadata' is missing!"); + }catch(Exception e){ + assertEquals(TAPException.class, e.getClass()); + assertEquals("The property \"" + KEY_METADATA + "\" is missing! It is required to create a TAP Service. Three possible values: " + VALUE_XML + " (to get metadata from a TableSet XML document), " + VALUE_DB + " (to fetch metadata from the database schema TAP_SCHEMA) or the name (between {}) of a class extending TAPMetadata.", e.getMessage()); + } + + // Missing metadata_file property: + try{ + new ConfigurableServiceConnection(missingMetaFileProp); + fail("This MUST have failed because the property 'metadata_file' is missing!"); + }catch(Exception e){ + assertEquals(TAPException.class, e.getClass()); + assertEquals("The property \"" + KEY_METADATA_FILE + "\" is missing! According to the property \"" + KEY_METADATA + "\", metadata must be fetched from an XML document. The local file path of it MUST be provided using the property \"" + KEY_METADATA_FILE + "\".", e.getMessage()); + } + + // Wrong metadata property: + try{ + new ConfigurableServiceConnection(wrongMetaProp); + fail("This MUST have failed because the property 'metadata' has a wrong value!"); + }catch(Exception e){ + assertEquals(TAPException.class, e.getClass()); + assertEquals("Unsupported value for the property \"" + KEY_METADATA + "\": \"foo\"! Only two values are allowed: " + VALUE_XML + " (to get metadata from a TableSet XML document) or " + VALUE_DB + " (to fetch metadata from the database schema TAP_SCHEMA).", e.getMessage()); + } + + // Wrong MANUAL metadata: + try{ + new ConfigurableServiceConnection(wrongManualMetaProp); + fail("This MUST have failed because the class specified in the property 'metadata' does not extend TAPMetadata but is TAPMetadata!"); + }catch(Exception e){ + assertEquals(TAPException.class, e.getClass()); + assertEquals("Wrong class for the property \"" + KEY_METADATA + "\": \"tap.metadata.TAPMetadata\"! The class provided in this property MUST EXTEND tap.metadata.TAPMetadata.", e.getMessage()); + } + + // Wrong metadata_file property: + try{ + new ConfigurableServiceConnection(wrongMetaFileProp); + fail("This MUST have failed because the property 'metadata_file' has a wrong value!"); + }catch(Exception e){ + assertEquals(TAPException.class, e.getClass()); + assertEquals("A grave error occurred while reading/parsing the TableSet XML document: \"foo\"!", e.getMessage()); + } + + // No File Manager: + try{ + new ConfigurableServiceConnection(noFmProp); + fail("This MUST have failed because no File Manager is specified!"); + }catch(Exception e){ + assertEquals(TAPException.class, e.getClass()); + assertEquals("The property \"" + KEY_FILE_MANAGER + "\" is missing! It is required to create a TAP Service. Two possible values: " + VALUE_LOCAL + " or a class name between {...}.", e.getMessage()); + } + + // File Manager = Class Name: + try{ + ServiceConnection connection = new ConfigurableServiceConnection(fmClassNameProp); + assertNotNull(connection.getLogger()); + assertEquals(LogLevel.DEBUG, ((DefaultUWSLog)connection.getLogger()).getMinLogLevel()); + assertNotNull(connection.getFileManager()); + assertEquals("daily at 00:00", ((LocalUWSFileManager)connection.getFileManager()).getLogRotationFreq()); + assertNotNull(connection.getFactory()); + assertNotNull(connection.getTAPMetadata()); + assertFalse(connection.isAvailable()); + + /* Retention periods and execution durations are different in this configuration file from the valid one (validProp). + * Max period and max duration are set in this file as less than respectively the default period and the default duration. + * In such situation, the default period/duration is set to the maximum one, in order to ensure that the maximum value is + * still greater or equals than the default one. So the max and default values must be equal there. + */ + assertTrue(connection.getRetentionPeriod()[0] == connection.getRetentionPeriod()[1]); + assertTrue(connection.getExecutionDuration()[0] == connection.getExecutionDuration()[1]); + }catch(Exception e){ + fail("This MUST have succeeded because the provided file manager is a class name valid! \nCaught exception: " + getPertinentMessage(e)); + } + + // Incorrect File Manager Value: + try{ + new ConfigurableServiceConnection(incorrectFmProp); + fail("This MUST have failed because an incorrect File Manager value has been provided!"); + }catch(Exception e){ + assertEquals(TAPException.class, e.getClass()); + assertEquals("Class name expected for the property \"file_manager\" instead of: \"foo\"! The specified class must extend/implement uws.service.file.UWSFileManager.", e.getMessage()); + } + + // Custom log level and log rotation: + try{ + ServiceConnection connection = new ConfigurableServiceConnection(correctLogProp); + assertNotNull(connection.getLogger()); + assertEquals(LogLevel.WARNING, ((DefaultUWSLog)connection.getLogger()).getMinLogLevel()); + assertNotNull(connection.getFileManager()); + assertEquals("monthly on the 5th at 06:03", ((LocalUWSFileManager)connection.getFileManager()).getLogRotationFreq()); + }catch(Exception e){ + fail("This MUST have succeeded because the provided log level and log rotation are valid! \nCaught exception: " + getPertinentMessage(e)); + } + + // Incorrect log level: + try{ + ServiceConnection connection = new ConfigurableServiceConnection(incorrectLogLevelProp); + assertNotNull(connection.getLogger()); + assertEquals(LogLevel.DEBUG, ((DefaultUWSLog)connection.getLogger()).getMinLogLevel()); + }catch(Exception e){ + fail("This MUST have succeeded because even if the provided log level is incorrect the default behavior is to not throw exception and set the default value! \nCaught exception: " + getPertinentMessage(e)); + } + + // Incorrect log rotation: + try{ + ServiceConnection connection = new ConfigurableServiceConnection(incorrectLogRotationProp); + assertNotNull(connection.getFileManager()); + assertEquals("daily at 00:00", ((LocalUWSFileManager)connection.getFileManager()).getLogRotationFreq()); + }catch(Exception e){ + fail("This MUST have succeeded because even if the provided log rotation is incorrect the default behavior is to not throw exception and set the default value! \nCaught exception: " + getPertinentMessage(e)); + } + + // Valid output formats list: + try{ + ServiceConnection connection = new ConfigurableServiceConnection(validFormatsProp); + assertNotNull(connection.getOutputFormat(VALUE_VOTABLE)); + assertNotNull(connection.getOutputFormat(VALUE_JSON)); + assertNotNull(connection.getOutputFormat(VALUE_CSV)); + assertNotNull(connection.getOutputFormat(VALUE_TSV)); + assertNotNull(connection.getOutputFormat("psv")); + assertNotNull(connection.getOutputFormat("text/psv")); + assertNotNull(connection.getOutputFormat("text")); + assertNotNull(connection.getOutputFormat("text/plain")); + assertNotNull(connection.getOutputFormat("test")); + assertNotNull(connection.getOutputFormat("text/arobase")); + }catch(Exception e){ + fail("This MUST have succeeded because the property file is valid! \nCaught exception: " + getPertinentMessage(e)); + } + + // Valid VOTable output formats list: + try{ + ServiceConnection connection = new ConfigurableServiceConnection(validVOTableFormatsProp); + Iterator it = connection.getOutputFormats(); + OutputFormat f = it.next(); /* votable */ + assertEquals(VOTableFormat.class, f.getClass()); + assertEquals("application/x-votable+xml", f.getMimeType()); + assertEquals(VALUE_VOTABLE, f.getShortMimeType()); + assertEquals(DataFormat.BINARY, ((VOTableFormat)f).getVotSerialization()); + assertEquals(VOTableVersion.V13, ((VOTableFormat)f).getVotVersion()); + f = it.next(); /* votable():: */ + assertEquals(VOTableFormat.class, f.getClass()); + assertEquals("application/x-votable+xml", f.getMimeType()); + assertEquals(VALUE_VOTABLE, f.getShortMimeType()); + assertEquals(DataFormat.BINARY, ((VOTableFormat)f).getVotSerialization()); + assertEquals(VOTableVersion.V13, ((VOTableFormat)f).getVotVersion()); + f = it.next(); /* vot() */ + assertEquals(VOTableFormat.class, f.getClass()); + assertEquals("application/x-votable+xml", f.getMimeType()); + assertEquals(VALUE_VOTABLE, f.getShortMimeType()); + assertEquals(DataFormat.BINARY, ((VOTableFormat)f).getVotSerialization()); + assertEquals(VOTableVersion.V13, ((VOTableFormat)f).getVotVersion()); + f = it.next(); /* vot:: */ + assertEquals(VOTableFormat.class, f.getClass()); + assertEquals("application/x-votable+xml", f.getMimeType()); + assertEquals(VALUE_VOTABLE, f.getShortMimeType()); + assertEquals(DataFormat.BINARY, ((VOTableFormat)f).getVotSerialization()); + assertEquals(VOTableVersion.V13, ((VOTableFormat)f).getVotVersion()); + f = it.next(); /* votable: */ + assertEquals(VOTableFormat.class, f.getClass()); + assertEquals("application/x-votable+xml", f.getMimeType()); + assertEquals(VALUE_VOTABLE, f.getShortMimeType()); + assertEquals(DataFormat.BINARY, ((VOTableFormat)f).getVotSerialization()); + assertEquals(VOTableVersion.V13, ((VOTableFormat)f).getVotVersion()); + f = it.next(); /* votable(Td, 1.0) */ + assertEquals(VOTableFormat.class, f.getClass()); + assertEquals("application/x-votable+xml;serialization=TABLEDATA", f.getMimeType()); + assertEquals("votable/td", f.getShortMimeType()); + assertEquals(DataFormat.TABLEDATA, ((VOTableFormat)f).getVotSerialization()); + assertEquals(VOTableVersion.V10, ((VOTableFormat)f).getVotVersion()); + f = it.next(); /* votable(TableData) */ + assertEquals(VOTableFormat.class, f.getClass()); + assertEquals("application/x-votable+xml;serialization=TABLEDATA", f.getMimeType()); + assertEquals("votable/td", f.getShortMimeType()); + assertEquals(DataFormat.TABLEDATA, ((VOTableFormat)f).getVotSerialization()); + assertEquals(VOTableVersion.V13, ((VOTableFormat)f).getVotVersion()); + f = it.next(); /* votable(, 1.2) */ + assertEquals(VOTableFormat.class, f.getClass()); + assertEquals("application/x-votable+xml", f.getMimeType()); + assertEquals(VALUE_VOTABLE, f.getShortMimeType()); + assertEquals(DataFormat.BINARY, ((VOTableFormat)f).getVotSerialization()); + assertEquals(VOTableVersion.V12, ((VOTableFormat)f).getVotVersion()); + f = it.next(); /* vot(fits):application/fits,supervot */ + assertEquals(VOTableFormat.class, f.getClass()); + assertEquals("application/fits", f.getMimeType()); + assertEquals("supervot", f.getShortMimeType()); + assertEquals(DataFormat.FITS, ((VOTableFormat)f).getVotSerialization()); + assertEquals(VOTableVersion.V13, ((VOTableFormat)f).getVotVersion()); + assertFalse(it.hasNext()); + }catch(Exception e){ + fail("This MUST have succeeded because the property file is valid! \nCaught exception: " + getPertinentMessage(e)); + } + + // Bad SV(...) format 1 = "sv": + try{ + new ConfigurableServiceConnection(badSVFormat1Prop); + fail("This MUST have failed because an incorrect SV output format value has been provided!"); + }catch(Exception e){ + assertEquals(TAPException.class, e.getClass()); + assertEquals("Missing separator char/string for the SV output format: \"sv\"!", e.getMessage()); + } + + // Bad SV(...) format 2 = "sv()": + try{ + new ConfigurableServiceConnection(badSVFormat2Prop); + fail("This MUST have failed because an incorrect SV output format value has been provided!"); + }catch(Exception e){ + assertEquals(TAPException.class, e.getClass()); + assertEquals("Missing separator char/string for the SV output format: \"sv()\"!", e.getMessage()); + } + + // Bad VOTable(...) format 1 = "votable(foo)": + try{ + new ConfigurableServiceConnection(badVotFormat1Prop); + fail("This MUST have failed because an incorrect VOTable output format value has been provided!"); + }catch(Exception e){ + assertEquals(TAPException.class, e.getClass()); + assertEquals("Unsupported VOTable serialization: \"foo\"! Accepted values: 'binary' (or 'b'), 'binary2' (or 'b2'), 'tabledata' (or 'td') and 'fits'.", e.getMessage()); + } + + // Bad VOTable(...) format 2 = "votable(,foo)": + try{ + new ConfigurableServiceConnection(badVotFormat2Prop); + fail("This MUST have failed because an incorrect VOTable output format value has been provided!"); + }catch(Exception e){ + assertEquals(TAPException.class, e.getClass()); + assertEquals("Unsupported VOTable version: \"foo\"! Accepted values: '1.0' (or 'v1.0'), '1.1' (or 'v1.1'), '1.2' (or 'v1.2') and '1.3' (or 'v1.3').", e.getMessage()); + } + + // Bad VOTable(...) format 3 = "text, vot(TD": + try{ + new ConfigurableServiceConnection(badVotFormat3Prop); + fail("This MUST have failed because an incorrect VOTable output format value has been provided!"); + }catch(Exception e){ + assertEquals(TAPException.class, e.getClass()); + assertEquals("Wrong output format specification syntax in: \"vot(TD\"! A VOTable parameters list must end with ')'.", e.getMessage()); + } + + // Bad VOTable(...) format 4 = "vot(TD, text": + try{ + new ConfigurableServiceConnection(badVotFormat4Prop); + fail("This MUST have failed because an incorrect VOTable output format value has been provided!"); + }catch(Exception e){ + assertEquals(TAPException.class, e.getClass()); + assertEquals("Missing right parenthesis in: \"vot(TD, text\"!", e.getMessage()); + } + + // Bad VOTable(...) format 5 = "vot(TD, 1.0, foo)": + try{ + new ConfigurableServiceConnection(badVotFormat5Prop); + fail("This MUST have failed because an incorrect VOTable output format value has been provided!"); + }catch(Exception e){ + assertEquals(TAPException.class, e.getClass()); + assertEquals("Wrong number of parameters for the output format VOTable: \"vot(TD, 1.0, foo)\"! Only two parameters may be provided: serialization and version.", e.getMessage()); + } + + // Bad VOTable(...) format 6 = "vot:application/xml:votable:foo": + try{ + new ConfigurableServiceConnection(badVotFormat6Prop); + fail("This MUST have failed because an incorrect VOTable output format value has been provided!"); + }catch(Exception e){ + assertEquals(TAPException.class, e.getClass()); + assertEquals("Wrong output format specification syntax in: \"vot:application/xml:votable:foo\"! After a MIME type and a short MIME type, no more information is expected.", e.getMessage()); + } + + // Unknown output format: + try{ + new ConfigurableServiceConnection(unknownFormatProp); + fail("This MUST have failed because an incorrect output format value has been provided!"); + }catch(Exception e){ + assertEquals(TAPException.class, e.getClass()); + assertEquals("Unknown output format: foo", e.getMessage()); + } + + // Valid value for max_async_jobs: + try{ + ServiceConnection connection = new ConfigurableServiceConnection(maxAsyncProp); + assertEquals(10, connection.getNbMaxAsyncJobs()); + }catch(Exception e){ + fail("This MUST have succeeded because a valid max_async_jobs is provided! \nCaught exception: " + getPertinentMessage(e)); + } + + // Negative value for max_async_jobs: + try{ + ServiceConnection connection = new ConfigurableServiceConnection(negativeMaxAsyncProp); + assertEquals(-2, connection.getNbMaxAsyncJobs()); + }catch(Exception e){ + fail("This MUST have succeeded because a negative max_async_jobs is equivalent to 'no restriction'! \nCaught exception: " + getPertinentMessage(e)); + } + + // A not integer value for max_async_jobs: + try{ + new ConfigurableServiceConnection(notIntMaxAsyncProp); + fail("This MUST have failed because a not integer value has been provided for \"" + KEY_MAX_ASYNC_JOBS + "\"!"); + }catch(Exception e){ + assertEquals(TAPException.class, e.getClass()); + assertEquals("Integer expected for the property \"" + KEY_MAX_ASYNC_JOBS + "\", instead of: \"foo\"!", e.getMessage()); + } + + // Test with no output limit specified: + try{ + ServiceConnection connection = new ConfigurableServiceConnection(validProp); + assertEquals(connection.getOutputLimit()[0], -1); + assertEquals(connection.getOutputLimit()[1], -1); + assertEquals(connection.getOutputLimitType()[0], LimitUnit.rows); + assertEquals(connection.getOutputLimitType()[1], LimitUnit.rows); + }catch(Exception e){ + fail("This MUST have succeeded because providing no output limit is valid! \nCaught exception: " + getPertinentMessage(e)); + } + + // Test with only a set default output limit: + try{ + ServiceConnection connection = new ConfigurableServiceConnection(defaultOutputLimitProp); + assertEquals(connection.getOutputLimit()[0], 100); + assertEquals(connection.getOutputLimit()[1], -1); + assertEquals(connection.getOutputLimitType()[0], LimitUnit.rows); + assertEquals(connection.getOutputLimitType()[1], LimitUnit.rows); + }catch(Exception e){ + fail("This MUST have succeeded because setting the default output limit is valid! \nCaught exception: " + getPertinentMessage(e)); + } + + // Test with only a set maximum output limit: + try{ + ServiceConnection connection = new ConfigurableServiceConnection(maxOutputLimitProp); + assertEquals(1000, connection.getOutputLimit()[0]); + assertEquals(1000, connection.getOutputLimit()[1]); + assertEquals(LimitUnit.rows, connection.getOutputLimitType()[0]); + assertEquals(LimitUnit.rows, connection.getOutputLimitType()[1]); + }catch(Exception e){ + fail("This MUST have succeeded because setting only the maximum output limit is valid! \nCaught exception: " + getPertinentMessage(e)); + } + + // Test with both a default and a maximum output limits where default <= max: + try{ + ServiceConnection connection = new ConfigurableServiceConnection(bothOutputLimitGoodProp); + assertEquals(connection.getOutputLimit()[0], 100); + assertEquals(connection.getOutputLimit()[1], 1000); + assertEquals(connection.getOutputLimitType()[0], LimitUnit.rows); + assertEquals(connection.getOutputLimitType()[1], LimitUnit.rows); + }catch(Exception e){ + fail("This MUST have succeeded because the default output limit is less or equal the maximum one! \nCaught exception: " + getPertinentMessage(e)); + } + + // Test with both a default and a maximum output limits BUT where default > max: + /* In a such case, the default value is set silently to the maximum one. */ + try{ + ServiceConnection connection = new ConfigurableServiceConnection(bothOutputLimitBadProp); + assertEquals(100, connection.getOutputLimit()[1]); + assertEquals(connection.getOutputLimit()[1], connection.getOutputLimit()[0]); + assertEquals(LimitUnit.rows, connection.getOutputLimitType()[1]); + assertEquals(connection.getOutputLimitType()[1], connection.getOutputLimitType()[0]); + }catch(Exception e){ + fail("This MUST have succeeded because the default output limit is set automatically to the maximum one if bigger! \nCaught exception: " + getPertinentMessage(e)); + } + + // Test with a not integer sync. fetch size: + try{ + new ConfigurableServiceConnection(notIntSyncFetchSizeProp); + fail("This MUST have failed because the set sync. fetch size is not an integer!"); + }catch(Exception e){ + assertEquals(TAPException.class, e.getClass()); + assertEquals("Integer expected for the property " + KEY_SYNC_FETCH_SIZE + ": \"foo\"!", e.getMessage()); + } + + // Test with a negative sync. fetch size: + try{ + ServiceConnection connection = new ConfigurableServiceConnection(negativeSyncFetchSizeProp); + assertNotNull(connection.getFetchSize()); + assertTrue(connection.getFetchSize().length >= 2); + assertEquals(connection.getFetchSize()[1], 0); + }catch(Exception e){ + fail("This MUST have succeeded because a negative fetch size must be set by default to 0 (meaning default JDBC driver value)! \nCaught exception: " + getPertinentMessage(e)); + } + + // Test with any valid sync. fetch size: + try{ + ServiceConnection connection = new ConfigurableServiceConnection(syncFetchSizeProp); + assertNotNull(connection.getFetchSize()); + assertTrue(connection.getFetchSize().length >= 2); + assertEquals(connection.getFetchSize()[1], 50); + }catch(Exception e){ + fail("This MUST have succeeded because a valid fetch size has been provided! \nCaught exception: " + getPertinentMessage(e)); + } + + // Test with a not integer async. fetch size: + try{ + new ConfigurableServiceConnection(notIntAsyncFetchSizeProp); + fail("This MUST have failed because the set async. fetch size is not an integer!"); + }catch(Exception e){ + assertEquals(TAPException.class, e.getClass()); + assertEquals("Integer expected for the property " + KEY_ASYNC_FETCH_SIZE + ": \"foo\"!", e.getMessage()); + } + + // Test with a negative async. fetch size: + try{ + ServiceConnection connection = new ConfigurableServiceConnection(negativeAsyncFetchSizeProp); + assertNotNull(connection.getFetchSize()); + assertTrue(connection.getFetchSize().length >= 1); + assertEquals(connection.getFetchSize()[0], 0); + }catch(Exception e){ + fail("This MUST have succeeded because a negative fetch size must be set by default to 0 (meaning default JDBC driver value)! \nCaught exception: " + getPertinentMessage(e)); + } + + // Test with any valid async. fetch size: + try{ + ServiceConnection connection = new ConfigurableServiceConnection(asyncFetchSizeProp); + assertNotNull(connection.getFetchSize()); + assertTrue(connection.getFetchSize().length >= 1); + assertEquals(connection.getFetchSize()[0], 50); + }catch(Exception e){ + fail("This MUST have succeeded because a valid fetch size has been provided! \nCaught exception: " + getPertinentMessage(e)); + } + + // Valid user identifier: + try{ + ServiceConnection connection = new ConfigurableServiceConnection(userIdentProp); + assertNotNull(connection.getUserIdentifier()); + assertNotNull(connection.getUserIdentifier().extractUserId(null, null)); + assertEquals("everybody", connection.getUserIdentifier().extractUserId(null, null).getID()); + }catch(Exception e){ + fail("This MUST have succeeded because the class path toward the fake UserIdentifier is correct! \nCaught exception: " + getPertinentMessage(e)); + } + + // Not a class name for user_identifier: + try{ + new ConfigurableServiceConnection(notClassPathUserIdentProp); + fail("This MUST have failed because the user_identifier value is not a class name!"); + }catch(Exception e){ + assertEquals(TAPException.class, e.getClass()); + assertEquals("Class name expected for the property \"" + KEY_USER_IDENTIFIER + "\" instead of: \"foo\"! The specified class must extend/implement uws.service.UserIdentifier.", e.getMessage()); + } + + // Valid geometry list: + try{ + ServiceConnection connection = new ConfigurableServiceConnection(geometriesProp); + assertNotNull(connection.getGeometries()); + assertEquals(4, connection.getGeometries().size()); + assertEquals("POINT", ((ArrayList)connection.getGeometries()).get(0)); + assertEquals("CIRCLE", ((ArrayList)connection.getGeometries()).get(1)); + assertEquals("CONTAINS", ((ArrayList)connection.getGeometries()).get(2)); + assertEquals("INTERSECTS", ((ArrayList)connection.getGeometries()).get(3)); + }catch(Exception e){ + fail("This MUST have succeeded because the given list of geometries is correct! \nCaught exception: " + getPertinentMessage(e)); + } + + // "NONE" as geometry list: + try{ + ServiceConnection connection = new ConfigurableServiceConnection(noneGeomProp); + assertNotNull(connection.getGeometries()); + assertEquals(0, connection.getGeometries().size()); + }catch(Exception e){ + fail("This MUST have succeeded because the given list of geometries is correct (reduced to only NONE)! \nCaught exception: " + getPertinentMessage(e)); + } + + // "ANY" as geometry list: + try{ + ServiceConnection connection = new ConfigurableServiceConnection(anyGeomProp); + assertNull(connection.getGeometries()); + }catch(Exception e){ + fail("This MUST have succeeded because the given list of geometries is correct (reduced to only ANY)! \nCaught exception: " + getPertinentMessage(e)); + } + + // "NONE" inside a geometry list: + try{ + new ConfigurableServiceConnection(noneInsideGeomProp); + fail("This MUST have failed because the given geometry list contains at least 2 items, whose one is NONE!"); + }catch(Exception e){ + assertEquals(TAPException.class, e.getClass()); + assertEquals("The special value \"" + VALUE_NONE + "\" can not be used inside a list! It MUST be used in replacement of a whole list to specify that no value is allowed.", e.getMessage()); + } + + // Unknown geometrical function: + try{ + new ConfigurableServiceConnection(unknownGeomProp); + fail("This MUST have failed because the given geometry list contains at least 1 unknown ADQL geometrical function!"); + }catch(Exception e){ + assertEquals(TAPException.class, e.getClass()); + assertEquals("Unknown ADQL geometrical function: \"foo\"!", e.getMessage()); + } + + // Valid coordinate systems list: + try{ + ServiceConnection connection = new ConfigurableServiceConnection(coordSysProp); + assertNotNull(connection.getCoordinateSystems()); + assertEquals(2, connection.getCoordinateSystems().size()); + assertEquals("icrs * *", ((ArrayList)connection.getCoordinateSystems()).get(0)); + assertEquals("ICrs * (Spherical2| CARTEsian2)", ((ArrayList)connection.getCoordinateSystems()).get(1)); + }catch(Exception e){ + fail("This MUST have succeeded because the given list of coordinate systems is correct! \nCaught exception: " + getPertinentMessage(e)); + } + + // "NONE" as coordinate systems list: + try{ + ServiceConnection connection = new ConfigurableServiceConnection(noneCoordSysProp); + assertNotNull(connection.getCoordinateSystems()); + assertEquals(0, connection.getCoordinateSystems().size()); + }catch(Exception e){ + fail("This MUST have succeeded because the given list of coordinate systems is correct (reduced to only NONE)! \nCaught exception: " + getPertinentMessage(e)); + } + + // "ANY" as coordinate systems list: + try{ + ServiceConnection connection = new ConfigurableServiceConnection(anyCoordSysProp); + assertNull(connection.getCoordinateSystems()); + }catch(Exception e){ + fail("This MUST have succeeded because the given list of coordinate systems is correct (reduced to only ANY)! \nCaught exception: " + getPertinentMessage(e)); + } + + // "NONE" inside a coordinate systems list: + try{ + new ConfigurableServiceConnection(noneInsideCoordSysProp); + fail("This MUST have failed because the given coordinate systems list contains at least 3 items, whose one is NONE!"); + }catch(Exception e){ + assertEquals(TAPException.class, e.getClass()); + assertEquals("The special value \"" + VALUE_NONE + "\" can not be used inside a list! It MUST be used in replacement of a whole list to specify that no value is allowed.", e.getMessage()); + } + + // Unknown coordinate system function: + try{ + new ConfigurableServiceConnection(unknownCoordSysProp); + fail("This MUST have failed because the given coordinate systems list contains at least 1 unknown coordinate system!"); + }catch(Exception e){ + assertEquals(TAPException.class, e.getClass()); + assertEquals("Incorrect coordinate system regular expression (\"ICRS foo *\"): Wrong allowed coordinate system syntax for the 1-th item: \"ICRS foo *\"! Expected: \"frameRegExp refposRegExp flavorRegExp\" ; where each xxxRegExp = (xxx | '*' | '('xxx ('|' xxx)*')'), frame=\"" + Frame.regexp + "\", refpos=\"" + RefPos.regexp + "\" and flavor=\"" + Flavor.regexp + "\" ; an empty string is also allowed and will be interpreted as '*' (so all possible values).", e.getMessage()); + } + + // "ANY" as UDFs list: + try{ + ServiceConnection connection = new ConfigurableServiceConnection(anyUdfsProp); + assertNull(connection.getUDFs()); + }catch(Exception e){ + fail("This MUST have succeeded because the given list of UDFs is correct (reduced to only ANY)! \nCaught exception: " + getPertinentMessage(e)); + } + + // "NONE" as UDFs list: + try{ + ServiceConnection connection = new ConfigurableServiceConnection(noneUdfsProp); + assertNotNull(connection.getUDFs()); + assertEquals(0, connection.getUDFs().size()); + }catch(Exception e){ + fail("This MUST have succeeded because the given list of UDFs is correct (reduced to only NONE)! \nCaught exception: " + getPertinentMessage(e)); + } + + // Valid list of UDFs: + try{ + ServiceConnection connection = new ConfigurableServiceConnection(udfsProp); + assertNotNull(connection.getUDFs()); + assertEquals(2, connection.getUDFs().size()); + Iterator it = connection.getUDFs().iterator(); + assertEquals("toto(a VARCHAR)", it.next().toString()); + assertEquals("titi(b REAL) -> DOUBLE", it.next().toString()); + }catch(Exception e){ + fail("This MUST have succeeded because the given list of UDFs contains valid items! \nCaught exception: " + getPertinentMessage(e)); + } + + // Valid list of UDFs containing one UDF with a class name: + try{ + ServiceConnection connection = new ConfigurableServiceConnection(udfsWithClassNameProp); + assertNotNull(connection.getUDFs()); + assertEquals(1, connection.getUDFs().size()); + FunctionDef def = connection.getUDFs().iterator().next(); + assertEquals("toto(a VARCHAR) -> VARCHAR", def.toString()); + assertEquals(UDFToto.class, def.getUDFClass()); + }catch(Exception e){ + fail("This MUST have succeeded because the given list of UDFs contains valid items! \nCaught exception: " + getPertinentMessage(e)); + } + + // "NONE" inside a UDFs list: + try{ + new ConfigurableServiceConnection(udfsListWithNONEorANYProp); + fail("This MUST have failed because the given UDFs list contains at least 2 items, whose one is ANY!"); + }catch(Exception e){ + assertEquals(TAPException.class, e.getClass()); + assertEquals("Wrong UDF declaration syntax: unexpected character at position 27 in the property " + KEY_UDFS + ": \"A\"! A UDF declaration must have one of the following syntaxes: \"[signature]\" or \"[signature,{className}]\".", e.getMessage()); + } + + // UDF with no brackets: + try{ + new ConfigurableServiceConnection(udfsWithMissingBracketsProp); + fail("This MUST have failed because one UDFs list item has no brackets!"); + }catch(Exception e){ + assertEquals(TAPException.class, e.getClass()); + assertEquals("Wrong UDF declaration syntax: unexpected character at position 1 in the property " + KEY_UDFS + ": \"t\"! A UDF declaration must have one of the following syntaxes: \"[signature]\" or \"[signature,{className}]\".", e.getMessage()); + } + + // UDFs whose one item have more parts than supported: + try{ + new ConfigurableServiceConnection(udfsWithWrongParamLengthProp); + fail("This MUST have failed because one UDFs list item has too many parameters!"); + }catch(Exception e){ + assertEquals(TAPException.class, e.getClass()); + assertEquals("Wrong UDF declaration syntax: only two items (signature and class name) can be given within brackets. (position in the property " + KEY_UDFS + ": 58)", e.getMessage()); + } + + // UDF with missing definition part (or wrong since there is no comma): + try{ + new ConfigurableServiceConnection(udfsWithMissingDefProp1); + fail("This MUST have failed because one UDFs list item has a wrong signature part (it has been forgotten)!"); + }catch(Exception e){ + assertEquals(TAPException.class, e.getClass()); + assertEquals("Wrong UDF declaration syntax: Wrong function definition syntax! Expected syntax: \"(?) ?\", where =\"[a-zA-Z]+[a-zA-Z0-9_]*\", =\" -> \", =\"( (, )*)\", should be one of the types described in the UPLOAD section of the TAP documentation. Examples of good syntax: \"foo()\", \"foo() -> VARCHAR\", \"foo(param INTEGER)\", \"foo(param1 INTEGER, param2 DOUBLE) -> DOUBLE\" (position in the property " + KEY_UDFS + ": 2-33)", e.getMessage()); + } + + // UDF with missing definition part (or wrong since there is no comma): + try{ + new ConfigurableServiceConnection(udfsWithMissingDefProp2); + fail("This MUST have failed because one UDFs list item has no signature part!"); + }catch(Exception e){ + assertEquals(TAPException.class, e.getClass()); + assertEquals("Missing UDF declaration! (position in the property " + KEY_UDFS + ": 2-2)", e.getMessage()); + } + + // Empty UDF item (without comma): + try{ + ServiceConnection connection = new ConfigurableServiceConnection(emptyUdfItemProp1); + assertNotNull(connection.getUDFs()); + assertEquals(0, connection.getUDFs().size()); + }catch(Exception e){ + fail("This MUST have succeeded because the given list of UDFs contains one empty UDF (which should be merely ignored)! \nCaught exception: " + getPertinentMessage(e)); + } + + // Empty UDF item (with comma): + try{ + ServiceConnection connection = new ConfigurableServiceConnection(emptyUdfItemProp2); + assertNotNull(connection.getUDFs()); + assertEquals(0, connection.getUDFs().size()); + }catch(Exception e){ + fail("This MUST have succeeded because the given list of UDFs contains one empty UDF (which should be merely ignored)! \nCaught exception: " + getPertinentMessage(e)); + } + + // UDF item without its closing bracket: + try{ + new ConfigurableServiceConnection(udfWithMissingEndBracketProp); + fail("This MUST have failed because one UDFs list item has no closing bracket!"); + }catch(Exception e){ + assertEquals(TAPException.class, e.getClass()); + assertEquals("Wrong UDF declaration syntax: missing closing bracket at position 24!", e.getMessage()); + } + + // Valid custom TAPFactory: + try{ + ServiceConnection connection = new ConfigurableServiceConnection(customFactoryProp); + assertNotNull(connection.getFactory()); + assertEquals(CustomTAPFactory.class, connection.getFactory().getClass()); + }catch(Exception e){ + fail("This MUST have succeeded because the given custom TAPFactory exists and have the required constructor! \nCaught exception: " + getPertinentMessage(e)); + } + + // Bad custom TAPFactory (required constructor missing): + try{ + new ConfigurableServiceConnection(badCustomFactoryProp); + fail("This MUST have failed because the specified TAPFactory extension does not have a constructor with ServiceConnection!"); + }catch(Exception e){ + assertEquals(TAPException.class, e.getClass()); + assertEquals("Missing constructor tap.config.TestConfigurableServiceConnection$BadCustomTAPFactory(tap.ServiceConnection)! See the value \"{tap.config.TestConfigurableServiceConnection$BadCustomTAPFactory}\" of the property \"" + KEY_TAP_FACTORY + "\".", e.getMessage()); + } + } + + @Test + public void testGetFile(){ + final String rootPath = "/ROOT", propertyName = "SuperProperty"; + String path; + + try{ + // NULL test => NULL must be returned. + assertNull(ConfigurableServiceConnection.getFile(null, rootPath, propertyName)); + + // Valid file URI: + path = "/custom/user/dir"; + assertEquals(path, ConfigurableServiceConnection.getFile("file://" + path, rootPath, propertyName).getAbsolutePath()); + + // Valid absolute file path: + assertEquals(path, ConfigurableServiceConnection.getFile(path, rootPath, propertyName).getAbsolutePath()); + + // File name relative to the given rootPath: + path = "dir"; + assertEquals(rootPath + File.separator + path, ConfigurableServiceConnection.getFile(path, rootPath, propertyName).getAbsolutePath()); + + // Idem but with a relative file path: + path = "gmantele/workspace"; + assertEquals(rootPath + File.separator + path, ConfigurableServiceConnection.getFile(path, rootPath, propertyName).getAbsolutePath()); + + }catch(Exception ex){ + ex.printStackTrace(); + fail("None of these tests should have failed!"); + } + + // Test with a file URI having a bad syntax: + path = "file:#toto^foo"; + try{ + ConfigurableServiceConnection.getFile(path, rootPath, propertyName); + fail("This test should have failed, because the given file URI has a bad syntax!"); + }catch(Exception ex){ + assertEquals(TAPException.class, ex.getClass()); + assertEquals("Incorrect file URI for the property \"" + propertyName + "\": \"" + path + "\"! Bad syntax for the given file URI.", ex.getMessage()); + } + + // Test with an URL: + path = "http://www.google.com"; + try{ + ConfigurableServiceConnection.getFile(path, rootPath, propertyName); + fail("This test should have failed, because the given URI uses the HTTP protocol (actually, it uses a protocol different from \"file\"!"); + }catch(Exception ex){ + assertEquals(TAPException.class, ex.getClass()); + assertEquals("Incorrect file URI for the property \"" + propertyName + "\": \"" + path + "\"! Only URI with the protocol \"file:\" are allowed.", ex.getMessage()); + } + + } + + public static final String getPertinentMessage(final Exception ex){ + return (ex.getCause() == null || ex.getMessage().equals(ex.getCause().getMessage())) ? ex.getMessage() : ex.getCause().getMessage(); + } + + /** + * A UWSFileManager to test the load of a UWSFileManager from the configuration file with a class path. + * + * @author Grégory Mantelet (ARI) + * @version 01/2015 + * @see TestConfigurableServiceConnection#testDefaultServiceConnectionProperties() + */ + public static class FileManagerTest extends LocalUWSFileManager { + public FileManagerTest(Properties tapConfig) throws UWSException{ + super(new File(tapConfig.getProperty("file_root_path")), true, false); + } + } + + /** + * A UserIdentifier which always return the same user...that's to say, all users are in a way still anonymous :-) + * This class is only for test purpose. + * + * @author Grégory Mantelet (ARI) + * @version 02/2015 + */ + public static class UserIdentifierTest implements UserIdentifier { + private static final long serialVersionUID = 1L; + + private final JobOwner everybody = new DefaultJobOwner("everybody"); + + @Override + public JobOwner extractUserId(UWSUrl urlInterpreter, HttpServletRequest request) throws UWSException{ + return everybody; + } + + @Override + public JobOwner restoreUser(String id, String pseudo, Map otherData) throws UWSException{ + return everybody; + } + + } + + /** + * TAPFactory just to test whether the property tap_factory works well. + * + * @author Grégory Mantelet (ARI) + * @version 02/2015 + */ + private static class CustomTAPFactory extends AbstractTAPFactory { + + private final JDBCConnection dbConn; + + public CustomTAPFactory(final ServiceConnection conn) throws DBException{ + super(conn); + dbConn = new JDBCConnection("", "jdbc:postgresql:gmantele", "gmantele", null, new PostgreSQLTranslator(), "TheOnlyConnection", conn.getLogger()); + } + + @Override + public DBConnection getConnection(final String jobID) throws TAPException{ + return dbConn; + } + + @Override + public void freeConnection(final DBConnection conn){} + + @Override + public void destroy(){ + try{ + dbConn.getInnerConnection().close(); + }catch(Exception ex){} + } + + } + + /** + * TAPFactory just to test whether the property tap_factory is rejected when no constructor with a single parameter of type ServiceConnection exists. + * + * @author Grégory Mantelet (ARI) + * @version 02/2015 + */ + private static class BadCustomTAPFactory extends AbstractTAPFactory { + + public BadCustomTAPFactory() throws DBException{ + super(null); + } + + @Override + public DBConnection getConnection(final String jobID) throws TAPException{ + return null; + } + + @Override + public void freeConnection(final DBConnection conn){} + + @Override + public void destroy(){} + + } + +} diff --git a/test/tap/config/TestConfigurableTAPFactory.java b/test/tap/config/TestConfigurableTAPFactory.java new file mode 100644 index 0000000..c6bf100 --- /dev/null +++ b/test/tap/config/TestConfigurableTAPFactory.java @@ -0,0 +1,512 @@ +package tap.config; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static tap.config.TAPConfiguration.KEY_BACKUP_BY_USER; +import static tap.config.TAPConfiguration.KEY_BACKUP_FREQUENCY; +import static tap.config.TAPConfiguration.KEY_DATABASE_ACCESS; +import static tap.config.TAPConfiguration.KEY_DATASOURCE_JNDI_NAME; +import static tap.config.TAPConfiguration.KEY_DB_PASSWORD; +import static tap.config.TAPConfiguration.KEY_DB_USERNAME; +import static tap.config.TAPConfiguration.KEY_JDBC_DRIVER; +import static tap.config.TAPConfiguration.KEY_JDBC_URL; +import static tap.config.TAPConfiguration.KEY_SQL_TRANSLATOR; +import static tap.config.TAPConfiguration.VALUE_JDBC; +import static tap.config.TAPConfiguration.VALUE_JNDI; +import static tap.config.TAPConfiguration.VALUE_PGSPHERE; +import static tap.config.TAPConfiguration.VALUE_POSTGRESQL; + +import java.io.File; +import java.sql.SQLException; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Properties; + +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.servlet.http.HttpServletRequest; + +import org.junit.BeforeClass; +import org.junit.Test; +import org.postgresql.ds.PGSimpleDataSource; +import org.postgresql.util.PSQLException; + +import tap.ServiceConnection; +import tap.TAPException; +import tap.TAPFactory; +import tap.backup.DefaultTAPBackupManager; +import tap.db.DBConnection; +import tap.db.DBException; +import tap.db.JDBCConnection; +import tap.formatter.OutputFormat; +import tap.log.DefaultTAPLog; +import tap.log.TAPLog; +import tap.metadata.TAPMetadata; +import uws.UWSException; +import uws.job.user.JobOwner; +import uws.service.UWSService; +import uws.service.UWSUrl; +import uws.service.UserIdentifier; +import uws.service.file.LocalUWSFileManager; +import uws.service.file.UWSFileManager; +import adql.db.FunctionDef; + +public class TestConfigurableTAPFactory { + + private static Properties validJDBCProp, validJNDIProp, + incorrectDBAccessProp, missingDBAccessProp, + missingDatasourceJNDINameProp, wrongDatasourceJNDINameProp, + noJdbcProp1, noJdbcProp2, noJdbcProp3, badJdbcProp, + missingTranslatorProp, badTranslatorProp, badDBNameProp, + badUsernameProp, badPasswordProp, validBackupFrequency, noBackup, + userBackup, badBackupFrequency; + + private static ServiceConnection serviceConnection = null; + + private static void setJNDIDatasource() throws NamingException{ + // Create an initial JNDI context: + /* note: this requires that the simple-jndi jar is in the classpath. (https://code.google.com/p/osjava/downloads/detail?name=simple-jndi-0.11.4.1.zip&can=2&q=) */ + System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.osjava.sj.memory.MemoryContextFactory"); + System.setProperty("org.osjava.sj.jndi.shared", "true"); // memory shared between all instances of InitialContext + + // Context initialization: + InitialContext ic = new InitialContext(); + + // Creation of a reference on a DataSource: + PGSimpleDataSource datasource = new PGSimpleDataSource(); + datasource.setServerName("localhost"); + datasource.setDatabaseName("gmantele"); + + // Link the datasource with the context: + ic.rebind("jdbc/MyDataSource", datasource); + } + + @BeforeClass + public static void beforeClass() throws Exception{ + // BUILD A FAKE SERVICE CONNECTION: + serviceConnection = new ServiceConnectionTest(); + + // LOAD ALL PROPERTIES FILES NEEDED FOR ALL THE TESTS: + validJDBCProp = AllTAPConfigTests.getValidProperties(); + + setJNDIDatasource(); + validJNDIProp = (Properties)validJDBCProp.clone(); + validJNDIProp.setProperty(KEY_DATABASE_ACCESS, "jndi"); + validJNDIProp.setProperty(KEY_DATASOURCE_JNDI_NAME, "jdbc/MyDataSource"); + validJNDIProp.remove(KEY_JDBC_URL); + validJNDIProp.remove(KEY_JDBC_DRIVER); + validJNDIProp.remove(KEY_DB_USERNAME); + validJNDIProp.remove(KEY_DB_PASSWORD); + + incorrectDBAccessProp = (Properties)validJDBCProp.clone(); + incorrectDBAccessProp.setProperty(KEY_DATABASE_ACCESS, "foo"); + + missingDBAccessProp = (Properties)validJDBCProp.clone(); + missingDBAccessProp.remove(KEY_DATABASE_ACCESS); + + missingDatasourceJNDINameProp = (Properties)validJNDIProp.clone(); + missingDatasourceJNDINameProp.remove(KEY_DATASOURCE_JNDI_NAME); + + wrongDatasourceJNDINameProp = (Properties)validJNDIProp.clone(); + wrongDatasourceJNDINameProp.setProperty(KEY_DATASOURCE_JNDI_NAME, "foo"); + + noJdbcProp1 = (Properties)validJDBCProp.clone(); + noJdbcProp1.remove(KEY_JDBC_DRIVER); + + noJdbcProp2 = (Properties)noJdbcProp1.clone(); + noJdbcProp2.setProperty(KEY_JDBC_URL, "jdbc:foo:gmantele"); + + noJdbcProp3 = (Properties)noJdbcProp1.clone(); + noJdbcProp3.remove(KEY_JDBC_URL); + + badJdbcProp = (Properties)validJDBCProp.clone(); + badJdbcProp.setProperty(KEY_JDBC_DRIVER, "foo"); + badJdbcProp.setProperty(KEY_JDBC_URL, "jdbc:foo:gmantele"); + + missingTranslatorProp = (Properties)validJDBCProp.clone(); + missingTranslatorProp.remove(KEY_SQL_TRANSLATOR); + + badTranslatorProp = (Properties)validJDBCProp.clone(); + badTranslatorProp.setProperty(KEY_SQL_TRANSLATOR, "foo"); + + badDBNameProp = (Properties)validJDBCProp.clone(); + badDBNameProp.setProperty(KEY_JDBC_URL, "jdbc:postgresql:foo"); + + badUsernameProp = (Properties)validJDBCProp.clone(); + badUsernameProp.setProperty(KEY_DB_USERNAME, "foo"); + + badPasswordProp = (Properties)validJDBCProp.clone(); + badPasswordProp.setProperty(KEY_DB_PASSWORD, "foo"); + + validBackupFrequency = (Properties)validJDBCProp.clone(); + validBackupFrequency.setProperty(KEY_BACKUP_FREQUENCY, "3600"); + + noBackup = (Properties)validJDBCProp.clone(); + noBackup.setProperty(KEY_BACKUP_FREQUENCY, "never"); + + userBackup = (Properties)validJDBCProp.clone(); + userBackup.setProperty(KEY_BACKUP_FREQUENCY, "user_action"); + + badBackupFrequency = (Properties)validJDBCProp.clone(); + badBackupFrequency.setProperty(KEY_BACKUP_FREQUENCY, "foo"); + } + + @Test + public void testDefaultServiceConnection(){ + // Correct Parameters (JDBC CASE): + DBConnection connection = null; + try{ + TAPFactory factory = new ConfigurableTAPFactory(serviceConnection, validJDBCProp); + connection = factory.getConnection("0"); + assertNotNull(connection); + assertNull(factory.createUWSBackupManager(new UWSService(factory, new LocalUWSFileManager(new File("."))))); + }catch(Exception ex){ + fail(getPertinentMessage(ex)); + }finally{ + if (connection != null){ + try{ + ((JDBCConnection)connection).getInnerConnection().close(); + connection = null; + }catch(SQLException se){} + } + } + + // Correct Parameters (JNDI CASE): + try{ + TAPFactory factory = new ConfigurableTAPFactory(serviceConnection, validJNDIProp); + connection = factory.getConnection("0"); + assertNotNull(connection); + }catch(Exception ex){ + fail(getPertinentMessage(ex)); + }finally{ + if (connection != null){ + try{ + ((JDBCConnection)connection).getInnerConnection().close(); + connection = null; + }catch(SQLException se){} + } + } + + // Incorrect database access method: + try{ + new ConfigurableServiceConnection(incorrectDBAccessProp); + fail("This MUST have failed because the value of the property '" + KEY_DATABASE_ACCESS + "' is incorrect!"); + }catch(Exception e){ + assertEquals(TAPException.class, e.getClass()); + assertEquals("Unsupported value for the property " + KEY_DATABASE_ACCESS + ": \"foo\"! Allowed values: \"" + VALUE_JNDI + "\" or \"" + VALUE_JDBC + "\".", e.getMessage()); + } + + // Missing database access method: + try{ + new ConfigurableServiceConnection(missingDBAccessProp); + fail("This MUST have failed because the property '" + KEY_DATABASE_ACCESS + "' is missing!"); + }catch(Exception e){ + assertEquals(TAPException.class, e.getClass()); + assertEquals("The property \"" + KEY_DATABASE_ACCESS + "\" is missing! It is required to connect to the database. Two possible values: \"" + VALUE_JDBC + "\" and \"" + VALUE_JNDI + "\".", e.getMessage()); + } + + // Missing JNDI name: + try{ + new ConfigurableServiceConnection(missingDatasourceJNDINameProp); + fail("This MUST have failed because the property '" + KEY_DATASOURCE_JNDI_NAME + "' is missing!"); + }catch(Exception e){ + assertEquals(TAPException.class, e.getClass()); + assertEquals("The property \"" + KEY_DATASOURCE_JNDI_NAME + "\" is missing! Since the choosen database access method is \"" + VALUE_JNDI + "\", this property is required.", e.getMessage()); + } + + // Wrong JNDI name: + try{ + new ConfigurableServiceConnection(wrongDatasourceJNDINameProp); + fail("This MUST have failed because the value of the property '" + KEY_DATASOURCE_JNDI_NAME + "' is incorrect!"); + }catch(Exception e){ + assertEquals(TAPException.class, e.getClass()); + assertEquals("No datasource found with the JNDI name \"foo\"!", e.getMessage()); + } + + // No JDBC Driver but the database type is known: + try{ + new ConfigurableTAPFactory(serviceConnection, noJdbcProp1); + }catch(Exception ex){ + fail(getPertinentMessage(ex)); + } + + // No JDBC Driver but the database type is UNKNOWN: + try{ + new ConfigurableTAPFactory(serviceConnection, noJdbcProp2); + fail("This MUST have failed because no JDBC Driver has been successfully guessed from the database type!"); + }catch(Exception ex){ + assertEquals(TAPException.class, ex.getClass()); + assertTrue(ex.getMessage().matches("No JDBC driver known for the DBMS \"[^\\\"]*\"!")); + } + + // Missing JDBC URL: + try{ + new ConfigurableTAPFactory(serviceConnection, noJdbcProp3); + fail("This MUST have failed because the property \"" + KEY_JDBC_URL + "\" is missing!"); + }catch(Exception ex){ + assertEquals(TAPException.class, ex.getClass()); + assertTrue(ex.getMessage().matches("The property \"" + KEY_JDBC_URL + "\" is missing! Since the choosen database access method is \"" + VALUE_JDBC + "\", this property is required.")); + } + + // Bad JDBC Driver: + try{ + new ConfigurableTAPFactory(serviceConnection, badJdbcProp); + fail("This MUST have failed because the provided JDBC Driver doesn't exist!"); + }catch(Exception ex){ + assertEquals(DBException.class, ex.getClass()); + assertTrue(ex.getMessage().matches("Impossible to find the JDBC driver \"[^\\\"]*\" !")); + } + + // Missing Translator: + try{ + new ConfigurableTAPFactory(serviceConnection, missingTranslatorProp); + fail("This MUST have failed because the provided SQL translator is missing!"); + }catch(Exception ex){ + assertEquals(TAPException.class, ex.getClass()); + assertTrue(ex.getMessage().matches("The property \"" + KEY_SQL_TRANSLATOR + "\" is missing! ADQL queries can not be translated without it. Allowed values: \"" + VALUE_POSTGRESQL + "\", \"" + VALUE_PGSPHERE + "\" or a class path of a class implementing SQLTranslator.")); + } + + // Bad Translator: + try{ + new ConfigurableTAPFactory(serviceConnection, badTranslatorProp); + fail("This MUST have failed because the provided SQL translator is incorrect!"); + }catch(Exception ex){ + assertEquals(TAPException.class, ex.getClass()); + assertTrue(ex.getMessage().matches("Unsupported value for the property sql_translator: \"[^\\\"]*\" !")); + } + + // Bad DB Name: + try{ + new ConfigurableTAPFactory(serviceConnection, badDBNameProp); + fail("This MUST have failed because the provided database name is incorrect!"); + }catch(Exception ex){ + assertEquals(DBException.class, ex.getClass()); + assertTrue(ex.getMessage().matches("Impossible to establish a connection to the database \"[^\\\"]*\"!")); + assertEquals(PSQLException.class, ex.getCause().getClass()); + assertTrue(ex.getCause().getMessage().matches("FATAL: password authentication failed for user \"[^\\\"]*\"")); + } + + // Bad DB Username: ABORTED BECAUSE THE BAD USERNAME IS NOT DETECTED FOR THE DB WHICH HAS THE SAME NAME AS THE USERNAME ! + try{ + new ConfigurableTAPFactory(serviceConnection, badUsernameProp); + fail("This MUST have failed because the provided database username is incorrect!"); + }catch(Exception ex){ + assertEquals(DBException.class, ex.getClass()); + assertTrue(ex.getMessage().matches("Impossible to establish a connection to the database \"[^\\\"]*\"!")); + assertEquals(PSQLException.class, ex.getCause().getClass()); + assertTrue(ex.getCause().getMessage().matches("FATAL: password authentication failed for user \"[^\\\"]*\"")); + } + + // Bad DB Password: + try{ + new ConfigurableTAPFactory(serviceConnection, badPasswordProp); + //fail("This MUST have failed because the provided database password is incorrect!"); // NOTE: In function of the database configuration, a password may be required or not. So this test is not automatic! + }catch(Exception ex){ + assertEquals(DBException.class, ex.getClass()); + assertTrue(ex.getMessage().matches("Impossible to establish a connection to the database \"[^\\\"]*\"!")); + assertEquals(PSQLException.class, ex.getCause().getClass()); + assertTrue(ex.getCause().getMessage().matches("FATAL: password authentication failed for user \"[^\\\"]*\"")); + } + + // Valid backup frequency: + try{ + ConfigurableTAPFactory factory = new ConfigurableTAPFactory(serviceConnection, validBackupFrequency); + DefaultTAPBackupManager backupManager = (DefaultTAPBackupManager)factory.createUWSBackupManager(new UWSService(factory, new LocalUWSFileManager(new File("/tmp")))); + assertEquals(3600L, backupManager.getBackupFreq()); + }catch(Exception ex){ + fail(getPertinentMessage(ex)); + } + + // No backup: + try{ + ConfigurableTAPFactory factory = new ConfigurableTAPFactory(serviceConnection, noBackup); + assertNull(factory.createUWSBackupManager(new UWSService(factory, new LocalUWSFileManager(new File("/tmp"))))); + }catch(Exception ex){ + fail(getPertinentMessage(ex)); + } + + // User backup: + try{ + UWSService uws; + UserIdentifier userIdent = new UserIdentifier(){ + private static final long serialVersionUID = 1L; + + @Override + public JobOwner restoreUser(String id, String pseudo, Map otherData) throws UWSException{ + return null; + } + + @Override + public JobOwner extractUserId(UWSUrl urlInterpreter, HttpServletRequest request) throws UWSException{ + return null; + } + }; + /* The value user_action has no effect if the by_user mode is not enabled. + * So, if this value is given, it's falling back to manual.*/ + userBackup.setProperty(KEY_BACKUP_BY_USER, "false"); + ConfigurableTAPFactory factory = new ConfigurableTAPFactory(serviceConnection, userBackup); + uws = new UWSService(factory, new LocalUWSFileManager(new File("/tmp"))); + DefaultTAPBackupManager backupManager = (DefaultTAPBackupManager)factory.createUWSBackupManager(uws); + assertEquals(DefaultTAPBackupManager.MANUAL, backupManager.getBackupFreq()); + + /* After having enabled the by_user mode, it should now work. */ + userBackup.setProperty(KEY_BACKUP_BY_USER, "true"); + factory = new ConfigurableTAPFactory(serviceConnection, userBackup); + uws = new UWSService(factory, new LocalUWSFileManager(new File("/tmp"))); + uws.setUserIdentifier(userIdent); + backupManager = (DefaultTAPBackupManager)factory.createUWSBackupManager(uws); + assertEquals(DefaultTAPBackupManager.AT_USER_ACTION, backupManager.getBackupFreq()); + }catch(Exception ex){ + fail(getPertinentMessage(ex)); + } + + // Bad backup frequency: + try{ + new ConfigurableTAPFactory(serviceConnection, badBackupFrequency); + }catch(Exception ex){ + assertEquals(TAPException.class, ex.getClass()); + assertEquals("Long expected for the property \"" + KEY_BACKUP_FREQUENCY + "\", instead of: \"foo\"!", ex.getMessage()); + } + } + + public static final String getPertinentMessage(final Exception ex){ + return (ex.getCause() == null || ex.getMessage().equals(ex.getCause().getMessage())) ? ex.getMessage() : ex.getCause().getMessage(); + } + + public static class ServiceConnectionTest implements ServiceConnection { + + private TAPLog logger = new DefaultTAPLog((UWSFileManager)null); + private boolean isAvailable = true; + + @Override + public String getProviderName(){ + return null; + } + + @Override + public String getProviderDescription(){ + return null; + } + + @Override + public boolean isAvailable(){ + return isAvailable; + } + + @Override + public String getAvailability(){ + return null; + } + + @Override + public int[] getRetentionPeriod(){ + return null; + } + + @Override + public int[] getExecutionDuration(){ + return null; + } + + @Override + public int[] getOutputLimit(){ + return null; + } + + @Override + public tap.ServiceConnection.LimitUnit[] getOutputLimitType(){ + return null; + } + + @Override + public UserIdentifier getUserIdentifier(){ + return null; + } + + @Override + public boolean uploadEnabled(){ + return false; + } + + @Override + public int[] getUploadLimit(){ + return null; + } + + @Override + public tap.ServiceConnection.LimitUnit[] getUploadLimitType(){ + return null; + } + + @Override + public int getMaxUploadSize(){ + return 0; + } + + @Override + public TAPMetadata getTAPMetadata(){ + return null; + } + + @Override + public Collection getCoordinateSystems(){ + return null; + } + + @Override + public TAPLog getLogger(){ + return logger; + } + + @Override + public TAPFactory getFactory(){ + return null; + } + + @Override + public UWSFileManager getFileManager(){ + return null; + } + + @Override + public Iterator getOutputFormats(){ + return null; + } + + @Override + public OutputFormat getOutputFormat(String mimeOrAlias){ + return null; + } + + @Override + public void setAvailable(boolean isAvailable, String message){ + this.isAvailable = isAvailable; + } + + @Override + public Collection getGeometries(){ + return null; + } + + @Override + public Collection getUDFs(){ + return null; + } + + @Override + public int getNbMaxAsyncJobs(){ + return -1; + } + + @Override + public int[] getFetchSize(){ + return null; + } + } + +} diff --git a/test/tap/config/TestTAPConfiguration.java b/test/tap/config/TestTAPConfiguration.java new file mode 100644 index 0000000..61a4104 --- /dev/null +++ b/test/tap/config/TestTAPConfiguration.java @@ -0,0 +1,416 @@ +package tap.config; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static tap.config.TAPConfiguration.KEY_DEFAULT_OUTPUT_LIMIT; +import static tap.config.TAPConfiguration.KEY_FILE_MANAGER; +import static tap.config.TAPConfiguration.KEY_MAX_OUTPUT_LIMIT; +import static tap.config.TAPConfiguration.fetchClass; +import static tap.config.TAPConfiguration.isClassName; +import static tap.config.TAPConfiguration.newInstance; +import static tap.config.TAPConfiguration.parseLimit; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +import org.junit.Before; +import org.junit.Test; + +import tap.ServiceConnection.LimitUnit; +import tap.TAPException; +import tap.metadata.TAPMetadata; +import tap.metadata.TAPSchema; +import adql.query.ColumnReference; + +public class TestTAPConfiguration { + + @Before + public void setUp() throws Exception{} + + /** + * TEST isClassName(String): + * - null, "", "{}", "an incorrect syntax" => FALSE must be returned + * - "{ }", "{ }", "{class.path}", "{ class.path }" => TRUE must be returned + * + * @see ConfigurableServiceConnection#isClassName(String) + */ + @Test + public void testIsClassPath(){ + // NULL and EMPTY: + assertFalse(isClassName(null)); + assertFalse(isClassName("")); + + // EMPTY CLASSPATH: + assertFalse(isClassName("{}")); + + // INCORRECT CLASSPATH: + assertFalse(isClassName("incorrect class name ; missing {}")); + + // VALID CLASSPATH: + assertTrue(isClassName("{class.path}")); + + // CLASSPATH VALID ONLY IN THE SYNTAX: + assertTrue(isClassName("{ }")); + assertTrue(isClassName("{ }")); + + // NOT TRIM CLASSPATH: + assertTrue(isClassName("{ class.name }")); + } + + /** + * TEST getClass(String,String,String): + * - null, "", "{}", "an incorrect syntax", "{ }", "{ }" => NULL must be returned + * - "{java.lang.String}", "{ java.lang.String }" => a valid DefaultServiceConnection must be returned + * - "{mypackage.foo}", "{java.util.ArrayList}" (while a String is expected) => a TAPException must be thrown + */ + @Test + public void testGetClassStringStringString(){ + // NULL and EMPTY: + try{ + assertNull(fetchClass(null, KEY_FILE_MANAGER, String.class)); + }catch(TAPException e){ + fail("If a NULL value is provided as class name: getClass(...) MUST return null!\nCaught exception: " + getPertinentMessage(e)); + } + try{ + assertNull(fetchClass("", KEY_FILE_MANAGER, String.class)); + }catch(TAPException e){ + fail("If an EMPTY value is provided as class name: getClass(...) MUST return null!\nCaught exception: " + getPertinentMessage(e)); + } + + // EMPTY CLASS NAME: + try{ + assertNull(fetchClass("{}", KEY_FILE_MANAGER, String.class)); + }catch(TAPException e){ + fail("If an EMPTY class name is provided: getClass(...) MUST return null!\nCaught exception: " + getPertinentMessage(e)); + } + + // INCORRECT SYNTAX: + try{ + assertNull(fetchClass("incorrect class name ; missing {}", KEY_FILE_MANAGER, String.class)); + }catch(TAPException e){ + fail("If an incorrect class name is provided: getClass(...) MUST return null!\nCaught exception: " + getPertinentMessage(e)); + } + + // VALID CLASS NAME: + try{ + Class classObject = fetchClass("{java.lang.String}", KEY_FILE_MANAGER, String.class); + assertNotNull(classObject); + assertEquals(classObject.getName(), "java.lang.String"); + }catch(TAPException e){ + fail("If a VALID class name is provided: getClass(...) MUST return a Class object of the wanted type!\nCaught exception: " + getPertinentMessage(e)); + } + + // INCORRECT CLASS NAME: + try{ + fetchClass("{mypackage.foo}", KEY_FILE_MANAGER, String.class); + fail("This MUST have failed because an incorrect class name is provided!"); + }catch(TAPException e){ + assertEquals(e.getClass(), TAPException.class); + assertEquals(e.getMessage(), "The class specified by the property \"" + KEY_FILE_MANAGER + "\" ({mypackage.foo}) can not be found."); + } + + // INCOMPATIBLE TYPES: + try{ + @SuppressWarnings("unused") + Class classObject = fetchClass("{java.util.ArrayList}", KEY_FILE_MANAGER, String.class); + fail("This MUST have failed because a class of a different type has been asked!"); + }catch(TAPException e){ + assertEquals(e.getClass(), TAPException.class); + assertEquals(e.getMessage(), "The class specified by the property \"" + KEY_FILE_MANAGER + "\" ({java.util.ArrayList}) is not implementing " + String.class.getName() + "."); + } + + // CLASS NAME VALID ONLY IN THE SYNTAX: + try{ + assertNull(fetchClass("{ }", KEY_FILE_MANAGER, String.class)); + }catch(TAPException e){ + fail("If an EMPTY class name is provided: getClass(...) MUST return null!\nCaught exception: " + getPertinentMessage(e)); + } + try{ + assertNull(fetchClass("{ }", KEY_FILE_MANAGER, String.class)); + }catch(TAPException e){ + fail("If an EMPTY class name is provided: getClass(...) MUST return null!\nCaught exception: " + getPertinentMessage(e)); + } + + // NOT TRIM CLASS NAME: + try{ + Class classObject = fetchClass("{ java.lang.String }", KEY_FILE_MANAGER, String.class); + assertNotNull(classObject); + assertEquals(classObject.getName(), "java.lang.String"); + }catch(TAPException e){ + fail("If a VALID class name is provided: getClass(...) MUST return a Class object of the wanted type!\nCaught exception: " + getPertinentMessage(e)); + } + } + + @Test + public void testNewInstance(){ + // VALID CONSTRUCTOR with no parameters: + try{ + TAPMetadata metadata = newInstance("{tap.metadata.TAPMetadata}", "metadata", TAPMetadata.class); + assertNotNull(metadata); + assertEquals("tap.metadata.TAPMetadata", metadata.getClass().getName()); + }catch(Exception ex){ + ex.printStackTrace(); + fail("This test should have succeeded: the parameters of newInstance(...) are all valid."); + } + + // VALID CONSTRUCTOR with some parameters: + try{ + final String schemaName = "MySuperSchema", description = "And its less super description.", utype = "UTYPE"; + TAPSchema schema = newInstance("{tap.metadata.TAPSchema}", "schema", TAPSchema.class, new Class[]{String.class,String.class,String.class}, new String[]{schemaName,description,utype}); + assertNotNull(schema); + assertEquals("tap.metadata.TAPSchema", schema.getClass().getName()); + assertEquals(schemaName, schema.getADQLName()); + assertEquals(description, schema.getDescription()); + assertEquals(utype, schema.getUtype()); + }catch(Exception ex){ + ex.printStackTrace(); + fail("This test should have succeeded: the constructor TAPSchema(String,String,String) exists."); + } + + // VALID CONSTRUCTOR with some parameters whose the type is an extension (not the exact type): + OutputStream output = null; + File tmp = new File("tmp.empty"); + try{ + output = newInstance("{java.io.BufferedOutputStream}", "stream", OutputStream.class, new Class[]{OutputStream.class}, new OutputStream[]{new FileOutputStream(tmp)}); + assertNotNull(output); + assertEquals(BufferedOutputStream.class, output.getClass()); + }catch(Exception ex){ + ex.printStackTrace(); + fail("This test should have succeeded: the constructor TAPSchema(String,String,String) exists."); + }finally{ + try{ + tmp.delete(); + if (output != null) + output.close(); + }catch(IOException ioe){} + } + + // NOT A CLASS NAME: + try{ + newInstance("tap.metadata.TAPMetadata", "metadata", TAPMetadata.class); + fail("This MUST have failed because the property value is not a class name!"); + }catch(Exception ex){ + assertEquals(TAPException.class, ex.getClass()); + assertEquals("Class name expected for the property \"metadata\" instead of: \"tap.metadata.TAPMetadata\"! The specified class must extend/implement tap.metadata.TAPMetadata.", ex.getMessage()); + } + + // NO MATCHING CONSTRUCTOR: + try{ + newInstance("{tap.metadata.TAPSchema}", "schema", TAPSchema.class, new Class[]{Integer.class}, new Object[]{new Integer(123)}); + fail("This MUST have failed because the specified class does not have any expected constructor!"); + }catch(Exception ex){ + assertEquals(TAPException.class, ex.getClass()); + assertEquals("Missing constructor tap.metadata.TAPSchema(java.lang.Integer)! See the value \"{tap.metadata.TAPSchema}\" of the property \"schema\".", ex.getMessage()); + } + + // VALID CONSTRUCTOR with primitive type: + try{ + ColumnReference colRef = newInstance("{adql.query.ColumnReference}", "colRef", ColumnReference.class, new Class[]{int.class}, new Object[]{123}); + assertNotNull(colRef); + assertEquals(ColumnReference.class, colRef.getClass()); + assertEquals(123, colRef.getColumnIndex()); + colRef = newInstance("{adql.query.ColumnReference}", "colRef", ColumnReference.class, new Class[]{int.class}, new Object[]{new Integer(123)}); + assertNotNull(colRef); + assertEquals(ColumnReference.class, colRef.getClass()); + assertEquals(123, colRef.getColumnIndex()); + }catch(Exception ex){ + ex.printStackTrace(); + fail("This test should have succeeded: the constructor ColumnReference(int) exists."); + } + + // WRONG CONSTRUCTOR with primitive type: + try{ + newInstance("{adql.query.ColumnReference}", "colRef", ColumnReference.class, new Class[]{Integer.class}, new Object[]{new Integer(123)}); + fail("This MUST have failed because the constructor of the specified class expects an int, not an java.lang.Integer!"); + }catch(Exception ex){ + assertEquals(TAPException.class, ex.getClass()); + assertEquals("Missing constructor adql.query.ColumnReference(java.lang.Integer)! See the value \"{adql.query.ColumnReference}\" of the property \"colRef\".", ex.getMessage()); + } + + // THE CONSTRUCTOR THROWS AN EXCEPTION: + try{ + newInstance("{tap.metadata.TAPSchema}", "schema", TAPSchema.class, new Class[]{String.class}, new Object[]{null}); + fail("This MUST have failed because the constructor of the specified class throws an exception!"); + }catch(Exception ex){ + assertEquals(TAPException.class, ex.getClass()); + assertNotNull(ex.getCause()); + assertEquals(NullPointerException.class, ex.getCause().getClass()); + assertEquals("Missing schema name!", ex.getCause().getMessage()); + } + + // THE CONSTRUCTOR THROWS A TAPEXCEPTION: + try{ + newInstance("{tap.config.TestTAPConfiguration$ClassAlwaysThrowTAPError}", "tapError", ClassAlwaysThrowTAPError.class); + fail("This MUST have failed because the constructor of the specified class throws a TAPException!"); + }catch(Exception ex){ + assertEquals(TAPException.class, ex.getClass()); + assertEquals("This error is always thrown by ClassAlwaysThrowTAPError ^^", ex.getMessage()); + } + } + + /** + * TEST parseLimit(String,String): + * - nothing, -123, 0 => {-1,LimitUnit.rows} + * - 20, 20r, 20R => {20,LimitUnit.rows} + * - 100B, 100 B => {100,LimitUnit.bytes} + * - 100kB, 100 k B => {100000,LimitUnit.bytes} + * - 100MB, 1 0 0MB => {100000000,LimitUnit.bytes} + * - 100GB, 1 0 0 G B => {100000000000,LimitUnit.bytes} + * - r => {-1,LimitUnit.rows} + * - kB => {-1,LimitUnit.bytes} + * - foo, 100b, 100TB, 1foo => an exception must occur + */ + @Test + public void testParseLimitStringString(){ + final String propertyName = KEY_DEFAULT_OUTPUT_LIMIT + " or " + KEY_MAX_OUTPUT_LIMIT; + // Test empty or negative or null values => OK! + try{ + String[] testValues = new String[]{null,""," ","-123"}; + Object[] limit; + for(String v : testValues){ + limit = parseLimit(v, propertyName, false); + assertEquals(limit[0], -1); + assertEquals(limit[1], LimitUnit.rows); + } + // 0 test: + limit = parseLimit("0", propertyName, false); + assertEquals(limit[0], 0); + assertEquals(limit[1], LimitUnit.rows); + }catch(TAPException te){ + fail("All these empty limit values are valid, so these tests should have succeeded!\nCaught exception: " + getPertinentMessage(te)); + } + + // Test all accepted rows values: + try{ + String[] testValues = new String[]{"20","20r","20 R"}; + Object[] limit; + for(String v : testValues){ + limit = parseLimit(v, propertyName, false); + assertEquals(limit[0], 20); + assertEquals(limit[1], LimitUnit.rows); + } + }catch(TAPException te){ + fail("All these rows limit values are valid, so these tests should have succeeded!\nCaught exception: " + getPertinentMessage(te)); + } + + // Test all accepted bytes values: + try{ + String[] testValues = new String[]{"100B","100 B"}; + Object[] limit; + for(String v : testValues){ + limit = parseLimit(v, propertyName, true); + assertEquals(limit[0], 100); + assertEquals(limit[1], LimitUnit.bytes); + } + }catch(TAPException te){ + fail("All these bytes limit values are valid, so these tests should have succeeded!\nCaught exception: " + getPertinentMessage(te)); + } + + // Test all accepted kilo-bytes values: + try{ + String[] testValues = new String[]{"100kB","100 k B"}; + Object[] limit; + for(String v : testValues){ + limit = parseLimit(v, propertyName, true); + assertEquals(limit[0], 100); + assertEquals(limit[1], LimitUnit.kilobytes); + } + }catch(TAPException te){ + fail("All these kilo-bytes limit values are valid, so these tests should have succeeded!\nCaught exception: " + getPertinentMessage(te)); + } + + // Test all accepted mega-bytes values: + try{ + String[] testValues = new String[]{"100MB","1 0 0MB"}; + Object[] limit; + for(String v : testValues){ + limit = parseLimit(v, propertyName, true); + assertEquals(limit[0], 100); + assertEquals(limit[1], LimitUnit.megabytes); + } + }catch(TAPException te){ + fail("All these mega-bytes limit values are valid, so these tests should have succeeded!\nCaught exception: " + getPertinentMessage(te)); + } + + // Test all accepted giga-bytes values: + try{ + String[] testValues = new String[]{"100GB","1 0 0 G B"}; + Object[] limit; + for(String v : testValues){ + limit = parseLimit(v, propertyName, true); + assertEquals(limit[0], 100); + assertEquals(limit[1], LimitUnit.gigabytes); + } + }catch(TAPException te){ + fail("All these giga-bytes limit values are valid, so these tests should have succeeded!\nCaught exception: " + getPertinentMessage(te)); + } + + // Test with only the ROWS unit provided: + try{ + Object[] limit = parseLimit("r", propertyName, false); + assertEquals(limit[0], -1); + assertEquals(limit[1], LimitUnit.rows); + }catch(TAPException te){ + fail("Providing only the ROWS unit is valid, so this test should have succeeded!\nCaught exception: " + getPertinentMessage(te)); + } + + // Test with only the BYTES unit provided: + try{ + Object[] limit = parseLimit("kB", propertyName, true); + assertEquals(limit[0], -1); + assertEquals(limit[1], LimitUnit.kilobytes); + }catch(TAPException te){ + fail("Providing only the BYTES unit is valid, so this test should have succeeded!\nCaught exception: " + getPertinentMessage(te)); + } + + // Test with incorrect limit formats: + String[] values = new String[]{"","100","100","1"}; + String[] unitPart = new String[]{"foo","b","TB","foo"}; + for(int i = 0; i < values.length; i++){ + try{ + parseLimit(values[i] + unitPart[i], propertyName, true); + fail("This test should have failed because an incorrect limit is provided: \"" + values[i] + unitPart[i] + "\"!"); + }catch(TAPException te){ + assertEquals(te.getClass(), TAPException.class); + assertEquals(te.getMessage(), "Unknown limit unit (" + unitPart[i] + ") for the property " + propertyName + ": \"" + values[i] + unitPart[i] + "\"!"); + + } + } + // Test with an incorrect numeric limit value: + try{ + parseLimit("abc100b", propertyName, true); + fail("This test should have failed because an incorrect limit is provided: \"abc100b\"!"); + }catch(TAPException te){ + assertEquals(te.getClass(), TAPException.class); + assertEquals(te.getMessage(), "Integer expected for the property " + propertyName + " for the substring \"abc100\" of the whole value: \"abc100b\"!"); + } + + // Test with a BYTES unit whereas the BYTES unit is forbidden: + try{ + parseLimit("100B", propertyName, false); + fail("This test should have failed because an incorrect limit is provided: \"100B\"!"); + }catch(TAPException te){ + assertEquals(te.getClass(), TAPException.class); + assertEquals(te.getMessage(), "BYTES unit is not allowed for the property " + propertyName + " (100B)!"); + } + } + + public static final String getPertinentMessage(final Exception ex){ + return (ex.getCause() == null || ex.getMessage().equals(ex.getCause().getMessage())) ? ex.getMessage() : ex.getCause().getMessage(); + } + + private static class ClassAlwaysThrowTAPError { + @SuppressWarnings("unused") + public ClassAlwaysThrowTAPError() throws TAPException{ + throw new TAPException("This error is always thrown by ClassAlwaysThrowTAPError ^^"); + } + } + +} diff --git a/test/tap/data/ResultSetTableIteratorTest.java b/test/tap/data/ResultSetTableIteratorTest.java new file mode 100644 index 0000000..bc78917 --- /dev/null +++ b/test/tap/data/ResultSetTableIteratorTest.java @@ -0,0 +1,127 @@ +package tap.data; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.sql.Connection; +import java.sql.ResultSet; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import testtools.DBTools; + +public class ResultSetTableIteratorTest { + + private static Connection conn; + + @BeforeClass + public static void setUpBeforeClass() throws Exception{ + conn = DBTools.createConnection("postgresql", "127.0.0.1", null, "gmantele", "gmantele", "pwd"); + } + + @AfterClass + public static void tearDownAfterClass() throws Exception{ + DBTools.closeConnection(conn); + } + + @Test + public void testWithRSNULL(){ + try{ + new ResultSetTableIterator(null); + fail("The constructor should have failed, because: the given ResultSet is NULL."); + }catch(Exception ex){ + assertEquals("java.lang.NullPointerException", ex.getClass().getName()); + assertEquals("Missing ResultSet object over which to iterate!", ex.getMessage()); + } + } + + @Test + public void testWithData(){ + TableIterator it = null; + try{ + ResultSet rs = DBTools.select(conn, "SELECT id, ra, deg, gmag FROM gums LIMIT 10;"); + + it = new ResultSetTableIterator(rs); + // TEST there is column metadata before starting the iteration: + assertTrue(it.getMetadata() != null); + final int expectedNbLines = 10, expectedNbColumns = 4; + int countLines = 0, countColumns = 0; + while(it.nextRow()){ + // count lines: + countLines++; + // reset columns count: + countColumns = 0; + while(it.hasNextCol()){ + it.nextCol(); + // count columns + countColumns++; + // TEST the column type is set (not null): + assertTrue(it.getColType() != null); + } + // TEST that all columns have been read: + assertEquals(expectedNbColumns, countColumns); + } + // TEST that all lines have been read: + assertEquals(expectedNbLines, countLines); + + }catch(Exception ex){ + ex.printStackTrace(System.err); + fail("An exception occurs while reading a correct ResultSet (containing some valid rows)."); + }finally{ + if (it != null){ + try{ + it.close(); + }catch(DataReadException dre){} + } + } + } + + @Test + public void testWithEmptySet(){ + TableIterator it = null; + try{ + ResultSet rs = DBTools.select(conn, "SELECT * FROM gums WHERE id = 'foo';"); + + it = new ResultSetTableIterator(rs); + // TEST there is column metadata before starting the iteration: + assertTrue(it.getMetadata() != null); + int countLines = 0; + // count lines: + while(it.nextRow()) + countLines++; + // TEST that no line has been read: + assertEquals(countLines, 0); + + }catch(Exception ex){ + ex.printStackTrace(System.err); + fail("An exception occurs while reading a correct ResultSet (containing some valid rows)."); + }finally{ + if (it != null){ + try{ + it.close(); + }catch(DataReadException dre){} + } + } + } + + @Test + public void testWithClosedSet(){ + try{ + // create a valid ResultSet: + ResultSet rs = DBTools.select(conn, "SELECT * FROM gums WHERE id = 'foo';"); + + // close the ResultSet: + rs.close(); + + // TRY to create a TableIterator with a closed ResultSet: + new ResultSetTableIterator(rs); + + fail("The constructor should have failed, because: the given ResultSet is closed."); + }catch(Exception ex){ + assertEquals(ex.getClass().getName(), "tap.data.DataReadException"); + } + } +} diff --git a/test/tap/data/VOTableIteratorTest.java b/test/tap/data/VOTableIteratorTest.java new file mode 100644 index 0000000..1bf1bf0 --- /dev/null +++ b/test/tap/data/VOTableIteratorTest.java @@ -0,0 +1,196 @@ +package tap.data; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.junit.Test; + +public class VOTableIteratorTest { + + public final static String directory = "/home/gmantele/workspace/tap/test/tap/data/"; + + public final static File dataVOTable = new File(directory + "testdata.vot"); + public final static File binaryVOTable = new File(directory + "testdata_binary.vot"); + + public final static File emptyVOTable = new File(directory + "emptyset.vot"); + public final static File emptyBinaryVOTable = new File(directory + "emptyset_binary.vot"); + + @Test + public void testWithNULL(){ + try{ + new VOTableIterator(null); + fail("The constructor should have failed, because: the given VOTable is NULL."); + }catch(Exception ex){ + assertEquals(ex.getClass().getName(), "java.lang.NullPointerException"); + } + } + + @Test + public void testWithData(){ + InputStream input = null; + TableIterator it = null; + try{ + input = new BufferedInputStream(new FileInputStream(dataVOTable)); + it = new VOTableIterator(input); + // TEST there is column metadata before starting the iteration: + assertTrue(it.getMetadata() != null); + final int expectedNbLines = 100, expectedNbColumns = 4; + int countLines = 0, countColumns = 0; + while(it.nextRow()){ + // count lines: + countLines++; + // reset columns count: + countColumns = 0; + while(it.hasNextCol()){ + it.nextCol(); + // count columns + countColumns++; + // TEST the column type is set (not null): + assertTrue(it.getColType() != null); + } + // TEST that all columns have been read: + assertEquals(expectedNbColumns, countColumns); + } + // TEST that all lines have been read: + assertEquals(expectedNbLines, countLines); + + }catch(Exception ex){ + ex.printStackTrace(System.err); + fail("An exception occurs while reading a correct VOTable (containing some valid rows)."); + }finally{ + try{ + if (input != null) + input.close(); + }catch(IOException e){ + e.printStackTrace(); + } + if (it != null){ + try{ + it.close(); + }catch(DataReadException dre){} + } + } + } + + @Test + public void testWithBinary(){ + InputStream input = null; + TableIterator it = null; + try{ + input = new BufferedInputStream(new FileInputStream(binaryVOTable)); + it = new VOTableIterator(input); + // TEST there is column metadata before starting the iteration: + assertTrue(it.getMetadata() != null); + final int expectedNbLines = 100, expectedNbColumns = 4; + int countLines = 0, countColumns = 0; + while(it.nextRow()){ + // count lines: + countLines++; + // reset columns count: + countColumns = 0; + while(it.hasNextCol()){ + it.nextCol(); + // count columns + countColumns++; + // TEST the column type is set (not null): + assertTrue(it.getColType() != null); + } + // TEST that all columns have been read: + assertEquals(expectedNbColumns, countColumns); + } + // TEST that all lines have been read: + assertEquals(expectedNbLines, countLines); + + }catch(Exception ex){ + ex.printStackTrace(System.err); + fail("An exception occurs while reading a correct VOTable (containing some valid rows)."); + }finally{ + try{ + if (input != null) + input.close(); + }catch(IOException e){ + e.printStackTrace(); + } + if (it != null){ + try{ + it.close(); + }catch(DataReadException dre){} + } + } + } + + @Test + public void testWithEmptySet(){ + InputStream input = null; + TableIterator it = null; + try{ + input = new BufferedInputStream(new FileInputStream(emptyVOTable)); + it = new VOTableIterator(input); + // TEST there is column metadata before starting the iteration: + assertTrue(it.getMetadata() != null); + int countLines = 0; + // count lines: + while(it.nextRow()) + countLines++; + // TEST that no line has been read: + assertEquals(countLines, 0); + + }catch(Exception ex){ + ex.printStackTrace(System.err); + fail("An exception occurs while reading a correct VOTable (even if empty)."); + }finally{ + try{ + if (input != null) + input.close(); + }catch(IOException e){ + e.printStackTrace(); + } + if (it != null){ + try{ + it.close(); + }catch(DataReadException dre){} + } + } + } + + @Test + public void testWithEmptyBinarySet(){ + InputStream input = null; + TableIterator it = null; + try{ + input = new BufferedInputStream(new FileInputStream(emptyBinaryVOTable)); + it = new VOTableIterator(input); + // TEST there is column metadata before starting the iteration: + assertTrue(it.getMetadata() != null); + int countLines = 0; + // count lines: + while(it.nextRow()) + countLines++; + // TEST that no line has been read: + assertEquals(countLines, 0); + + }catch(Exception ex){ + ex.printStackTrace(System.err); + fail("An exception occurs while reading a correct binary VOTable (even if empty)."); + }finally{ + try{ + if (input != null) + input.close(); + }catch(IOException e){ + e.printStackTrace(); + } + if (it != null){ + try{ + it.close(); + }catch(DataReadException dre){} + } + } + } +} diff --git a/test/tap/data/emptyset.vot b/test/tap/data/emptyset.vot new file mode 100644 index 0000000..fe9b28f --- /dev/null +++ b/test/tap/data/emptyset.vot @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + +GUMS-10 is the 10th version of the Gaia Universe Model Snapshot, a +simulation of the expected contents of the Gaia cataloge run at the +MareNostrum supercomputer. The models used and the characteristics of +GUMS-10 are described in: A.C. Robin et al, "Gaia Universe Model +Snapshot. A statistical analysis of the expected contents of the Gaia +catalogue", Astronomy & Astrophysics (2012), in press. For more +details see also http://gaia.am.ub.es/GUMS-10/ + + +Supernovae in the GUMS-10 simulated GAIA result set. + + +If you use this data, please acknowledge that GUMS was created using +the MareNostrum supercomputer. + + +Query successful + + +Short name for TAP service + + +TAP service title + + +Unique resource registry identifier + + +Publisher for TAP service + + +Descriptive URL for search resource + + +Individual to contact about this service + + +Intrinsic apparent V magnitude + + +Intrinsic V-I color. + + +Mean absolute V magnitude. + + +Object redshift. + + +Right ascention of the barycenter at J2010 reference epoch in the ICRS frame + + +Declination of the barycenter at J2010 reference epoch in the ICRS frame + + +Distance from the barycenter of the Solar System to the barycenter of the source at J2010 reference epoch + + +Proper motion along right ascention at J2010 reference epoch + + +Proper motion along declination at J2010 reference epoch + + +Radial Velocity at J2010 reference epoch + + +Interstellar absorption in the G band assuming the extinction law of 1989ApJ...345..245C. + + +Interstellar absorption in the V-band assuming the extinction law of 1989ApJ...345..245C. + + +Extinction parameter according to 2003A&A...409..205D. + + +GAIA G band apparent magnitude at reference epoch. The GAIA G-band has a wide bandpass between 350 and 150 nm. This is close to Johnson V for V-I between -0.4 and 1.4. + + +GAIA G_BP band apparent magnitude at reference epoch. The GAIA G_BP band has a bandpass between 350 and 770 nm. + + +GAIA G_RP band apparent magnitude at reference epoch. The GAIA G_RP band has a bandpass between 650 and 1050 nm. + + +GAIA G_RVS band apparent magnitude at reference epoch. The GAIA G_RVS band has a narrow bandpass between 850 and 880 nm. + + +Supernova type (one of Ia, Ib/c, II-L, II-P) + + +GUMS source identifier + + + +GUMS extended source identifier + + + + + +
    +
    +
    diff --git a/test/tap/data/emptyset_binary.vot b/test/tap/data/emptyset_binary.vot new file mode 100644 index 0000000..e123464 --- /dev/null +++ b/test/tap/data/emptyset_binary.vot @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + +GUMS-10 is the 10th version of the Gaia Universe Model Snapshot, a +simulation of the expected contents of the Gaia cataloge run at the +MareNostrum supercomputer. The models used and the characteristics of +GUMS-10 are described in: A.C. Robin et al, "Gaia Universe Model +Snapshot. A statistical analysis of the expected contents of the Gaia +catalogue", Astronomy & Astrophysics (2012), in press. For more +details see also http://gaia.am.ub.es/GUMS-10/ + + +Supernovae in the GUMS-10 simulated GAIA result set. + + +If you use this data, please acknowledge that GUMS was created using +the MareNostrum supercomputer. + + +Query successful + + +Short name for TAP service + + +TAP service title + + +Unique resource registry identifier + + +Publisher for TAP service + + +Descriptive URL for search resource + + +Individual to contact about this service + + +Intrinsic apparent V magnitude + + +Intrinsic V-I color. + + +Mean absolute V magnitude. + + +Object redshift. + + +Right ascention of the barycenter at J2010 reference epoch in the ICRS frame + + +Declination of the barycenter at J2010 reference epoch in the ICRS frame + + +Distance from the barycenter of the Solar System to the barycenter of the source at J2010 reference epoch + + +Proper motion along right ascention at J2010 reference epoch + + +Proper motion along declination at J2010 reference epoch + + +Radial Velocity at J2010 reference epoch + + +Interstellar absorption in the G band assuming the extinction law of 1989ApJ...345..245C. + + +Interstellar absorption in the V-band assuming the extinction law of 1989ApJ...345..245C. + + +Extinction parameter according to 2003A&A...409..205D. + + +GAIA G band apparent magnitude at reference epoch. The GAIA G-band has a wide bandpass between 350 and 150 nm. This is close to Johnson V for V-I between -0.4 and 1.4. + + +GAIA G_BP band apparent magnitude at reference epoch. The GAIA G_BP band has a bandpass between 350 and 770 nm. + + +GAIA G_RP band apparent magnitude at reference epoch. The GAIA G_RP band has a bandpass between 650 and 1050 nm. + + +GAIA G_RVS band apparent magnitude at reference epoch. The GAIA G_RVS band has a narrow bandpass between 850 and 880 nm. + + +Supernova type (one of Ia, Ib/c, II-L, II-P) + + +GUMS source identifier + + + +GUMS extended source identifier + + + + + + + +
    +
    +
    diff --git a/test/tap/data/testdata.vot b/test/tap/data/testdata.vot new file mode 100644 index 0000000..bfec957 --- /dev/null +++ b/test/tap/data/testdata.vot @@ -0,0 +1,676 @@ + + + + + + + + + + + + + + + +GUMS-10 is the 10th version of the Gaia Universe Model Snapshot, a +simulation of the expected contents of the Gaia cataloge run at the +MareNostrum supercomputer. The models used and the characteristics of +GUMS-10 are described in: A.C. Robin et al, "Gaia Universe Model +Snapshot. A statistical analysis of the expected contents of the Gaia +catalogue", Astronomy & Astrophysics (2012), in press. For more +details see also http://gaia.am.ub.es/GUMS-10/ + + +Supernovae in the GUMS-10 simulated GAIA result set. + + +If you use this data, please acknowledge that GUMS was created using +the MareNostrum supercomputer. + + +Query successful + + + +Short name for TAP service + + +TAP service title + + +Unique resource registry identifier + + +Publisher for TAP service + + +Descriptive URL for search resource + + +Individual to contact about this service + + +GUMS source identifier + + + +Right ascention of the barycenter at J2010 reference epoch in the ICRS frame + + +Declination of the barycenter at J2010 reference epoch in the ICRS frame + + +GAIA G band apparent magnitude at reference epoch. The GAIA G-band has a wide bandpass between 350 and 150 nm. This is close to Johnson V for V-I between -0.4 and 1.4. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    5227706337957249025315.19628886221935.83480695019760.0
    5228535898005569537316.14792101640836.64463270384960.0
    5228619830256467969318.33075000654637.14270906949810.0
    5228490199553540097317.27748666080737.03936222834720.0
    5228526638056079361315.53965150215536.83307992230320.0
    5228191235470000129314.54754433643337.03163818919780.0
    5228254186805657601313.51515837991237.45944945117250.0
    5228511485411459073315.1463013300537.304639204680.0
    5228510390194798593315.13872441345637.39393574196660.0
    5228207019474812929315.10509057177937.79676638844290.0
    5228645273642729473316.44559396706837.77246108902850.0
    5228210176275775489314.20230957162237.76220180744210.0
    5228340180640858113313.92694076089238.16812566865320.0
    5228338668812369921313.9007812617838.34683031113870.0
    5228678658423521281318.63831534002737.45990618237210.0
    5228643048849670145319.84261859804437.91205442803580.0
    5228662732684787713317.82238275293137.7301991426550.0
    5228557072194338817320.85113216583738.07016787573650.0
    5228059294074667009322.68360302989638.7328465130210.0
    5227925888095485953320.55388286402838.67896391879460.0
    5227935367088308225320.10129065997939.04618980406040.0
    5227934688483475457320.56765795442239.25691873314240.0
    5228419671895572481316.50119712407138.35429489661480.0
    5228404837078532097317.53040384091939.57113780233830.0
    5228457149780197377319.12995797034538.89036315744430.0
    5228411159270391809318.35016753345639.37397763646560.0
    5228411343953985537318.48229132170239.46828134668950.0
    5224886425639452673319.91533016839940.54746066853770.0
    5226038743890132993310.80914271364737.29816030415020.0
    5225901627059208193310.19529762062438.16829838052550.0
    5228389010124046337314.31127836060639.11323442433410.0
    5228357485064093697313.76548769408739.20102059460550.0
    5228357416344616961313.94825377071739.23413349189520.0
    5228138918473367553311.60021505581438.78295799623780.0
    5225905883371798529309.20906548301338.39229399340940.0
    5225922745413402625308.27318763430838.6234727174390.0
    5228170748475998209311.50480935525939.27369492390310.0
    5228128756580745217310.06011765724139.61509236671960.0
    5228125286247170049310.39197560767739.85674478636780.0
    5224523603982155777310.15254846688140.59333977905030.0
    5228287051895406593315.25419508303840.05401245156790.0
    5228273862050840577316.99935233799340.0275603977330.0
    5225021858843197441318.64447845853740.67794380999240.0
    5225035018622992385317.99146351987140.96774411441940.0
    5225072127140429825316.86744117805641.90577726631510.0
    5225072367658598401317.07590799751841.95974788519520.0
    5224716538208059393313.03338159042441.08621109829320.0
    5224690454871670785311.94845897719841.25306754526540.0
    5225070142865539073317.08355465527842.12593272386970.0
    5224684089730138113314.08916189225741.97082848725330.0
    5224636059110866945314.40032150297542.30877268517340.0
    5224644606095785985313.64570938685942.49715494818610.0
    5225193335412490241315.24405685240942.9063874510960.0
    5228101612387434497323.94674963199938.97748114075510.0
    5228088950823845889322.96576053397639.48163582297340.0
    5228086506987454465322.52464216575139.39134129852950.0
    5228082809020612609323.08652599603839.95690587782030.0
    5228085098238181377323.37897674512639.99099656719130.0
    5227900238550794241325.94764479528739.86226849273860.0
    5224755742669537281327.29207806486440.4735804884630.0
    5224801402466861057326.4156946150241.16750128811960.0
    5224907135971753985321.81458559330740.50171079874820.0
    5224912689364467713322.47466810337640.78764615253130.0
    5224941637444042753320.73832821072940.56299256949480.0
    5224940666781433857320.25089399298441.16341300353940.0
    5224927141929418753321.86354218178441.57292542112980.0
    5224986902104375297325.24623815594842.15657778124230.0
    5225019698474647553323.39678505719741.58803073987690.0
    5224955600382722049322.51780085803741.82376102718470.0
    5224831699166167041323.11849460086942.37503580624870.0
    5224858555596668929324.91811616015642.37525149433180.0
    5224856356573413377324.478481630442.469641158570.0
    5260489398125854721328.50119247274540.71503466153740.0
    5264154912194822145328.84957710811741.06957878151770.0
    5260548449631207425328.80098806444442.06648454407370.0
    5224757619570245633326.10137957423841.8062103705320.0
    5224756915195609089326.47195686454442.01262823122720.0
    5260532828835151873327.43529414076642.39922626729420.0
    5260531072193527809327.19890951608242.74784196361050.0
    5260542758799540225327.88760656793242.62365661885350.0
    5260660973479395329326.07385800842943.35464558283750.0
    5260725440938508289326.55822113875643.41061677113290.0
    5264186024937914369329.48648962038642.65163993874620.0
    5260511212264751105328.3197909565943.16637259658850.0
    5264374758685802497329.51522294269843.99048083699810.0
    5264303453638754305329.43547386359644.07587327077290.0
    5260592159513378817329.12851838638644.26147041132090.0
    5225085673467281409319.12073151292441.55409528884620.0
    5224915403783798785320.79198979350742.22865228533180.0
    5225266367036391425320.76234723673542.46490333148460.0
    5224960535300145153322.5400306215142.24075631011430.0
    5224862219203772417323.33310116147842.54947909462440.0
    5224835594701504513322.06506264050643.13387234898860.0
    5225263253185101825316.39088985420742.74842186980540.0
    5225213848176295937316.47276521481643.4994511765080.0
    5225112469768241153319.65765992345443.44087872498050.0
    5225153065799122945320.38766986108843.87256378887190.0
    5225098042973093889321.97084737119544.27314775868040.0
    5224328779970641921321.40278639325944.54593683196670.0
    5224353149615079425318.91878858993544.38091599121240.0
    +
    +
    diff --git a/test/tap/data/testdata_binary.vot b/test/tap/data/testdata_binary.vot new file mode 100644 index 0000000..829a5b8 --- /dev/null +++ b/test/tap/data/testdata_binary.vot @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + + +GUMS-10 is the 10th version of the Gaia Universe Model Snapshot, a +simulation of the expected contents of the Gaia cataloge run at the +MareNostrum supercomputer. The models used and the characteristics of +GUMS-10 are described in: A.C. Robin et al, "Gaia Universe Model +Snapshot. A statistical analysis of the expected contents of the Gaia +catalogue", Astronomy & Astrophysics (2012), in press. For more +details see also http://gaia.am.ub.es/GUMS-10/ + + +Supernovae in the GUMS-10 simulated GAIA result set. + + +If you use this data, please acknowledge that GUMS was created using +the MareNostrum supercomputer. + + +Query successful + + + +Short name for TAP service + + +TAP service title + + +Unique resource registry identifier + + +Publisher for TAP service + + +Descriptive URL for search resource + + +Individual to contact about this service + + +GUMS source identifier + + + +Right ascention of the barycenter at J2010 reference epoch in the ICRS frame + + +Declination of the barycenter at J2010 reference epoch in the ICRS frame + + +GAIA G band apparent magnitude at reference epoch. The GAIA G-band has a wide bandpass between 350 and 150 nm. This is close to Johnson V for V-I between -0.4 and 1.4. + + + + +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 +W6obV0BGMMHa7mPtAAAAAA== + + + +
    +
    +
    diff --git a/test/tap/db/JDBCConnectionTest.java b/test/tap/db/JDBCConnectionTest.java new file mode 100644 index 0000000..b018612 --- /dev/null +++ b/test/tap/db/JDBCConnectionTest.java @@ -0,0 +1,1094 @@ +package tap.db; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Iterator; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import tap.data.DataReadException; +import tap.data.TableIterator; +import tap.data.VOTableIterator; +import tap.metadata.TAPColumn; +import tap.metadata.TAPForeignKey; +import tap.metadata.TAPMetadata; +import tap.metadata.TAPMetadata.STDSchema; +import tap.metadata.TAPMetadata.STDTable; +import tap.metadata.TAPSchema; +import tap.metadata.TAPTable; +import testtools.DBTools; +import adql.db.DBChecker; +import adql.db.DBColumn; +import adql.db.DBTable; +import adql.db.DBType; +import adql.db.DBType.DBDatatype; +import adql.parser.ADQLParser; +import adql.parser.ParseException; +import adql.query.ADQLQuery; +import adql.query.IdentifierField; +import adql.translator.PostgreSQLTranslator; + +public class JDBCConnectionTest { + + private static Connection pgConnection; + private static JDBCConnection pgJDBCConnection; + private static JDBCConnection sensPgJDBCConnection; + + private static Connection sqliteConnection; + private static JDBCConnection sqliteJDBCConnection; + private static JDBCConnection sensSqliteJDBCConnection; + + private static String uploadExamplePath; + + @BeforeClass + public static void setUpBeforeClass() throws Exception{ + + String projectDir = (new File("")).getAbsolutePath(); + uploadExamplePath = projectDir + "/test/tap/db/upload_example.vot"; + + final String sqliteDbFile = projectDir + "/test/tap/db/TestTAPDb.db"; + + pgConnection = DBTools.createConnection("postgresql", "127.0.0.1", null, "gmantele", "gmantele", "pwd"); + pgJDBCConnection = new JDBCConnection(pgConnection, new PostgreSQLTranslator(false), "POSTGRES", null); + sensPgJDBCConnection = new JDBCConnection(pgConnection, new PostgreSQLTranslator(true, true, true, true), "SensitivePSQL", null); + + sqliteConnection = DBTools.createConnection("sqlite", null, null, sqliteDbFile, null, null); + sqliteJDBCConnection = new JDBCConnection(sqliteConnection, new PostgreSQLTranslator(false), "SQLITE", null); + sensSqliteJDBCConnection = new JDBCConnection(sqliteConnection, new PostgreSQLTranslator(true), "SensitiveSQLite", null); + + } + + @AfterClass + public static void tearDownAfterClass() throws Exception{ + // There should be no difference between a POSTGRESQL connection and a SQLITE one! + JDBCConnection[] connections = new JDBCConnection[]{pgJDBCConnection,sensPgJDBCConnection,sqliteJDBCConnection,sensSqliteJDBCConnection}; + for(JDBCConnection conn : connections){ + dropSchema(STDSchema.TAPSCHEMA.label, conn); + dropSchema(STDSchema.UPLOADSCHEMA.label, conn); + } + pgConnection.close(); + sqliteConnection.close(); + } + + /* ***** */ + /* TESTS */ + /* ***** */ + + @Test + public void testGetTAPSchemaTablesDef(){ + // There should be no difference between a POSTGRESQL connection and a SQLITE one! + JDBCConnection[] connections = new JDBCConnection[]{pgJDBCConnection,sensPgJDBCConnection,sqliteJDBCConnection,sensSqliteJDBCConnection}; + for(JDBCConnection conn : connections){ + TAPMetadata meta = createCustomSchema(); + TAPTable customColumns = meta.getTable(STDSchema.TAPSCHEMA.toString(), STDTable.COLUMNS.toString()); + TAPTable[] tapTables = conn.mergeTAPSchemaDefs(meta); + TAPSchema stdSchema = TAPMetadata.getStdSchema(conn.supportsSchema); + assertEquals(5, tapTables.length); + assertTrue(equals(tapTables[0], stdSchema.getTable(STDTable.SCHEMAS.label))); + assertEquals(customColumns.getSchema(), tapTables[0].getSchema()); + assertTrue(equals(tapTables[1], stdSchema.getTable(STDTable.TABLES.label))); + assertEquals(customColumns.getSchema(), tapTables[1].getSchema()); + assertTrue(equals(tapTables[2], customColumns)); + assertTrue(equals(tapTables[3], stdSchema.getTable(STDTable.KEYS.label))); + assertEquals(customColumns.getSchema(), tapTables[3].getSchema()); + assertTrue(equals(tapTables[4], stdSchema.getTable(STDTable.KEY_COLUMNS.label))); + assertEquals(customColumns.getSchema(), tapTables[4].getSchema()); + } + } + + @Test + public void testSetTAPSchema(){ + // There should be no difference between a POSTGRESQL connection and a SQLITE one! + JDBCConnection[] connections = new JDBCConnection[]{pgJDBCConnection,sensPgJDBCConnection,sqliteJDBCConnection,sensSqliteJDBCConnection}; + for(JDBCConnection conn : connections){ + short cnt = -1; + while(cnt < 1){ + /* NO CUSTOM DEFINITION */ + // Prepare the test: + if (cnt == -1) + dropSchema(STDSchema.TAPSCHEMA.label, conn); + else + createTAPSchema(conn); + // Do the test: + try{ + TAPMetadata meta = new TAPMetadata(); + int[] expectedCounts = getStats(meta); + conn.setTAPSchema(meta); + int[] effectiveCounts = getStats(conn, meta); + for(int i = 0; i < expectedCounts.length; i++) + assertEquals(expectedCounts[i], effectiveCounts[i]); + }catch(DBException dbe){ + dbe.printStackTrace(System.err); + fail("[" + conn.getID() + ";no def] No error should happen here ; when an empty list of metadata is given, at least the TAP_SCHEMA should be created and filled with a description of itself."); + } + + /* CUSTOM DEFINITION */ + // Prepare the test: + if (cnt == -1) + dropSchema(STDSchema.TAPSCHEMA.label, conn); + // Do the test: + try{ + TAPMetadata meta = createCustomSchema(); + int[] expectedCounts = getStats(meta); + conn.setTAPSchema(meta); + int[] effectiveCounts = getStats(conn, meta); + for(int i = 0; i < expectedCounts.length; i++) + assertEquals(expectedCounts[i], effectiveCounts[i]); + }catch(DBException dbe){ + dbe.printStackTrace(System.err); + fail("[" + conn.getID() + ";custom def] No error should happen here!"); + } + + cnt++; + } + } + } + + @Test + public void testGetCreationOrder(){ + // There should be no difference between a POSTGRESQL connection and a SQLITE one! + JDBCConnection[] connections = new JDBCConnection[]{pgJDBCConnection,sensPgJDBCConnection,sqliteJDBCConnection,sensSqliteJDBCConnection}; + for(JDBCConnection conn : connections){ + assertEquals(-1, conn.getCreationOrder(null)); + assertEquals(0, conn.getCreationOrder(STDTable.SCHEMAS)); + assertEquals(1, conn.getCreationOrder(STDTable.TABLES)); + assertEquals(2, conn.getCreationOrder(STDTable.COLUMNS)); + assertEquals(3, conn.getCreationOrder(STDTable.KEYS)); + assertEquals(4, conn.getCreationOrder(STDTable.KEY_COLUMNS)); + } + } + + @Test + public void testGetDBMSDatatype(){ + assertEquals("VARCHAR", pgJDBCConnection.defaultTypeConversion(null)); + assertEquals("TEXT", sqliteJDBCConnection.defaultTypeConversion(null)); + + assertEquals("bytea", pgJDBCConnection.defaultTypeConversion(new DBType(DBDatatype.VARBINARY))); + assertEquals("BLOB", sqliteJDBCConnection.defaultTypeConversion(new DBType(DBDatatype.VARBINARY))); + } + + @Test + public void testMergeTAPSchemaDefs(){ + // There should be no difference between a POSTGRESQL connection and a SQLITE one! + JDBCConnection[] connections = new JDBCConnection[]{pgJDBCConnection,sensPgJDBCConnection,sqliteJDBCConnection,sensSqliteJDBCConnection}; + for(JDBCConnection conn : connections){ + + // TEST WITH NO METADATA OBJECT: + // -> expected: throws a NULL exception. + try{ + conn.mergeTAPSchemaDefs(null); + }catch(Exception e){ + assertEquals(NullPointerException.class, e.getClass()); + } + + // TEST WITH EMPTY METADATA OBJECT: + // -> expected: returns at least the 5 tables of the TAP_SCHEMA. + TAPTable[] stdTables = conn.mergeTAPSchemaDefs(new TAPMetadata()); + + assertEquals(5, stdTables.length); + + for(TAPTable t : stdTables) + assertEquals(STDSchema.TAPSCHEMA.toString(), t.getADQLSchemaName()); + + assertEquals(STDTable.SCHEMAS.toString(), stdTables[0].getADQLName()); + assertEquals(STDTable.TABLES.toString(), stdTables[1].getADQLName()); + assertEquals(STDTable.COLUMNS.toString(), stdTables[2].getADQLName()); + assertEquals(STDTable.KEYS.toString(), stdTables[3].getADQLName()); + assertEquals(STDTable.KEY_COLUMNS.toString(), stdTables[4].getADQLName()); + + // TEST WITH INCOMPLETE TAP_SCHEMA TABLES LIST + 1 CUSTOM TAP_SCHEMA TABLE (here: TAP_SCHEMA.columns): + // -> expected: the 5 tables of the TAP_SCHEMA including the modification of the standard tables & ignore the additional table(s) if any (which is the case here). + TAPMetadata customMeta = createCustomSchema(); + stdTables = conn.mergeTAPSchemaDefs(customMeta); + + assertEquals(5, stdTables.length); + + for(TAPTable t : stdTables) + assertEquals(STDSchema.TAPSCHEMA.toString(), t.getADQLSchemaName()); + + assertEquals(STDTable.SCHEMAS.toString(), stdTables[0].getADQLName()); + assertEquals(STDTable.TABLES.toString(), stdTables[1].getADQLName()); + assertEquals(STDTable.COLUMNS.toString(), stdTables[2].getADQLName()); + assertEquals("Columns", stdTables[2].getDBName()); + assertNotNull(stdTables[2].getColumn("TestNewColumn")); + assertEquals(STDTable.KEYS.toString(), stdTables[3].getADQLName()); + assertEquals(STDTable.KEY_COLUMNS.toString(), stdTables[4].getADQLName()); + } + } + + @Test + public void testEquals(){ + // There should be no difference between a POSTGRESQL connection and a SQLITE one! + JDBCConnection[] connections = new JDBCConnection[]{pgJDBCConnection,sensPgJDBCConnection,sqliteJDBCConnection,sensSqliteJDBCConnection}; + for(JDBCConnection conn : connections){ + // NULL tests: + assertFalse(conn.equals("tap_schema", null, false)); + assertFalse(conn.equals("tap_schema", null, true)); + assertFalse(conn.equals(null, "tap_schema", false)); + assertFalse(conn.equals(null, "tap_schema", true)); + assertFalse(conn.equals(null, null, false)); + assertFalse(conn.equals(null, null, true)); + + // CASE SENSITIVE tests: + if (conn.supportsMixedCaseQuotedIdentifier || conn.mixedCaseQuoted){ + assertFalse(conn.equals("tap_schema", "TAP_SCHEMA", true)); + assertTrue(conn.equals("TAP_SCHEMA", "TAP_SCHEMA", true)); + assertFalse(conn.equals("TAP_SCHEMA", "tap_schema", true)); + assertFalse(conn.equals("Columns", "columns", true)); + assertFalse(conn.equals("columns", "Columns", true)); + }else if (conn.lowerCaseQuoted){ + assertTrue(conn.equals("tap_schema", "TAP_SCHEMA", true)); + assertFalse(conn.equals("TAP_SCHEMA", "TAP_SCHEMA", true)); + assertFalse(conn.equals("TAP_SCHEMA", "tap_schema", true)); + assertFalse(conn.equals("Columns", "columns", true)); + assertTrue(conn.equals("columns", "Columns", true)); + }else if (conn.upperCaseQuoted){ + assertFalse(conn.equals("tap_schema", "TAP_SCHEMA", true)); + assertTrue(conn.equals("TAP_SCHEMA", "TAP_SCHEMA", true)); + assertTrue(conn.equals("TAP_SCHEMA", "tap_schema", true)); + assertFalse(conn.equals("Columns", "columns", true)); + assertFalse(conn.equals("columns", "Columns", true)); + }else{ + assertTrue(conn.equals("tap_schema", "TAP_SCHEMA", true)); + assertTrue(conn.equals("TAP_SCHEMA", "TAP_SCHEMA", true)); + assertTrue(conn.equals("TAP_SCHEMA", "tap_schema", true)); + assertTrue(conn.equals("Columns", "columns", true)); + assertTrue(conn.equals("columns", "Columns", true)); + } + + // CASE INSENSITIVE tests: + if (conn.supportsMixedCaseUnquotedIdentifier){ + assertTrue(conn.equals("tap_schema", "TAP_SCHEMA", false)); + assertTrue(conn.equals("TAP_SCHEMA", "TAP_SCHEMA", false)); + assertTrue(conn.equals("TAP_SCHEMA", "tap_schema", false)); + assertTrue(conn.equals("Columns", "columns", false)); + assertTrue(conn.equals("columns", "Columns", false)); + }else if (conn.lowerCaseUnquoted){ + assertTrue(conn.equals("tap_schema", "TAP_SCHEMA", false)); + assertFalse(conn.equals("TAP_SCHEMA", "TAP_SCHEMA", false)); + assertFalse(conn.equals("TAP_SCHEMA", "tap_schema", false)); + assertFalse(conn.equals("Columns", "columns", false)); + assertTrue(conn.equals("columns", "Columns", false)); + }else if (conn.upperCaseUnquoted){ + assertFalse(conn.equals("tap_schema", "TAP_SCHEMA", false)); + assertTrue(conn.equals("TAP_SCHEMA", "TAP_SCHEMA", false)); + assertTrue(conn.equals("TAP_SCHEMA", "tap_schema", false)); + assertFalse(conn.equals("Columns", "columns", false)); + assertFalse(conn.equals("columns", "Columns", false)); + }else{ + assertTrue(conn.equals("tap_schema", "TAP_SCHEMA", false)); + assertTrue(conn.equals("TAP_SCHEMA", "TAP_SCHEMA", false)); + assertTrue(conn.equals("TAP_SCHEMA", "tap_schema", false)); + assertTrue(conn.equals("Columns", "columns", false)); + assertTrue(conn.equals("columns", "Columns", false)); + } + } + } + + @Test + public void testGetTAPSchema(){ + // There should be no difference between a POSTGRESQL connection and a SQLITE one! + JDBCConnection[] connections = new JDBCConnection[]{pgJDBCConnection,sensPgJDBCConnection,sqliteJDBCConnection,sensSqliteJDBCConnection}; + for(JDBCConnection conn : connections){ + try{ + // Prepare the test: + createTAPSchema(conn); + // Try to get it (which should work without any problem here): + conn.getTAPSchema(); + }catch(DBException de){ + de.printStackTrace(System.err); + fail("No pbm should happen here (either for the creation of a std TAP_SCHEMA or for its reading)! CAUSE: " + de.getMessage()); + } + + try{ + // Prepare the test: + dropSchema(STDSchema.TAPSCHEMA.label, conn); + // Try to get it (which should work without any problem here): + conn.getTAPSchema(); + fail("DBException expected, because none of the TAP_SCHEMA tables exist."); + }catch(DBException de){ + assertTrue(de.getMessage().equals("Impossible to load schemas from TAP_SCHEMA.schemas!")); + } + } + } + + @Test + public void testIsTableExisting(){ + // There should be no difference between a POSTGRESQL connection and a SQLITE one! + JDBCConnection[] connections = new JDBCConnection[]{pgJDBCConnection,sensPgJDBCConnection,sqliteJDBCConnection,sensSqliteJDBCConnection}; + for(JDBCConnection conn : connections){ + try{ + // Get the database metadata: + DatabaseMetaData dbMeta = conn.connection.getMetaData(); + + // Prepare the test: + createTAPSchema(conn); + // Test the existence of all TAP_SCHEMA tables: + assertTrue(conn.isTableExisting(STDSchema.TAPSCHEMA.label, (conn.supportsSchema ? STDTable.SCHEMAS.label : STDSchema.TAPSCHEMA.label + "_" + STDTable.SCHEMAS.label), dbMeta)); + assertTrue(conn.isTableExisting(STDSchema.TAPSCHEMA.label, (conn.supportsSchema ? STDTable.TABLES.label : STDSchema.TAPSCHEMA.label + "_" + STDTable.TABLES.label), dbMeta)); + assertTrue(conn.isTableExisting(STDSchema.TAPSCHEMA.label, (conn.supportsSchema ? STDTable.COLUMNS.label : STDSchema.TAPSCHEMA.label + "_" + STDTable.COLUMNS.label), dbMeta)); + assertTrue(conn.isTableExisting(STDSchema.TAPSCHEMA.label, (conn.supportsSchema ? STDTable.KEYS.label : STDSchema.TAPSCHEMA.label + "_" + STDTable.KEYS.label), dbMeta)); + assertTrue(conn.isTableExisting(STDSchema.TAPSCHEMA.label, (conn.supportsSchema ? STDTable.KEY_COLUMNS.label : STDSchema.TAPSCHEMA.label + "_" + STDTable.KEY_COLUMNS.label), dbMeta)); + // Test the non-existence of any other table: + assertFalse(conn.isTableExisting(null, "foo", dbMeta)); + + // Prepare the test: + dropSchema(STDSchema.TAPSCHEMA.label, conn); + // Test the non-existence of all TAP_SCHEMA tables: + assertFalse(conn.isTableExisting(STDSchema.TAPSCHEMA.label, (conn.supportsSchema ? STDTable.SCHEMAS.label : STDSchema.TAPSCHEMA.label + "_" + STDTable.SCHEMAS.label), dbMeta)); + assertFalse(conn.isTableExisting(STDSchema.TAPSCHEMA.label, (conn.supportsSchema ? STDTable.TABLES.label : STDSchema.TAPSCHEMA.label + "_" + STDTable.TABLES.label), dbMeta)); + assertFalse(conn.isTableExisting(STDSchema.TAPSCHEMA.label, (conn.supportsSchema ? STDTable.COLUMNS.label : STDSchema.TAPSCHEMA.label + "_" + STDTable.COLUMNS.label), dbMeta)); + assertFalse(conn.isTableExisting(STDSchema.TAPSCHEMA.label, (conn.supportsSchema ? STDTable.KEYS.label : STDSchema.TAPSCHEMA.label + "_" + STDTable.KEYS.label), dbMeta)); + assertFalse(conn.isTableExisting(STDSchema.TAPSCHEMA.label, (conn.supportsSchema ? STDTable.KEY_COLUMNS.label : STDSchema.TAPSCHEMA.label + "_" + STDTable.KEY_COLUMNS.label), dbMeta)); + }catch(Exception ex){ + ex.printStackTrace(System.err); + fail("{" + conn.getID() + "} Testing the existence of a table should not throw an error!"); + } + } + } + + @Test + public void testIsColumnExisting(){ + // There should be no difference between a POSTGRESQL connection and a SQLITE one! + JDBCConnection[] connections = new JDBCConnection[]{pgJDBCConnection,sensPgJDBCConnection,sqliteJDBCConnection,sensSqliteJDBCConnection}; + int i = -1; + for(JDBCConnection conn : connections){ + i++; + try{ + // Get the database metadata: + DatabaseMetaData dbMeta = conn.connection.getMetaData(); + + // Prepare the test: + createTAPSchema(conn); + // Test the existence of one column for all TAP_SCHEMA tables: + assertTrue(conn.isColumnExisting(STDSchema.TAPSCHEMA.label, (conn.supportsSchema ? STDTable.SCHEMAS.label : STDSchema.TAPSCHEMA.label + "_" + STDTable.SCHEMAS.label), "schema_name", dbMeta)); + assertTrue(conn.isColumnExisting(STDSchema.TAPSCHEMA.label, (conn.supportsSchema ? STDTable.TABLES.label : STDSchema.TAPSCHEMA.label + "_" + STDTable.TABLES.label), "table_name", dbMeta)); + assertTrue(conn.isColumnExisting(STDSchema.TAPSCHEMA.label, (conn.supportsSchema ? STDTable.COLUMNS.label : STDSchema.TAPSCHEMA.label + "_" + STDTable.COLUMNS.label), "column_name", dbMeta)); + assertTrue(conn.isColumnExisting(STDSchema.TAPSCHEMA.label, (conn.supportsSchema ? STDTable.KEYS.label : STDSchema.TAPSCHEMA.label + "_" + STDTable.KEYS.label), "key_id", dbMeta)); + assertTrue(conn.isColumnExisting(STDSchema.TAPSCHEMA.label, (conn.supportsSchema ? STDTable.KEY_COLUMNS.label : STDSchema.TAPSCHEMA.label + "_" + STDTable.KEY_COLUMNS.label), "key_id", dbMeta)); + // Test the non-existence of any column: + assertFalse(conn.isColumnExisting(null, null, "foo", dbMeta)); + + // Prepare the test: + dropSchema(STDSchema.TAPSCHEMA.label, conn); + // Test the non-existence of the same column for all TAP_SCHEMA tables: + assertFalse(conn.isColumnExisting(STDSchema.TAPSCHEMA.label, (conn.supportsSchema ? STDTable.SCHEMAS.label : STDSchema.TAPSCHEMA.label + "_" + STDTable.SCHEMAS.label), "schema_name", dbMeta)); + assertFalse(conn.isColumnExisting(STDSchema.TAPSCHEMA.label, (conn.supportsSchema ? STDTable.TABLES.label : STDSchema.TAPSCHEMA.label + "_" + STDTable.TABLES.label), "table_name", dbMeta)); + assertFalse(conn.isColumnExisting(STDSchema.TAPSCHEMA.label, (conn.supportsSchema ? STDTable.COLUMNS.label : STDSchema.TAPSCHEMA.label + "_" + STDTable.COLUMNS.label), "column_name", dbMeta)); + assertFalse(conn.isColumnExisting(STDSchema.TAPSCHEMA.label, (conn.supportsSchema ? STDTable.KEYS.label : STDSchema.TAPSCHEMA.label + "_" + STDTable.KEYS.label), "key_id", dbMeta)); + assertFalse(conn.isColumnExisting(STDSchema.TAPSCHEMA.label, (conn.supportsSchema ? STDTable.KEY_COLUMNS.label : STDSchema.TAPSCHEMA.label + "_" + STDTable.KEY_COLUMNS.label), "key_id", dbMeta)); + }catch(Exception ex){ + ex.printStackTrace(System.err); + fail("{" + conn.getID() + "} Testing the existence of a column should not throw an error!"); + } + } + } + + @Test + public void testAddUploadedTable(){ + // There should be no difference between a POSTGRESQL connection and a SQLITE one! + JDBCConnection[] connections = new JDBCConnection[]{pgJDBCConnection,sensPgJDBCConnection,sqliteJDBCConnection,sensSqliteJDBCConnection}; + TAPTable tableDef = null; + for(JDBCConnection conn : connections){ + InputStream io = null; + try{ + io = new FileInputStream(uploadExamplePath); + TableIterator it = new VOTableIterator(io); + + TAPColumn[] cols = it.getMetadata(); + tableDef = new TAPTable("UploadExample"); + for(TAPColumn c : cols) + tableDef.addColumn(c); + + // Test with no schema set: + try{ + conn.addUploadedTable(tableDef, it); + fail("The table is not inside a TAPSchema, so this test should have failed!"); + }catch(Exception ex){ + assertTrue(ex instanceof DBException); + assertEquals("Missing upload schema! An uploaded table must be inside a schema whose the ADQL name is strictly equals to \"" + STDSchema.UPLOADSCHEMA.label + "\" (but the DB name may be different).", ex.getMessage()); + } + + // Specify the UPLOAD schema for the table to upload: + TAPSchema schema = new TAPSchema(STDSchema.UPLOADSCHEMA.label); + schema.addTable(tableDef); + + // Prepare the test: no TAP_UPLOAD schema and no table TAP_UPLOAD.UploadExample: + dropSchema(STDSchema.UPLOADSCHEMA.label, conn); + // Test: + try{ + assertTrue(conn.addUploadedTable(tableDef, it)); + }catch(Exception ex){ + ex.printStackTrace(System.err); + fail("{" + conn.ID + "} This error should not happen: no TAP_UPLOAD schema."); + } + + close(io); + io = new FileInputStream(uploadExamplePath); + it = new VOTableIterator(io); + + // Prepare the test: the TAP_UPLOAD schema exist but not the table TAP_UPLOAD.UploadExample: + dropTable(tableDef.getDBSchemaName(), tableDef.getDBName(), conn); + // Test: + try{ + assertTrue(conn.addUploadedTable(tableDef, it)); + }catch(Exception ex){ + ex.printStackTrace(System.err); + fail("{" + conn.ID + "} This error should not happen: no TAP_UPLOAD schema."); + } + + close(io); + io = new FileInputStream(uploadExamplePath); + it = new VOTableIterator(io); + + // Prepare the test: the TAP_UPLOAD schema and the table TAP_UPLOAD.UploadExample BOTH exist: + ; + // Test: + try{ + assertFalse(conn.addUploadedTable(tableDef, it)); + }catch(Exception ex){ + if (ex instanceof DBException) + assertEquals("Impossible to create the user uploaded table in the database: " + conn.translator.getTableName(tableDef, conn.supportsSchema) + "! This table already exists.", ex.getMessage()); + else{ + ex.printStackTrace(System.err); + fail("{" + conn.ID + "} DBException was the expected exception!"); + } + } + + }catch(Exception ex){ + ex.printStackTrace(System.err); + fail("{" + conn.ID + "} This error should never happen except there is a problem with the file (" + uploadExamplePath + ")."); + }finally{ + close(io); + } + } + } + + @Test + public void testDropUploadedTable(){ + TAPTable tableDef = new TAPTable("TableToDrop"); + TAPSchema uploadSchema = new TAPSchema(STDSchema.UPLOADSCHEMA.label); + uploadSchema.addTable(tableDef); + + // There should be no difference between a POSTGRESQL connection and a SQLITE one! + JDBCConnection[] connections = new JDBCConnection[]{pgJDBCConnection,sensPgJDBCConnection,sqliteJDBCConnection,sensSqliteJDBCConnection}; + for(JDBCConnection conn : connections){ + try{ + // 1st TEST CASE: the schema TAP_UPLOAD does not exist -> no error should be raised! + // drop the TAP_UPLOAD schema: + dropSchema(uploadSchema.getDBName(), conn); + // try to drop the table: + assertTrue(conn.dropUploadedTable(tableDef)); + + // 2nd TEST CASE: the table does not exists -> no error should be raised! + // create the TAP_UPLOAD schema, but not the table: + createSchema(uploadSchema.getDBName(), conn); + // try to drop the table: + assertTrue(conn.dropUploadedTable(tableDef)); + + // 3rd TEST CASE: the table and the schema exist -> the table should be created without any error! + // create the fake uploaded table: + createFooTable(tableDef.getDBSchemaName(), tableDef.getDBName(), conn); + // try to drop the table: + assertTrue(conn.dropUploadedTable(tableDef)); + + }catch(Exception ex){ + ex.printStackTrace(System.err); + fail("{" + conn.ID + "} This error should not happen. The table should be dropped and even if it does not exist, no error should be thrown."); + } + } + } + + @Test + public void testExecuteQuery(){ + // There should be no difference between a POSTGRESQL connection and a SQLITE one! + JDBCConnection[] connections = new JDBCConnection[]{pgJDBCConnection,sensPgJDBCConnection,sqliteJDBCConnection,sensSqliteJDBCConnection}; + for(JDBCConnection conn : connections){ + + TAPSchema schema = TAPMetadata.getStdSchema(conn.supportsSchema); + ArrayList tables = new ArrayList(schema.getNbTables()); + for(TAPTable t : schema) + tables.add(t); + + ADQLParser parser = new ADQLParser(new DBChecker(tables)); + parser.setDebug(false); + + /*if (conn.ID.equalsIgnoreCase("SQLITE")){ + for(DBTable t : tables){ + TAPTable tapT = (TAPTable)t; + tapT.getSchema().setDBName(null); + tapT.setDBName(tapT.getSchema().getADQLName() + "_" + tapT.getDBName()); + } + }*/ + + TableIterator result = null; + try{ + // Prepare the test: create the TAP_SCHEMA: + dropSchema(STDSchema.TAPSCHEMA.label, conn); + // Build the ADQLQuery object: + ADQLQuery query = parser.parseQuery("SELECT table_name FROM TAP_SCHEMA.tables;"); + // Execute the query: + result = conn.executeQuery(query); + fail("{" + conn.ID + "} This test should have failed because TAP_SCHEMA was supposed to not exist!"); + }catch(DBException de){ + assertTrue(de.getMessage().startsWith("Unexpected error while executing a SQL query: ")); + assertTrue(de.getMessage().indexOf("tap_schema") > 0 || de.getMessage().indexOf("TAP_SCHEMA") > 0); + }catch(ParseException pe){ + pe.printStackTrace(System.err); + fail("There should be no pbm to parse the ADQL expression!"); + }finally{ + if (result != null){ + try{ + result.close(); + }catch(DataReadException de){} + result = null; + } + } + + try{ + // Prepare the test: create the TAP_SCHEMA: + createTAPSchema(conn); + // Build the ADQLQuery object: + ADQLQuery query = parser.parseQuery("SELECT table_name FROM TAP_SCHEMA.tables;"); + // Execute the query: + result = conn.executeQuery(query); + assertEquals(1, result.getMetadata().length); + int cntRow = 0; + while(result.nextRow()){ + cntRow++; + assertTrue(result.hasNextCol()); + assertNotNull(TAPMetadata.resolveStdTable((String)result.nextCol())); + assertFalse(result.hasNextCol()); + } + assertEquals(5, cntRow); + }catch(DBException de){ + de.printStackTrace(System.err); + fail("No ADQL/SQL query error was expected here!"); + }catch(ParseException pe){ + fail("There should be no pbm to parse the ADQL expression!"); + }catch(DataReadException e){ + e.printStackTrace(System.err); + fail("There should be no pbm when accessing rows and the first (and only) columns of the result!"); + }catch(Exception ex){ + ex.printStackTrace(System.err); + fail("There should be no pbm when reading the query result!"); + }finally{ + if (result != null){ + try{ + result.close(); + }catch(DataReadException de){} + result = null; + } + } + } + } + + /* ************** */ + /* TOOL FUNCTIONS */ + /* ************** */ + + public final static void main(final String[] args) throws Throwable{ + JDBCConnection conn = new JDBCConnection(DBTools.createConnection("postgresql", "127.0.0.1", null, "gmantele", "gmantele", "pwd"), new PostgreSQLTranslator(), "TEST_POSTGRES", null); + JDBCConnectionTest.createTAPSchema(conn); + JDBCConnectionTest.dropSchema(STDSchema.TAPSCHEMA.label, conn); + } + + /** + *

    Build a table prefix with the given schema name.

    + * + *

    By default, this function returns: schemaName + "_".

    + * + *

    CAUTION: + * This function is used only when schemas are not supported by the DBMS connection. + * It aims to propose an alternative of the schema notion by prefixing the table name by the schema name. + *

    + * + *

    Note: + * If the given schema is NULL or is an empty string, an empty string will be returned. + * Thus, no prefix will be set....which is very useful when the table name has already been prefixed + * (in such case, the DB name of its schema has theoretically set to NULL). + *

    + * + * @param schemaName (DB) Schema name. + * + * @return The corresponding table prefix, or "" if the given schema name is an empty string or NULL. + */ + protected static String getTablePrefix(final String schemaName){ + if (schemaName != null && schemaName.trim().length() > 0) + return schemaName + "_"; + else + return ""; + } + + private static void dropSchema(final String schemaName, final JDBCConnection conn){ + Statement stmt = null; + ResultSet rs = null; + try{ + stmt = conn.connection.createStatement(); + + final boolean caseSensitive = conn.translator.isCaseSensitive(IdentifierField.SCHEMA); + if (conn.supportsSchema) + stmt.executeUpdate("DROP SCHEMA IF EXISTS " + formatIdentifier(schemaName, caseSensitive) + " CASCADE;"); + else{ + startTransaction(conn); + final String tablePrefix = getTablePrefix(schemaName); + final int prefixLen = tablePrefix.length(); + if (prefixLen <= 0) + return; + rs = conn.connection.getMetaData().getTables(null, null, null, null); + ArrayList tablesToDrop = new ArrayList(); + while(rs.next()){ + String table = rs.getString(3); + if (table.length() > prefixLen){ + if (equals(schemaName, table.substring(0, prefixLen - 1), caseSensitive)) + tablesToDrop.add(table); + } + } + close(rs); + rs = null; + for(String t : tablesToDrop) + stmt.executeUpdate("DROP TABLE IF EXISTS \"" + t + "\";"); + commit(conn); + } + }catch(Exception ex){ + rollback(conn); + ex.printStackTrace(System.err); + fail("{" + conn.ID + "} Impossible to prepare a test by: dropping the schema " + schemaName + "!"); + }finally{ + close(rs); + close(stmt); + } + } + + private static void dropTable(final String schemaName, final String tableName, final JDBCConnection conn){ + Statement stmt = null; + ResultSet rs = null; + try{ + final boolean sCaseSensitive = conn.translator.isCaseSensitive(IdentifierField.SCHEMA); + final boolean tCaseSensitive = conn.translator.isCaseSensitive(IdentifierField.TABLE); + stmt = conn.connection.createStatement(); + if (conn.supportsSchema) + stmt.executeUpdate("DROP TABLE IF EXISTS " + formatIdentifier(schemaName, sCaseSensitive) + "." + formatIdentifier(tableName, tCaseSensitive) + ";"); + else{ + rs = conn.connection.getMetaData().getTables(null, null, null, null); + String tableToDrop = null; + while(rs.next()){ + String table = rs.getString(3); + if (equals(tableName, table, tCaseSensitive)){ + tableToDrop = table; + break; + } + } + close(rs); + if (tableToDrop != null) + stmt.executeUpdate("DROP TABLE IF EXISTS \"" + tableToDrop + "\";"); + } + }catch(Exception ex){ + ex.printStackTrace(System.err); + fail("{" + conn.ID + "} Impossible to prepare a test by: dropping the table " + schemaName + "." + tableName + "!"); + }finally{ + close(rs); + close(stmt); + } + } + + private static void createSchema(final String schemaName, final JDBCConnection conn){ + if (!conn.supportsSchema) + return; + + dropSchema(schemaName, conn); + + Statement stmt = null; + ResultSet rs = null; + try{ + final boolean sCaseSensitive = conn.translator.isCaseSensitive(IdentifierField.SCHEMA); + stmt = conn.connection.createStatement(); + stmt.executeUpdate("CREATE SCHEMA " + formatIdentifier(schemaName, sCaseSensitive) + ";"); + }catch(Exception ex){ + ex.printStackTrace(System.err); + fail("{" + conn.ID + "} Impossible to prepare a test by: creating the schema " + schemaName + "!"); + }finally{ + close(rs); + close(stmt); + } + } + + private static void createFooTable(final String schemaName, final String tableName, final JDBCConnection conn){ + dropTable(schemaName, tableName, conn); + + Statement stmt = null; + ResultSet rs = null; + try{ + final boolean sCaseSensitive = conn.translator.isCaseSensitive(IdentifierField.SCHEMA); + final boolean tCaseSensitive = conn.translator.isCaseSensitive(IdentifierField.TABLE); + String tablePrefix = formatIdentifier(schemaName, sCaseSensitive); + if (!conn.supportsSchema || tablePrefix == null) + tablePrefix = ""; + else + tablePrefix += "."; + stmt = conn.connection.createStatement(); + stmt.executeUpdate("CREATE TABLE " + tablePrefix + formatIdentifier(tableName, tCaseSensitive) + " (ID integer);"); + }catch(Exception ex){ + ex.printStackTrace(System.err); + fail("{" + conn.ID + "} Impossible to prepare a test by: creating the table " + schemaName + "." + tableName + "!"); + }finally{ + close(rs); + close(stmt); + } + } + + private static TAPMetadata createTAPSchema(final JDBCConnection conn){ + dropSchema(STDSchema.TAPSCHEMA.label, conn); + + TAPMetadata metadata = new TAPMetadata(); + Statement stmt = null; + try{ + final boolean sCaseSensitive = conn.translator.isCaseSensitive(IdentifierField.SCHEMA); + final boolean tCaseSensitive = conn.translator.isCaseSensitive(IdentifierField.TABLE); + String[] tableNames = new String[]{STDTable.SCHEMAS.label,STDTable.TABLES.label,STDTable.COLUMNS.label,STDTable.KEYS.label,STDTable.KEY_COLUMNS.label}; + if (conn.supportsSchema){ + for(int i = 0; i < tableNames.length; i++) + tableNames[i] = formatIdentifier(STDSchema.TAPSCHEMA.label, sCaseSensitive) + "." + formatIdentifier(tableNames[i], tCaseSensitive); + }else{ + for(int i = 0; i < tableNames.length; i++) + tableNames[i] = formatIdentifier(getTablePrefix(STDSchema.TAPSCHEMA.label) + tableNames[i], tCaseSensitive); + } + + startTransaction(conn); + + stmt = conn.connection.createStatement(); + + if (conn.supportsSchema) + stmt.executeUpdate("CREATE SCHEMA " + formatIdentifier(STDSchema.TAPSCHEMA.label, sCaseSensitive) + ";"); + + stmt.executeUpdate("CREATE TABLE " + tableNames[0] + "(\"schema_name\" VARCHAR,\"description\" VARCHAR,\"utype\" VARCHAR,\"dbname\" VARCHAR, PRIMARY KEY(\"schema_name\"));"); + stmt.executeUpdate("DELETE FROM " + tableNames[0] + ";"); + + stmt.executeUpdate("CREATE TABLE " + tableNames[1] + "(\"schema_name\" VARCHAR,\"table_name\" VARCHAR,\"table_type\" VARCHAR,\"description\" VARCHAR,\"utype\" VARCHAR,\"dbname\" VARCHAR, PRIMARY KEY(\"schema_name\", \"table_name\"));"); + stmt.executeUpdate("DELETE FROM " + tableNames[1] + ";"); + + stmt.executeUpdate("CREATE TABLE " + tableNames[2] + "(\"table_name\" VARCHAR,\"column_name\" VARCHAR,\"description\" VARCHAR,\"unit\" VARCHAR,\"ucd\" VARCHAR,\"utype\" VARCHAR,\"datatype\" VARCHAR,\"size\" INTEGER,\"principal\" INTEGER,\"indexed\" INTEGER,\"std\" INTEGER,\"dbname\" VARCHAR, PRIMARY KEY(\"table_name\", \"column_name\"));"); + stmt.executeUpdate("DELETE FROM " + tableNames[2] + ";"); + + stmt.executeUpdate("CREATE TABLE " + tableNames[3] + "(\"key_id\" VARCHAR,\"from_table\" VARCHAR,\"target_table\" VARCHAR,\"description\" VARCHAR,\"utype\" VARCHAR, PRIMARY KEY(\"key_id\"));"); + stmt.executeUpdate("DELETE FROM " + tableNames[3] + ";"); + + stmt.executeUpdate("CREATE TABLE " + tableNames[4] + "(\"key_id\" VARCHAR,\"from_column\" VARCHAR,\"target_column\" VARCHAR, PRIMARY KEY(\"key_id\"));"); + stmt.executeUpdate("DELETE FROM " + tableNames[4] + ";"); + + /*if (!conn.supportsSchema){ + TAPSchema stdSchema = TAPMetadata.getStdSchema(); + for(TAPTable t : stdSchema) + t.setDBName(getTablePrefix(STDSchema.TAPSCHEMA.label) + t.getADQLName()); + metadata.addSchema(stdSchema); + }else*/ + metadata.addSchema(TAPMetadata.getStdSchema(conn.supportsSchema)); + + ArrayList lstTables = new ArrayList(); + for(TAPSchema schema : metadata){ + stmt.executeUpdate("INSERT INTO " + tableNames[0] + " VALUES('" + schema.getADQLName() + "','" + schema.getDescription() + "','" + schema.getUtype() + "','" + schema.getDBName() + "')"); + for(TAPTable t : schema) + lstTables.add(t); + } + + ArrayList lstCols = new ArrayList(); + for(TAPTable table : lstTables){ + stmt.executeUpdate("INSERT INTO " + tableNames[1] + " VALUES('" + table.getADQLSchemaName() + "','" + table.getADQLName() + "','" + table.getType() + "','" + table.getDescription() + "','" + table.getUtype() + "','" + table.getDBName() + "')"); + for(DBColumn c : table) + lstCols.add(c); + + } + lstTables = null; + + for(DBColumn c : lstCols){ + TAPColumn col = (TAPColumn)c; + stmt.executeUpdate("INSERT INTO " + tableNames[2] + " VALUES('" + col.getTable().getADQLName() + "','" + col.getADQLName() + "','" + col.getDescription() + "','" + col.getUnit() + "','" + col.getUcd() + "','" + col.getUtype() + "','" + col.getDatatype().type + "'," + col.getDatatype().length + "," + (col.isPrincipal() ? 1 : 0) + "," + (col.isIndexed() ? 1 : 0) + "," + (col.isStd() ? 1 : 0) + ",'" + col.getDBName() + "')"); + } + + commit(conn); + + }catch(Exception ex){ + rollback(conn); + ex.printStackTrace(System.err); + fail("{" + conn.ID + "} Impossible to prepare a test by: creating TAP_SCHEMA!"); + }finally{ + close(stmt); + } + + return metadata; + } + + private static void startTransaction(final JDBCConnection conn){ + try{ + conn.connection.setAutoCommit(false); + }catch(SQLException se){} + } + + private static void commit(final JDBCConnection conn){ + try{ + conn.connection.commit(); + conn.connection.setAutoCommit(true); + }catch(SQLException se){} + + } + + private static void rollback(final JDBCConnection conn){ + try{ + conn.connection.rollback(); + conn.connection.setAutoCommit(true); + }catch(SQLException se){} + + } + + private static String formatIdentifier(final String identifier, final boolean caseSensitive){ + if (identifier == null) + return null; + else if (identifier.charAt(0) == '"') + return identifier; + else if (caseSensitive) + return "\"" + identifier + "\""; + else + return identifier; + } + + private static boolean equals(final String name1, final String name2, final boolean caseSensitive){ + return (name1 != null && name2 != null && (caseSensitive ? name1.equals(name2) : name1.equalsIgnoreCase(name2))); + } + + private static boolean equals(final TAPTable table1, final TAPTable table2){ + if (table1 == null || table2 == null){ + //System.out.println("[EQUALS] tables null!"); + return false; + } + + if (!table1.getFullName().equals(table2.getFullName())){ + //System.out.println("[EQUALS] tables name different: " + table1.getFullName() + " != " + table2.getFullName() + "!"); + return false; + } + + if (table1.getType() != table2.getType()){ + //System.out.println("[EQUALS] tables type different: " + table1.getType() + " != " + table2.getType() + "!"); + return false; + } + + if (table1.getNbColumns() != table2.getNbColumns()){ + //System.out.println("[EQUALS] tables length different: " + table1.getNbColumns() + " columns != " + table2.getNbColumns() + " columns!"); + return false; + } + + Iterator it = table1.getColumns(); + while(it.hasNext()){ + TAPColumn col1 = it.next(); + if (!equals(col1, table2.getColumn(col1.getADQLName()))){ + //System.out.println("[EQUALS] tables columns different!"); + return false; + } + } + + return true; + } + + private static boolean equals(final TAPColumn col1, final TAPColumn col2){ + if (col1 == null || col2 == null){ + //System.out.println("[EQUALS] columns null!"); + return false; + } + + if (!col1.getADQLName().equals(col2.getADQLName())){ + //System.out.println("[EQUALS] columns name different: " + col1.getADQLName() + " != " + col2.getADQLName() + "!"); + return false; + } + + if (!equals(col1.getDatatype(), col2.getDatatype())){ + //System.out.println("[EQUALS] columns type different: " + col1.getDatatype() + " != " + col2.getDatatype() + "!"); + return false; + } + + if (col1.getUnit() != col2.getUnit()){ + //System.out.println("[EQUALS] columns unit different: " + col1.getUnit() + " != " + col2.getUnit() + "!"); + return false; + } + + if (col1.getUcd() != col2.getUcd()){ + //System.out.println("[EQUALS] columns ucd different: " + col1.getUcd() + " != " + col2.getUcd() + "!"); + return false; + } + + return true; + } + + private static boolean equals(final DBType type1, final DBType type2){ + return type1 != null && type2 != null && type1.type == type2.type && type1.length == type2.length; + } + + private static TAPMetadata createCustomSchema(){ + TAPMetadata tapMeta = new TAPMetadata(); + TAPSchema tapSchema = new TAPSchema(STDSchema.TAPSCHEMA.toString()); + TAPTable customColumns = (TAPTable)TAPMetadata.getStdTable(STDTable.COLUMNS).copy("Columns", STDTable.COLUMNS.label); + customColumns.addColumn("TestNewColumn", new DBType(DBDatatype.VARCHAR), "This is a fake column, just for test purpose.", null, null, null); + tapSchema.addTable(customColumns); + TAPTable addTable = new TAPTable("AdditionalTable"); + addTable.addColumn("Blabla"); + addTable.addColumn("Foo"); + tapSchema.addTable(addTable); + tapMeta.addSchema(tapSchema); + return tapMeta; + } + + /** + *

    Get the expected counts after a call of {@link JDBCConnection#setTAPSchema(TAPMetadata)}.

    + * + *

    Counts are computed from the given metadata ; the same metadata that will be given to {@link JDBCConnection#setTAPSchema(TAPMetadata)}.

    + * + * @param meta + * + * @return An integer array with the following values: [0]=nbSchemas, [1]=nbTables, [2]=nbColumns, [3]=nbKeys and [4]=nbKeyColumns. + */ + private static int[] getStats(final TAPMetadata meta){ + int[] counts = new int[]{1,5,0,0,0}; + + int[] stdColCounts = new int[]{3,5,11,5,3}; + for(int c = 0; c < stdColCounts.length; c++) + counts[2] += stdColCounts[c]; + + Iterator itSchemas = meta.iterator(); + while(itSchemas.hasNext()){ + TAPSchema schema = itSchemas.next(); + + boolean isTapSchema = (schema.getADQLName().equalsIgnoreCase(STDSchema.TAPSCHEMA.toString())); + if (!isTapSchema) + counts[0]++; + + Iterator itTables = schema.iterator(); + while(itTables.hasNext()){ + TAPTable table = itTables.next(); + if (isTapSchema && TAPMetadata.resolveStdTable(table.getADQLName()) != null){ + int ind = pgJDBCConnection.getCreationOrder(TAPMetadata.resolveStdTable(table.getADQLName())); + counts[2] -= stdColCounts[ind]; + }else + counts[1]++; + + Iterator itColumns = table.iterator(); + while(itColumns.hasNext()){ + itColumns.next(); + counts[2]++; + } + + Iterator itKeys = table.getForeignKeys(); + while(itKeys.hasNext()){ + TAPForeignKey fk = itKeys.next(); + counts[3]++; + counts[4] += fk.getNbRelations(); + } + } + } + + return counts; + } + + /** + *

    Get the effective counts after a call of {@link JDBCConnection#setTAPSchema(TAPMetadata)}.

    + * + *

    Counts are computed directly from the DB using the given connection; the same connection used to set the TAP schema in {@link JDBCConnection#setTAPSchema(TAPMetadata)}.

    + * + * @param conn + * @param meta Metadata, in order to get the standard TAP tables' name. + * + * @return An integer array with the following values: [0]=nbSchemas, [1]=nbTables, [2]=nbColumns, [3]=nbKeys and [4]=nbKeyColumns. + */ + private static int[] getStats(final JDBCConnection conn, final TAPMetadata meta){ + int[] counts = new int[5]; + + Statement stmt = null; + try{ + stmt = conn.connection.createStatement(); + + TAPSchema tapSchema = meta.getSchema(STDSchema.TAPSCHEMA.toString()); + + String schemaPrefix = formatIdentifier(tapSchema.getDBName(), conn.translator.isCaseSensitive(IdentifierField.SCHEMA)); + if (!conn.supportsSchema || schemaPrefix == null) + schemaPrefix = ""; + else + schemaPrefix += "."; + + boolean tCaseSensitive = conn.translator.isCaseSensitive(IdentifierField.TABLE); + TAPTable tapTable = tapSchema.getTable(STDTable.SCHEMAS.toString()); + counts[0] = count(stmt, schemaPrefix + formatIdentifier(tapTable.getDBName(), tCaseSensitive), tapSchema.getADQLName() + "." + tapTable.getADQLName()); + + tapTable = tapSchema.getTable(STDTable.TABLES.toString()); + counts[1] = count(stmt, schemaPrefix + formatIdentifier(tapTable.getDBName(), tCaseSensitive), tapSchema.getADQLName() + "." + tapTable.getADQLName()); + + tapTable = tapSchema.getTable(STDTable.COLUMNS.toString()); + counts[2] = count(stmt, schemaPrefix + formatIdentifier(tapTable.getDBName(), tCaseSensitive), tapSchema.getADQLName() + "." + tapTable.getADQLName()); + + tapTable = tapSchema.getTable(STDTable.KEYS.toString()); + counts[3] = count(stmt, schemaPrefix + formatIdentifier(tapTable.getDBName(), tCaseSensitive), tapSchema.getADQLName() + "." + tapTable.getADQLName()); + + tapTable = tapSchema.getTable(STDTable.KEY_COLUMNS.toString()); + counts[4] = count(stmt, schemaPrefix + formatIdentifier(tapTable.getDBName(), tCaseSensitive), tapSchema.getADQLName() + "." + tapTable.getADQLName()); + + }catch(SQLException se){ + fail("Can not create a statement!"); + }finally{ + try{ + if (stmt != null) + stmt.close(); + }catch(SQLException ex){} + } + return counts; + } + + private static int count(final Statement stmt, final String qualifiedTableName, final String adqlTableName){ + ResultSet rs = null; + try{ + rs = stmt.executeQuery("SELECT COUNT(*) FROM " + qualifiedTableName + ";"); + rs.next(); + return rs.getInt(1); + }catch(Exception e){ + e.printStackTrace(System.err); + fail("Can not count! Maybe " + qualifiedTableName + " (in ADQL: " + adqlTableName + ") does not exist."); + return -1; + }finally{ + close(rs); + } + } + + private static void close(final ResultSet rs){ + if (rs == null) + return; + try{ + rs.close(); + }catch(SQLException se){} + } + + private static void close(final Statement stmt){ + try{ + if (stmt != null) + stmt.close(); + }catch(SQLException se){} + } + + private static void close(final InputStream io){ + try{ + if (io != null) + io.close(); + }catch(IOException ioe){} + } + +} diff --git a/test/tap/db/TestTAPDb.db b/test/tap/db/TestTAPDb.db new file mode 100644 index 0000000000000000000000000000000000000000..c36006ea9746e081c41096322c5a83eae4b678d4 GIT binary patch literal 26624 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 E|2^4%-~a#s literal 0 HcmV?d00001 diff --git a/test/tap/db/upload_example.vot b/test/tap/db/upload_example.vot new file mode 100644 index 0000000..84f83a8 --- /dev/null +++ b/test/tap/db/upload_example.vot @@ -0,0 +1,75 @@ + + + + VizieR Astronomical Server vizier.u-strasbg.fr + Date: 2014-07-18T16:16:29 [V1.99+ (14-Oct-2013)] + Explanations and Statistics of UCDs: See LINK below + In case of problem, please report to: cds-question@unistra.fr + In this version, NULL integer columns are written as an empty string + <TD></TD>, explicitely possible from VOTable-1.3 + + + + + + Polarisation of Be stars (McDavid, 1986-1999) + + + Standard and Program Be stars + + + Right ascension (FK5, Equinox=J2000.0) (computed by VizieR, not part of the original data) + + + Declination (FK5, Equinox=J2000.0) (computed by VizieR, not part of the original data) + + + [p/s] Program or Standard star + + + Star name + + + HD (Cat. <III/135>) catalog number + + + BS (Cat. <V/50>) catalog number + + + Visual magnitude (BSC4, See Cat. <V/50>) + + + Right Ascension J2000 + + + Declination J2000 + + + MK Spectral type (1) + + + ? projected rotational velocity (1) [NULL integer written as an empty string] + + + + ask the {\bf\fg{FireBrick}Simbad} data-base about this object + + + + + + + + + + + + + +
    052.2671+59.9403s2H Cam2129110354.2303 29 04.1+59 56 25B9IaSimbad
    245.1587-24.1689somi Sco14708460814.5516 20 38.1-24 10 08A5IISimbad
    014.1758+60.7169pgam Cas53942642.4700 56 42.2+60 43 01B0.5IVe230Simbad
    025.9142+50.6889pphi Per105164964.0701 43 39.4+50 41 20B1.5(V:)e-shell400Simbad
    062.1646+47.7131p48 Per2594012734.0404 08 39.5+47 42 47B4Ve200Simbad
    084.4108+21.1428pzet Tau3720219103.0005 37 38.6+21 08 34B1IVe-shell220Simbad
    239.5471-14.2792p48 Lib14298359414.8815 58 11.3-14 16 45B3:IV:e-shell400Simbad
    246.7554-18.4558pchi Oph14818461184.4216 27 01.3-18 27 21B1.5Ve140Simbad
    336.3187+1.3772ppi Aqr21257185394.6622 25 16.5+01 22 38B1III-IVe300Simbad
    345.4796+42.3261pomi And21767587623.6223 01 55.1+42 19 34B6III260Simbad
    + + +
    +
    diff --git a/test/tap/formatter/JSONFormatTest.java b/test/tap/formatter/JSONFormatTest.java new file mode 100644 index 0000000..33deaf6 --- /dev/null +++ b/test/tap/formatter/JSONFormatTest.java @@ -0,0 +1,142 @@ +package tap.formatter; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.OutputStream; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.HashMap; + +import org.json.JSONObject; +import org.json.JSONTokener; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import tap.ServiceConnection; +import tap.TAPExecutionReport; +import tap.TAPJob; +import tap.data.ResultSetTableIterator; +import tap.data.TableIterator; +import tap.metadata.TAPColumn; +import tap.parameters.TAPParameters; +import testtools.DBTools; +import adql.db.DBType; +import adql.db.DBType.DBDatatype; + +/** + *

    Test the JSONFormat function {@link JSONFormat#writeResult(TableIterator, OutputStream, TAPExecutionReport, Thread)}.

    + * + *

    2 test ares done: 1 with an overflow and another without.

    + * + * @author Grégory Mantelet (ARI) + * @version 2.0 (07/2014) + */ +public class JSONFormatTest { + + private static Connection conn; + private static ServiceConnection serviceConn; + private static TAPColumn[] resultingColumns; + private static File jsonFile = new File("/home/gmantele/Desktop/json_test.json"); + + @BeforeClass + public static void setUpBeforeClass() throws Exception{ + conn = DBTools.createConnection("postgresql", "127.0.0.1", null, "gmantele", "gmantele", "pwd"); + serviceConn = new ServiceConnection4Test(); + + resultingColumns = new TAPColumn[4]; + resultingColumns[0] = new TAPColumn("ID", new DBType(DBDatatype.VARCHAR)); + resultingColumns[1] = new TAPColumn("ra", new DBType(DBDatatype.DOUBLE), "Right ascension", "deg", "pos.eq.ra", null); + resultingColumns[2] = new TAPColumn("deg", new DBType(DBDatatype.DOUBLE), "Declination", "deg", "pos.eq.dec", null); + resultingColumns[3] = new TAPColumn("gmag", new DBType(DBDatatype.DOUBLE), "G magnitude", "mag", "phot.mag;em.opt.B", null); + + if (!jsonFile.exists()) + jsonFile.createNewFile(); + } + + @AfterClass + public static void tearDownAfterClass() throws Exception{ + DBTools.closeConnection(conn); + jsonFile.delete(); + } + + @Test + public void testWriteResult(){ + ResultSet rs = null; + try{ + rs = DBTools.select(conn, "SELECT id, ra, deg, gmag FROM gums LIMIT 10;"); + + HashMap tapParams = new HashMap(1); + tapParams.put(TAPJob.PARAM_MAX_REC, "100"); + TAPParameters params = new TAPParameters(serviceConn, tapParams); + TAPExecutionReport report = new TAPExecutionReport("123456A", true, params); + report.resultingColumns = resultingColumns; + + TableIterator it = new ResultSetTableIterator(rs); + + JSONFormat formatter = new JSONFormat(serviceConn); + OutputStream output = new BufferedOutputStream(new FileOutputStream(jsonFile)); + formatter.writeResult(it, output, report, Thread.currentThread()); + output.close(); + + JSONTokener tok = new JSONTokener(new FileInputStream(jsonFile)); + JSONObject obj = (JSONObject)tok.nextValue(); + assertEquals(obj.getJSONArray("data").length(), 10); + + }catch(Exception t){ + t.printStackTrace(); + fail("Unexpected exception!"); + }finally{ + if (rs != null){ + try{ + rs.close(); + }catch(SQLException se){} + } + } + } + + @Test + public void testWriteResultWithOverflow(){ + ResultSet rs = null; + try{ + rs = DBTools.select(conn, "SELECT id, ra, deg, gmag FROM gums LIMIT 10;"); + + HashMap tapParams = new HashMap(1); + tapParams.put(TAPJob.PARAM_MAX_REC, "5"); + TAPParameters params = new TAPParameters(serviceConn, tapParams); + TAPExecutionReport report = new TAPExecutionReport("123456A", true, params); + report.resultingColumns = resultingColumns; + + TableIterator it = new ResultSetTableIterator(rs); + + JSONFormat formatter = new JSONFormat(serviceConn); + OutputStream output = new BufferedOutputStream(new FileOutputStream(jsonFile)); + formatter.writeResult(it, output, report, Thread.currentThread()); + output.close(); + + JSONTokener tok = new JSONTokener(new FileInputStream(jsonFile)); + JSONObject obj = (JSONObject)tok.nextValue(); + assertEquals(obj.getJSONArray("data").length(), 5); + + }catch(Exception t){ + t.printStackTrace(); + fail("Unexpected exception!"); + }finally{ + if (rs != null){ + try{ + rs.close(); + }catch(SQLException e){ + System.err.println("Can not close the RESULTSET!"); + e.printStackTrace(); + } + } + } + } + +} diff --git a/test/tap/formatter/SVFormatTest.java b/test/tap/formatter/SVFormatTest.java new file mode 100644 index 0000000..933f9c9 --- /dev/null +++ b/test/tap/formatter/SVFormatTest.java @@ -0,0 +1,136 @@ +package tap.formatter; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStream; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.HashMap; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import tap.ServiceConnection; +import tap.TAPExecutionReport; +import tap.TAPJob; +import tap.data.ResultSetTableIterator; +import tap.data.TableIterator; +import tap.metadata.TAPColumn; +import tap.parameters.TAPParameters; +import testtools.CommandExecute; +import testtools.DBTools; +import adql.db.DBType; +import adql.db.DBType.DBDatatype; + +/** + *

    Test the SVFormat function {@link SVFormat#writeResult(TableIterator, OutputStream, TAPExecutionReport, Thread)}.

    + * + *

    2 test ares done: 1 with an overflow and another without.

    + * + * @author Grégory Mantelet (ARI) + * @version 2.0 (09/2014) + */ +public class SVFormatTest { + + private static Connection conn; + private static ServiceConnection serviceConn; + private static TAPColumn[] resultingColumns; + private static File svFile = new File("/home/gmantele/Desktop/sv_test.txt"); + + @BeforeClass + public static void setUpBeforeClass() throws Exception{ + conn = DBTools.createConnection("postgresql", "127.0.0.1", null, "gmantele", "gmantele", "pwd"); + serviceConn = new ServiceConnection4Test(); + + resultingColumns = new TAPColumn[4]; + resultingColumns[0] = new TAPColumn("ID", new DBType(DBDatatype.VARCHAR)); + resultingColumns[1] = new TAPColumn("ra", new DBType(DBDatatype.DOUBLE), "Right ascension", "deg", "pos.eq.ra", null); + resultingColumns[2] = new TAPColumn("deg", new DBType(DBDatatype.DOUBLE), "Declination", "deg", "pos.eq.dec", null); + resultingColumns[3] = new TAPColumn("gmag", new DBType(DBDatatype.DOUBLE), "G magnitude", "mag", "phot.mag;em.opt.B", null); + + if (!svFile.exists()) + svFile.createNewFile(); + } + + @AfterClass + public static void tearDownAfterClass() throws Exception{ + DBTools.closeConnection(conn); + svFile.delete(); + } + + @Test + public void testWriteResult(){ + ResultSet rs = null; + try{ + rs = DBTools.select(conn, "SELECT id, ra, deg, gmag FROM gums LIMIT 10;"); + + HashMap tapParams = new HashMap(1); + tapParams.put(TAPJob.PARAM_MAX_REC, "100"); + TAPParameters params = new TAPParameters(serviceConn, tapParams); + TAPExecutionReport report = new TAPExecutionReport("123456A", true, params); + report.resultingColumns = resultingColumns; + + TableIterator it = new ResultSetTableIterator(rs); + + SVFormat formatter = new SVFormat(serviceConn, SVFormat.COMMA_SEPARATOR); + OutputStream output = new BufferedOutputStream(new FileOutputStream(svFile)); + formatter.writeResult(it, output, report, Thread.currentThread()); + output.close(); + + assertTrue(CommandExecute.execute("wc -l < \"" + svFile.getAbsolutePath() + "\"").trim().equals("11")); + + }catch(Exception t){ + t.printStackTrace(); + fail("Unexpected exception!"); + }finally{ + if (rs != null){ + try{ + rs.close(); + }catch(SQLException se){} + } + } + } + + @Test + public void testWriteResultWithOverflow(){ + ResultSet rs = null; + try{ + rs = DBTools.select(conn, "SELECT id, ra, deg, gmag FROM gums LIMIT 10;"); + + HashMap tapParams = new HashMap(1); + tapParams.put(TAPJob.PARAM_MAX_REC, "5"); + TAPParameters params = new TAPParameters(serviceConn, tapParams); + TAPExecutionReport report = new TAPExecutionReport("123456A", true, params); + report.resultingColumns = resultingColumns; + + TableIterator it = new ResultSetTableIterator(rs); + + SVFormat formatter = new SVFormat(serviceConn, SVFormat.COMMA_SEPARATOR); + OutputStream output = new BufferedOutputStream(new FileOutputStream(svFile)); + formatter.writeResult(it, output, report, Thread.currentThread()); + output.close(); + + assertTrue(CommandExecute.execute("wc -l < \"" + svFile.getAbsolutePath() + "\"").trim().equals("6")); + + }catch(Exception t){ + t.printStackTrace(); + fail("Unexpected exception!"); + }finally{ + if (rs != null){ + try{ + rs.close(); + }catch(SQLException e){ + System.err.println("Can not close the RESULTSET!"); + e.printStackTrace(); + } + } + } + } + +} diff --git a/test/tap/formatter/ServiceConnection4Test.java b/test/tap/formatter/ServiceConnection4Test.java new file mode 100644 index 0000000..074448a --- /dev/null +++ b/test/tap/formatter/ServiceConnection4Test.java @@ -0,0 +1,139 @@ +package tap.formatter; + +import java.util.Collection; +import java.util.Iterator; + +import tap.ServiceConnection; +import tap.TAPFactory; +import tap.log.TAPLog; +import tap.metadata.TAPMetadata; +import uws.service.UserIdentifier; +import uws.service.file.UWSFileManager; +import adql.db.FunctionDef; + +public class ServiceConnection4Test implements ServiceConnection { + + @Override + public int[] getOutputLimit(){ + return new int[]{1000000,1000000}; + } + + @Override + public LimitUnit[] getOutputLimitType(){ + return new LimitUnit[]{LimitUnit.bytes,LimitUnit.bytes}; + } + + @Override + public String getProviderName(){ + return null; + } + + @Override + public String getProviderDescription(){ + return null; + } + + @Override + public boolean isAvailable(){ + return true; + } + + @Override + public String getAvailability(){ + return "AVAILABLE"; + } + + @Override + public int[] getRetentionPeriod(){ + return null; + } + + @Override + public int[] getExecutionDuration(){ + return null; + } + + @Override + public UserIdentifier getUserIdentifier(){ + return null; + } + + @Override + public boolean uploadEnabled(){ + return false; + } + + @Override + public int[] getUploadLimit(){ + return null; + } + + @Override + public LimitUnit[] getUploadLimitType(){ + return null; + } + + @Override + public int getMaxUploadSize(){ + return 0; + } + + @Override + public TAPMetadata getTAPMetadata(){ + return null; + } + + @Override + public Collection getCoordinateSystems(){ + return null; + } + + @Override + public Collection getGeometries(){ + return null; + } + + @Override + public Collection getUDFs(){ + return null; + } + + @Override + public TAPLog getLogger(){ + return null; + } + + @Override + public TAPFactory getFactory(){ + return null; + } + + @Override + public UWSFileManager getFileManager(){ + return null; + } + + @Override + public Iterator getOutputFormats(){ + return null; + } + + @Override + public OutputFormat getOutputFormat(String mimeOrAlias){ + return null; + } + + @Override + public int getNbMaxAsyncJobs(){ + return -1; + } + + @Override + public void setAvailable(boolean isAvailable, String message){} + + @Override + public int[] getFetchSize(){ + return null; + } + +} \ No newline at end of file diff --git a/test/tap/formatter/TextFormatTest.java b/test/tap/formatter/TextFormatTest.java new file mode 100644 index 0000000..dfb266e --- /dev/null +++ b/test/tap/formatter/TextFormatTest.java @@ -0,0 +1,136 @@ +package tap.formatter; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStream; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.HashMap; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import tap.ServiceConnection; +import tap.TAPExecutionReport; +import tap.TAPJob; +import tap.data.ResultSetTableIterator; +import tap.data.TableIterator; +import tap.metadata.TAPColumn; +import tap.parameters.TAPParameters; +import testtools.CommandExecute; +import testtools.DBTools; +import adql.db.DBType; +import adql.db.DBType.DBDatatype; + +/** + *

    Test the TestFormat function {@link TestFormat#writeResult(TableIterator, OutputStream, TAPExecutionReport, Thread)}.

    + * + *

    2 test ares done: 1 with an overflow and another without.

    + * + * @author Grégory Mantelet (ARI) + * @version 2.0 (09/2014) + */ +public class TextFormatTest { + + private static Connection conn; + private static ServiceConnection serviceConn; + private static TAPColumn[] resultingColumns; + private static File textFile = new File("/home/gmantele/Desktop/text_test.txt"); + + @BeforeClass + public static void setUpBeforeClass() throws Exception{ + conn = DBTools.createConnection("postgresql", "127.0.0.1", null, "gmantele", "gmantele", "pwd"); + serviceConn = new ServiceConnection4Test(); + + resultingColumns = new TAPColumn[4]; + resultingColumns[0] = new TAPColumn("ID", new DBType(DBDatatype.VARCHAR)); + resultingColumns[1] = new TAPColumn("ra", new DBType(DBDatatype.DOUBLE), "Right ascension", "deg", "pos.eq.ra", null); + resultingColumns[2] = new TAPColumn("deg", new DBType(DBDatatype.DOUBLE), "Declination", "deg", "pos.eq.dec", null); + resultingColumns[3] = new TAPColumn("gmag", new DBType(DBDatatype.DOUBLE), "G magnitude", "mag", "phot.mag;em.opt.B", null); + + if (!textFile.exists()) + textFile.createNewFile(); + } + + @AfterClass + public static void tearDownAfterClass() throws Exception{ + DBTools.closeConnection(conn); + textFile.delete(); + } + + @Test + public void testWriteResult(){ + ResultSet rs = null; + try{ + rs = DBTools.select(conn, "SELECT id, ra, deg, gmag FROM gums LIMIT 10;"); + + HashMap tapParams = new HashMap(1); + tapParams.put(TAPJob.PARAM_MAX_REC, "100"); + TAPParameters params = new TAPParameters(serviceConn, tapParams); + TAPExecutionReport report = new TAPExecutionReport("123456A", true, params); + report.resultingColumns = resultingColumns; + + TableIterator it = new ResultSetTableIterator(rs); + + TextFormat formatter = new TextFormat(serviceConn); + OutputStream output = new BufferedOutputStream(new FileOutputStream(textFile)); + formatter.writeResult(it, output, report, Thread.currentThread()); + output.close(); + + assertTrue(CommandExecute.execute("wc -l < \"" + textFile.getAbsolutePath() + "\"").trim().equals("12")); + + }catch(Exception t){ + t.printStackTrace(); + fail("Unexpected exception!"); + }finally{ + if (rs != null){ + try{ + rs.close(); + }catch(SQLException se){} + } + } + } + + @Test + public void testWriteResultWithOverflow(){ + ResultSet rs = null; + try{ + rs = DBTools.select(conn, "SELECT id, ra, deg, gmag FROM gums LIMIT 10;"); + + HashMap tapParams = new HashMap(1); + tapParams.put(TAPJob.PARAM_MAX_REC, "5"); + TAPParameters params = new TAPParameters(serviceConn, tapParams); + TAPExecutionReport report = new TAPExecutionReport("123456A", true, params); + report.resultingColumns = resultingColumns; + + TableIterator it = new ResultSetTableIterator(rs); + + TextFormat formatter = new TextFormat(serviceConn); + OutputStream output = new BufferedOutputStream(new FileOutputStream(textFile)); + formatter.writeResult(it, output, report, Thread.currentThread()); + output.close(); + + assertTrue(CommandExecute.execute("wc -l < \"" + textFile.getAbsolutePath() + "\"").trim().equals("7")); + + }catch(Exception t){ + t.printStackTrace(); + fail("Unexpected exception!"); + }finally{ + if (rs != null){ + try{ + rs.close(); + }catch(SQLException e){ + System.err.println("Can not close the RESULTSET!"); + e.printStackTrace(); + } + } + } + } + +} diff --git a/test/tap/formatter/VOTableFormatTest.java b/test/tap/formatter/VOTableFormatTest.java new file mode 100644 index 0000000..eb28fde --- /dev/null +++ b/test/tap/formatter/VOTableFormatTest.java @@ -0,0 +1,141 @@ +package tap.formatter; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStream; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.HashMap; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import tap.ServiceConnection; +import tap.TAPExecutionReport; +import tap.TAPJob; +import tap.data.ResultSetTableIterator; +import tap.data.TableIterator; +import tap.metadata.TAPColumn; +import tap.parameters.TAPParameters; +import testtools.CommandExecute; +import testtools.DBTools; +import uk.ac.starlink.votable.DataFormat; +import adql.db.DBType; +import adql.db.DBType.DBDatatype; + +/** + *

    Test the VOTableFormat function {@link VOTableFormat#writeResult(TableIterator, OutputStream, TAPExecutionReport, Thread)}.

    + * + *

    2 test ares done: 1 with an overflow and another without.

    + * + * @author Grégory Mantelet (ARI) + * @version 2.0 (09/2014) + */ +public class VOTableFormatTest { + + private static Connection conn; + private static ServiceConnection serviceConn; + private static TAPColumn[] resultingColumns; + private static File votableFile = new File("/home/gmantele/Desktop/votable_test.xml"); + + @BeforeClass + public static void setUpBeforeClass() throws Exception{ + conn = DBTools.createConnection("postgresql", "127.0.0.1", null, "gmantele", "gmantele", "pwd"); + serviceConn = new ServiceConnection4Test(); + + resultingColumns = new TAPColumn[4]; + resultingColumns[0] = new TAPColumn("ID", new DBType(DBDatatype.VARCHAR)); + resultingColumns[1] = new TAPColumn("ra", new DBType(DBDatatype.DOUBLE), "Right ascension", "deg", "pos.eq.ra", null); + resultingColumns[2] = new TAPColumn("deg", new DBType(DBDatatype.DOUBLE), "Declination", "deg", "pos.eq.dec", null); + resultingColumns[3] = new TAPColumn("gmag", new DBType(DBDatatype.DOUBLE), "G magnitude", "mag", "phot.mag;em.opt.B", null); + + if (!votableFile.exists()) + votableFile.createNewFile(); + } + + @AfterClass + public static void tearDownAfterClass() throws Exception{ + DBTools.closeConnection(conn); + votableFile.delete(); + } + + @Test + public void testWriteResult(){ + ResultSet rs = null; + try{ + rs = DBTools.select(conn, "SELECT id, ra, deg, gmag FROM gums LIMIT 10;"); + + HashMap tapParams = new HashMap(1); + tapParams.put(TAPJob.PARAM_MAX_REC, "100"); + TAPParameters params = new TAPParameters(serviceConn, tapParams); + TAPExecutionReport report = new TAPExecutionReport("123456A", true, params); + report.resultingColumns = resultingColumns; + + TableIterator it = new ResultSetTableIterator(rs); + + VOTableFormat formatter = new VOTableFormat(serviceConn, DataFormat.TABLEDATA); + OutputStream output = new BufferedOutputStream(new FileOutputStream(votableFile)); + formatter.writeResult(it, output, report, Thread.currentThread()); + output.close(); + + // note: due to the pipe (|), we must call /bin/sh as a command whose the command to execute in is the "grep ... | wc -l": + assertEquals("10", CommandExecute.execute("grep \"\" \"" + votableFile.getAbsolutePath() + "\" | wc -l").trim()); + assertEquals("0", CommandExecute.execute("grep \"\" \"" + votableFile.getAbsolutePath() + "\" | wc -l").trim()); + + }catch(Exception t){ + t.printStackTrace(); + fail("Unexpected exception!"); + }finally{ + if (rs != null){ + try{ + rs.close(); + }catch(SQLException se){} + } + } + } + + @Test + public void testWriteResultWithOverflow(){ + ResultSet rs = null; + try{ + rs = DBTools.select(conn, "SELECT id, ra, deg, gmag FROM gums LIMIT 10;"); + + HashMap tapParams = new HashMap(1); + tapParams.put(TAPJob.PARAM_MAX_REC, "5"); + TAPParameters params = new TAPParameters(serviceConn, tapParams); + TAPExecutionReport report = new TAPExecutionReport("123456A", true, params); + report.resultingColumns = resultingColumns; + + TableIterator it = new ResultSetTableIterator(rs); + + VOTableFormat formatter = new VOTableFormat(serviceConn, DataFormat.TABLEDATA); + OutputStream output = new BufferedOutputStream(new FileOutputStream(votableFile)); + formatter.writeResult(it, output, report, Thread.currentThread()); + output.close(); + + // note: due to the pipe (|), we must call /bin/sh as a command whose the command to execute in is the "grep ... | wc -l": + assertEquals("5", CommandExecute.execute("grep \"\" \"" + votableFile.getAbsolutePath() + "\" | wc -l").trim()); + assertEquals("1", CommandExecute.execute("grep \"\" \"" + votableFile.getAbsolutePath() + "\" | wc -l").trim()); + + }catch(Exception t){ + t.printStackTrace(); + fail("Unexpected exception!"); + }finally{ + if (rs != null){ + try{ + rs.close(); + }catch(SQLException e){ + System.err.println("Can not close the RESULTSET!"); + e.printStackTrace(); + } + } + } + } + +} diff --git a/test/tap/metadata/MetadataExtractionTest.java b/test/tap/metadata/MetadataExtractionTest.java new file mode 100644 index 0000000..bc35a12 --- /dev/null +++ b/test/tap/metadata/MetadataExtractionTest.java @@ -0,0 +1,147 @@ +package tap.metadata; + +/* + * This file is part of TAPLibrary. + * + * TAPLibrary is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * TAPLibrary is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with TAPLibrary. If not, see . + * + * Copyright 2014 - Astronomisches Rechen Institute (ARI) + */ + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import tap.metadata.TAPTable.TableType; + +/** + * @author Grégory Mantelet (ARI) - gmantele@ari.uni-heidelberg.de + * @version 1.1 (04/2014) + */ +public class MetadataExtractionTest { + + public static void main(String[] args) throws Throwable{ + MetadataExtractionTest extractor = new MetadataExtractionTest(); + try{ + extractor.connect(); + extractor.printTableMetadata("gums"); + }finally{ + extractor.close(); + } + } + + private Connection connection = null; + private Statement statement = null; + + public void connect(){ + try{ + Class.forName("org.postgresql.Driver"); + connection = DriverManager.getConnection("jdbc:postgresql:gmantele", "gmantele", "pwd"); + statement = connection.createStatement(); + System.out.println("[OK] DB connection successfully established !"); + }catch(ClassNotFoundException notFoundException){ + notFoundException.printStackTrace(); + System.err.println("[ERROR] Connection error !"); + }catch(SQLException sqlException){ + sqlException.printStackTrace(); + System.err.println("[ERROR] Connection error !"); + } + } + + public ResultSet query(String requet){ + ResultSet resultat = null; + try{ + resultat = statement.executeQuery(requet); + }catch(SQLException e){ + e.printStackTrace(); + System.out.println("Erreur dans la requête: " + requet); + } + return resultat; + + } + + public TAPSchema printTableMetadata(final String table){ + try{ + + DatabaseMetaData dbMeta = connection.getMetaData(); + TAPSchema tapSchema = null; + TAPTable tapTable = null; + + // Extract Table metadata (schema, table, type): + ResultSet rs = dbMeta.getTables(null, null, table, null); + rs.last(); + if (rs.getRow() == 0) + System.err.println("[ERROR] No found table for \"" + table + "\" !"); + else if (rs.getRow() > 1){ + rs.first(); + System.err.println("[ERROR] More than one match for \"" + table + "\":"); + while(rs.next()) + System.err.println(rs.getString(2) + "." + rs.getString(3) + " : " + rs.getString(4)); + }else{ + rs.first(); + tapSchema = new TAPSchema(rs.getString(2)); + TableType tableType = TableType.table; + if (rs.getString(4) != null){ + try{ + tableType = TableType.valueOf(rs.getString(4)); + }catch(IllegalArgumentException iae){} + } + tapTable = new TAPTable(rs.getString(3), tableType); + tapSchema.addTable(tapTable); + System.out.println("[OK] 1 table FOUND ! => " + tapTable + " : " + tapTable.getType()); + } + + // Extract all columns metadata (type, precision, scale): + rs = dbMeta.getColumns(null, tapSchema.getDBName(), tapTable.getDBName(), null); + String type; + while(rs.next()){ + type = rs.getString(6); + if (type.endsWith("char") || type.equals("numeric")){ + type += "(" + rs.getInt(7); + if (type.startsWith("numeric")) + type += "," + rs.getInt(9); + type += ")"; + } + System.out.println(" * " + rs.getString(4) + " : " + type); + } + + // Extract all indexed columns: + rs = dbMeta.getIndexInfo(null, tapSchema.getDBName(), tapTable.getDBName(), false, true); + while(rs.next()){ + System.out.println(" # " + rs.getString(6) + " : " + rs.getShort(7) + " (unique ? " + (!rs.getBoolean(4)) + ") -> " + rs.getString(9) + " => " + rs.getInt(11) + " unique values in the index ; " + rs.getInt(12) + " pages"); + } + + return tapSchema; + + }catch(SQLException e){ + e.printStackTrace(); + return null; + } + } + + public void close(){ + try{ + connection.close(); + statement.close(); + System.out.println("[OK] Connection closed !"); + }catch(SQLException e){ + e.printStackTrace(); + System.out.println("[ERROR] Connection CAN NOT be closed !"); + } + } + +} diff --git a/test/tap/metadata/TableSetParserTest.java b/test/tap/metadata/TableSetParserTest.java new file mode 100644 index 0000000..ed42f40 --- /dev/null +++ b/test/tap/metadata/TableSetParserTest.java @@ -0,0 +1,1162 @@ +package tap.metadata; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.StringBufferInputStream; +import java.util.ArrayList; + +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamConstants; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import tap.TAPException; +import tap.metadata.TableSetParser.ForeignKey; +import adql.db.DBType; +import adql.db.DBType.DBDatatype; + +@SuppressWarnings("deprecation") +public class TableSetParserTest { + + private static TableSetParser parser = null; + private static XMLInputFactory factory = null; + + private static final String namespaceDef = "xmlns:vs=\"http://www.ivoa.net/xml/VODataService/v1.1\" xmlns:vtm=\"http://www.ivoa.net/xml/VOSITables/v1.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://www.ivoa.net/xml/VODataService/v1.1 http://vo.ari.uni-heidelberg.de/docs/schemata/VODataService-v1.1.xsd http://www.ivoa.net/xml/VOSITables/v1.0 http://vo.ari.uni-heidelberg.de/docs/schemata/VOSITables-v1.0.xsd\""; + + @BeforeClass + public static void setUpBeforeClass() throws Exception{ + // Build an empty parser: + parser = new TableSetParser(); + + // Build the XML factory: + factory = XMLInputFactory.newInstance(); + } + + @AfterClass + public static void tearDownAfterClass() throws Exception{} + + @Before + public void setUp() throws Exception{} + + @After + public void tearDown() throws Exception{} + + private static XMLStreamReader buildReader(final String xmlContent) throws XMLStreamException{ + return factory.createXMLStreamReader(new StringBufferInputStream(xmlContent)); + } + + private static void close(final XMLStreamReader reader){ + if (reader != null){ + try{ + reader.close(); + }catch(Throwable t){} + } + } + + @Test + public void testGetPosition(){ + XMLStreamReader reader = null; + try{ + + // Build a reader with an empty XML document: + reader = buildReader(""); + assertEquals("[l.1,c.1]", parser.getPosition(reader)); + // note: reader.next() is throwing an error on an empty document => no need to test that. + close(reader); + + // Build a reader with a simple XML: + reader = buildReader("node value"); + + // Position before starting reading: + assertEquals("[l.1,c.1]", parser.getPosition(reader)); + + // Position after getting the node: + reader.next(); // START_ELEMENT("A") + assertEquals("[l.1,c.23]", parser.getPosition(reader)); + // The position after getting an attribute should not change: + reader.getAttributeLocalName(0); // ATTRIBUTE("attrValue") + assertEquals("[l.1,c.23]", parser.getPosition(reader)); + + // Position after getting the text: + reader.next(); // CHARACTERS("node value") + assertEquals("[l.1,c.35]", parser.getPosition(reader)); + + // Position after getting the node ending tag: + reader.next(); // END_ELEMENT("A") + assertEquals("[l.1,c.37]", parser.getPosition(reader)); + + // Position once the end reached: + reader.next(); // NULL + assertEquals("[l.-1,c.-1]", parser.getPosition(reader)); + + }catch(Exception e){ + e.printStackTrace(); + if (e instanceof XMLStreamException) + fail("Unexpected error while reading the XML content: " + e.getMessage()); + else + fail("Unexpected error: " + e.getMessage()); + }finally{ + close(reader); + } + } + + @Test + public void testGoToEndTag(){ + XMLStreamReader reader = null; + try{ + + /* Test with a single empty node AND WITH NULL or "" + * => NO TAG SHOULD HAVE BEEN READ: */ + // CASE: null + reader = buildReader(""); + parser.goToEndTag(reader, null); + assertEquals("[l.1,c.1]", parser.getPosition(reader)); + close(reader); + // CASE: empty string + reader = buildReader(""); + parser.goToEndTag(reader, ""); + assertEquals("[l.1,c.1]", parser.getPosition(reader)); + close(reader); + + /* Test BEFORE having read the start element: + * => AN EXCEPTION SHOULD BE THROWN */ + reader = buildReader(""); + try{ + parser.goToEndTag(reader, "A"); + fail("This function should have failed: the START ELEMENT has not yet been read!"); + }catch(Exception e){ + if (e instanceof TAPException) + assertEquals("[l.-1,c.-1] Malformed XML document: missing an END TAG !", e.getMessage()); + else + throw e; + } + close(reader); + + /* Test AFTER having read the start element: + * => NORMAL USAGE */ + reader = buildReader(""); + reader.next(); // START ELEMENT("A") + parser.goToEndTag(reader, "A"); + assertEquals("[l.1,c.8]", parser.getPosition(reader)); + close(reader); + + /* Test AFTER having read the start element: + * => NORMAL USAGE with an embedded node */ + // search for the root node end: + reader = buildReader(""); + reader.next(); // START ELEMENT("A") + parser.goToEndTag(reader, "A"); + assertEquals("[l.1,c.15]", parser.getPosition(reader)); + close(reader); + // variant with some texts: + reader = buildReader("super blabla"); + reader.next(); // START ELEMENT("A") + parser.goToEndTag(reader, "A"); + assertEquals("[l.1,c.27]", parser.getPosition(reader)); + close(reader); + // variant with some texts + child node: + reader = buildReader("superblabla"); + reader.next(); // START ELEMENT("A") + parser.goToEndTag(reader, "A"); + assertEquals("[l.1,c.33]", parser.getPosition(reader)); + close(reader); + // search for the child node end: + reader = buildReader(""); + reader.next(); // START ELEMENT("A") + reader.next(); // START ELEMENT("B") + parser.goToEndTag(reader, "B"); + assertEquals("[l.1,c.11]", parser.getPosition(reader)); + close(reader); + // variant with some texts: + reader = buildReader("super blabla"); + reader.next(); // START ELEMENT("A") + reader.next(); // START ELEMENT("B") + parser.goToEndTag(reader, "B"); + assertEquals("[l.1,c.23]", parser.getPosition(reader)); + close(reader); + // variant with some texts + child node: + reader = buildReader("superblabla"); + reader.next(); // START ELEMENT("A") + reader.next(); // START ELEMENT("B") + parser.goToEndTag(reader, "B"); + assertEquals("[l.1,c.29]", parser.getPosition(reader)); + close(reader); + + // Test: Search the end tag while the reader is inside one of its children: + reader = buildReader("superblabla"); + reader.next(); // START ELEMENT("A") + reader.next(); // START ELEMENT("B") + parser.goToEndTag(reader, "A"); + assertEquals("[l.1,c.33]", parser.getPosition(reader)); + close(reader); + + // Test with a wrong start node name: + reader = buildReader(""); + reader.next(); // START ELEMENT("A") + try{ + parser.goToEndTag(reader, "B"); + fail("This function should have failed: the given node name is wrong (no such node in the XML document)!"); + }catch(Exception e){ + if (e instanceof TAPException) + assertEquals("[l.-1,c.-1] Malformed XML document: missing an END TAG !", e.getMessage()); + else + throw e; + } + close(reader); + + // Test with malformed XML document: + // CASE: missing end tag for the root node: + reader = buildReader(""); + reader.next(); // START ELEMENT("A") + try{ + parser.goToEndTag(reader, "A"); + fail("This function should have failed: the node A has no END TAG!"); + }catch(Exception e){ + if (e instanceof XMLStreamException) + assertEquals("ParseError at [row,col]:[1,11]\nMessage: XML document structures must start and end within the same entity.", e.getMessage()); + else + throw e; + } + close(reader); + // CASE: missing end tag for a child: + reader = buildReader(""); + reader.next(); // START ELEMENT("A") + try{ + parser.goToEndTag(reader, "A"); + fail("This function should have failed: the node B has no END TAG!"); + }catch(Exception e){ + if (e instanceof XMLStreamException) + assertEquals("ParseError at [row,col]:[1,9]\nMessage: The element type \"B\" must be terminated by the matching end-tag \"\".", e.getMessage()); + else + throw e; + } + close(reader); + // CASE: missing end tag for the child to search: + reader = buildReader(""); + reader.next(); // START ELEMENT("A") + reader.next(); // START ELEMENT("B") + try{ + parser.goToEndTag(reader, "B"); + fail("This function should have failed: the node B has no END TAG!"); + }catch(Exception e){ + if (e instanceof XMLStreamException) + assertEquals("ParseError at [row,col]:[1,9]\nMessage: The element type \"B\" must be terminated by the matching end-tag \"\".", e.getMessage()); + else + throw e; + } + close(reader); + + }catch(Exception e){ + e.printStackTrace(); + if (e instanceof XMLStreamException) + fail("Unexpected error while reading the XML content: " + e.getMessage()); + else + fail("Unexpected error: " + e.getMessage()); + }finally{ + close(reader); + } + } + + @Test + public void testGetText(){ + XMLStreamReader reader = null; + String txt; + try{ + + // Test with a simple XML and an empty text: + reader = buildReader(""); + txt = parser.getText(reader); + assertEquals(0, txt.length()); + assertEquals("[l.1,c.4]", parser.getPosition(reader)); + assertEquals(XMLStreamConstants.START_ELEMENT, reader.getEventType()); + close(reader); + // variant with spaces and tabs: + reader = buildReader(" "); + txt = parser.getText(reader); + assertEquals(0, txt.length()); + assertEquals("[l.1,c.8]", parser.getPosition(reader)); + assertEquals(XMLStreamConstants.START_ELEMENT, reader.getEventType()); + close(reader); + // variant with line returns: + reader = buildReader(" \n "); + txt = parser.getText(reader); + assertEquals(0, txt.length()); + assertEquals("[l.2,c.5]", parser.getPosition(reader)); + assertEquals(XMLStreamConstants.START_ELEMENT, reader.getEventType()); + close(reader); + + // Test with a single line text: + reader = buildReader(" Super blabla "); + reader.next(); // START ELEMENT("A") + txt = parser.getText(reader); + assertEquals("Super blabla", txt); + assertEquals("[l.1,c.27]", parser.getPosition(reader)); + assertEquals(XMLStreamConstants.END_ELEMENT, reader.getEventType()); + close(reader); + // variant with CDATA: + reader = buildReader(" Super "); + reader.next(); // START ELEMENT("A") + txt = parser.getText(reader); + assertEquals("Super blabla", txt); + assertEquals("[l.1,c.39]", parser.getPosition(reader)); + assertEquals(XMLStreamConstants.END_ELEMENT, reader.getEventType()); + close(reader); + + // Test with a text of 2 lines: + reader = buildReader(" Super \n blabla "); + reader.next(); // START ELEMENT("A") + txt = parser.getText(reader); + assertEquals("Super\nblabla", txt); + assertEquals("[l.2,c.18]", parser.getPosition(reader)); + assertEquals(XMLStreamConstants.END_ELEMENT, reader.getEventType()); + close(reader); + // same test but with an empty line between both: + reader = buildReader(" Super \n \n blabla "); + reader.next(); // START ELEMENT("A") + txt = parser.getText(reader); + assertEquals("Super\n\nblabla", txt); + assertEquals("[l.3,c.18]", parser.getPosition(reader)); + assertEquals(XMLStreamConstants.END_ELEMENT, reader.getEventType()); + close(reader); + // same test but starting with an empty line: + reader = buildReader("\n Super \n bla bla "); + reader.next(); // START ELEMENT("A") + txt = parser.getText(reader); + assertEquals("Super\nbla bla", txt); + assertEquals("[l.3,c.20]", parser.getPosition(reader)); + assertEquals(XMLStreamConstants.END_ELEMENT, reader.getEventType()); + close(reader); + // same test but a comment splitting a text part: + reader = buildReader(" Super \n bla bla "); + reader.next(); // START ELEMENT("A") + txt = parser.getText(reader); + assertEquals("Super\nbla bla", txt); + assertEquals("[l.2,c.44]", parser.getPosition(reader)); + assertEquals(XMLStreamConstants.END_ELEMENT, reader.getEventType()); + close(reader); + + }catch(Exception e){ + e.printStackTrace(); + if (e instanceof XMLStreamException) + fail("Unexpected error while reading the XML content: " + e.getMessage()); + else + fail("Unexpected error: " + e.getMessage()); + }finally{ + close(reader); + } + } + + @Test + public void testSearchTable(){ + try{ + + // Create fake metadata: + TAPMetadata meta = new TAPMetadata(); + TAPSchema schema = new TAPSchema("SA"); + schema.addTable("TA"); + schema.addTable("TB"); + meta.addSchema(schema); + schema = new TAPSchema("SB"); + schema.addTable("TB"); + meta.addSchema(schema); + + // Create a fake position: + final String pos = "[l.10,c.1]"; + + // Search for an existing table WITHOUT SCHEMA specification: + TAPTable t = parser.searchTable("TA", meta, pos); + assertEquals("TA", t.getADQLName()); + assertEquals("SA", t.getADQLSchemaName()); + // variant with a different case: + t = parser.searchTable("ta", meta, pos); + assertEquals("TA", t.getADQLName()); + assertEquals("SA", t.getADQLSchemaName()); + + // Search for an existing table WITH SCHEMA specification: + t = parser.searchTable("SA.TA", meta, pos); + assertEquals("TA", t.getADQLName()); + assertEquals("SA", t.getADQLSchemaName()); + // variant with a different case: + t = parser.searchTable("sa.ta", meta, pos); + assertEquals("TA", t.getADQLName()); + assertEquals("SA", t.getADQLSchemaName()); + + // Search with a wrong table name: + try{ + parser.searchTable("TC", meta, pos); + fail("This test should have not failed: there is no table named TC in the given metadata."); + }catch(Exception e){ + if (e instanceof TAPException) + assertEquals(pos + " Unknown table: \"TC\"!", e.getMessage()); + else + throw e; + } + // variant with a correct schema name: + try{ + parser.searchTable("SA.TC", meta, pos); + fail("This test should have not failed: there is no table named SA.TC in the given metadata."); + }catch(Exception e){ + if (e instanceof TAPException) + assertEquals(pos + " Unknown table: \"SA.TC\"!", e.getMessage()); + else + throw e; + } + + // Search with a wrong schema name: + try{ + parser.searchTable("SC.TB", meta, pos); + fail("This test should have not failed: there is no schema named SC in the given metadata."); + }catch(Exception e){ + if (e instanceof TAPException) + assertEquals(pos + " Unknown table: \"SC.TB\"!", e.getMessage()); + else + throw e; + } + + // Search with an ambiguous table name (missing schema name): + try{ + parser.searchTable("TB", meta, pos); + fail("This test should have not failed: there are two table named TB ; a schema name is required to choose the table to select."); + }catch(Exception e){ + if (e instanceof TAPException) + assertEquals(pos + " Unresolved table: \"TB\"! Several tables have the same name but in different schemas (here: SA.TB, SB.TB). You must prefix this table name by a schema name (expected syntax: \"schema.table\").", e.getMessage()); + else + throw e; + } + + // Provide a schema + table name with a wrong syntax (missing table name or schema name): + try{ + parser.searchTable(".TB", meta, pos); + fail("This test should have not failed: the schema name is missing before the '.'."); + }catch(Exception e){ + if (e instanceof TAPException) + assertEquals(pos + " Incorrect full table name - \".TB\": empty schema name!", e.getMessage()); + else + throw e; + } + try{ + parser.searchTable("SB.", meta, pos); + fail("This test should have not failed: the table name is missing after the '.'."); + }catch(Exception e){ + if (e instanceof TAPException) + assertEquals(pos + " Incorrect full table name - \"SB.\": empty table name!", e.getMessage()); + else + throw e; + } + try{ + parser.searchTable("toto.SB.TB", meta, pos); + fail("This test should have not failed: the table name is missing after the '.'."); + }catch(Exception e){ + if (e instanceof TAPException) + assertEquals(pos + " Incorrect full table name - \"toto.SB.TB\": only a schema and a table name can be specified (expected syntax: \"schema.table\")\"!", e.getMessage()); + else + throw e; + } + + }catch(Exception e){ + e.printStackTrace(); + fail("Unexpected error: " + e.getMessage()); + } + } + + @Test + public void testParseFKey(){ + XMLStreamReader reader = null; + try{ + + // Test while search outside from the foreignKey node: + reader = buildReader("SA.TBtruc.choseForeign key\ndescription.col1col2
    "); + reader.next(); // START ELEMENT("table") + try{ + parser.parseFKey(reader); + fail("This test should have failed: the reader has not just read the \"foreignKey\" START ELEMENT tag."); + }catch(Exception e){ + if (e instanceof IllegalStateException) + assertEquals("[l.1,c.8] Illegal usage of TableSetParser.parseFKey(XMLStreamParser)! This function can be called only when the reader has just read the START ELEMENT tag \"foreignKey\".", e.getMessage()); + else + throw e; + }finally{ + close(reader); + } + + // Test with a complete and correct XML foreignKey node: + reader = buildReader("SA.TBtruc.choseForeign key\ndescription.col1col2"); + reader.next(); // START ELEMENT("foreignKey") + ForeignKey fk = parser.parseFKey(reader); + assertEquals("SA.TB", fk.targetTable); + assertEquals("[l.1,c.45]", fk.targetTablePosition); + assertEquals("truc.chose", fk.utype); + assertEquals("Foreign key\ndescription.", fk.description); + assertEquals(1, fk.keyColumns.size()); + assertEquals("col2", fk.keyColumns.get("col1")); + close(reader); + // variant with some comments: + reader = buildReader("SA.TBForeign key\ndescription.col1col2"); + reader.next(); // START ELEMENT("foreignKey") + fk = parser.parseFKey(reader); + assertEquals("SA.TB", fk.targetTable); + assertEquals("Foreign key\ndescription.", fk.description); + assertEquals(1, fk.keyColumns.size()); + assertEquals("col2", fk.keyColumns.get("col1")); + close(reader); + // variant with texts at unapropriate places: + reader = buildReader("Here, we are SA.TBForeign key\ndescription.col1col2Here is the end!Nothing more!"); + reader.next(); // START ELEMENT("foreignKey") + fk = parser.parseFKey(reader); + assertEquals("SA.TB", fk.targetTable); + assertEquals("Foreign key\ndescription.", fk.description); + assertEquals(1, fk.keyColumns.size()); + assertEquals("col2", fk.keyColumns.get("col1")); + close(reader); + + // Test with a missing targetTable: + reader = buildReader("Foreign key\ndescription.col1col2"); + reader.next(); // START ELEMENT("foreignKey") + try{ + parser.parseFKey(reader); + fail("This test should have failed: the targetTable node is missing!"); + }catch(Exception e){ + if (e instanceof TAPException) + assertEquals("[l.2,c.123] Missing \"targetTable\"!", e.getMessage()); + }finally{ + close(reader); + } + // variant with duplicated targetTable: + reader = buildReader("SA.TBSA.TAForeign key\ndescription.col1col2"); + reader.next(); // START ELEMENT("foreignKey") + try{ + parser.parseFKey(reader); + fail("This test should have failed: the targetTable node is duplicated!"); + }catch(Exception e){ + if (e instanceof TAPException) + assertEquals("[l.1,c.58] Only one \"targetTable\" element can exist in a /tableset/schema/table/foreignKey!", e.getMessage()); + }finally{ + close(reader); + } + + // Test with a missing fkColumn: + reader = buildReader("SA.TBForeign key\ndescription."); + reader.next(); // START ELEMENT("foreignKey") + try{ + parser.parseFKey(reader); + fail("This test should have failed: at least 1 fkColumn node is missing!"); + }catch(Exception e){ + if (e instanceof TAPException) + assertEquals("[l.2,c.40] Missing at least one \"fkColumn\"!", e.getMessage()); + }finally{ + close(reader); + } + // variant with several fkColumn: + reader = buildReader("SA.TBForeign key\ndescription.col1col2col3col4"); + reader.next(); // START ELEMENT("foreignKey") + fk = parser.parseFKey(reader); + assertEquals("SA.TB", fk.targetTable); + assertEquals("Foreign key\ndescription.", fk.description); + assertEquals(2, fk.keyColumns.size()); + assertEquals("col2", fk.keyColumns.get("col1")); + assertEquals("col4", fk.keyColumns.get("col3")); + close(reader); + + // Test with a missing fromColumn: + reader = buildReader("SA.TBcol2"); + reader.next(); // START ELEMENT("foreignKey") + try{ + parser.parseFKey(reader); + fail("This test should have failed: the fromColumn node is missing!"); + }catch(Exception e){ + if (e instanceof TAPException) + assertEquals("[l.1,c.99] Missing \"fromColumn\"!", e.getMessage()); + }finally{ + close(reader); + } + // variant with several fromColumn: + reader = buildReader("SA.TBcol1col1biscol2"); + reader.next(); // START ELEMENT("foreignKey") + try{ + parser.parseFKey(reader); + fail("This test should have failed: sereval fromColumn are found!"); + }catch(Exception e){ + if (e instanceof TAPException) + assertEquals("[l.1,c.96] Only one \"fromColumn\" element can exist in a /tableset/schema/table/foreignKey/fkColumn !", e.getMessage()); + }finally{ + close(reader); + } + + // Test with a missing targetColumn: + reader = buildReader("SA.TBcol1"); + reader.next(); // START ELEMENT("foreignKey") + try{ + parser.parseFKey(reader); + fail("This test should have failed: the targetColumn node is missing!"); + }catch(Exception e){ + if (e instanceof TAPException) + assertEquals("[l.1,c.95] Missing \"targetColumn\"!", e.getMessage()); + }finally{ + close(reader); + } + // variant with several fromColumn: + reader = buildReader("SA.TBcol1col2col2bis"); + reader.next(); // START ELEMENT("foreignKey") + try{ + parser.parseFKey(reader); + fail("This test should have failed: several targetColumn are found!"); + }catch(Exception e){ + if (e instanceof TAPException) + assertEquals("[l.1,c.131] Only one \"targetColumn\" element can exist in a /tableset/schema/table/foreignKey/fkColumn !", e.getMessage()); + }finally{ + close(reader); + } + + // Test with a additional node: + reader = buildReader("blablaanythingSA.TBcol1col2"); + reader.next(); // START ELEMENT("foreignKey") + fk = parser.parseFKey(reader); + assertEquals("SA.TB", fk.targetTable); + assertNull(fk.description); + assertEquals(1, fk.keyColumns.size()); + assertEquals("col2", fk.keyColumns.get("col1")); + close(reader); + + }catch(Exception e){ + e.printStackTrace(); + if (e instanceof XMLStreamException) + fail("Unexpected error while reading the XML content: " + e.getMessage()); + else + fail("Unexpected error: " + e.getMessage()); + }finally{ + close(reader); + } + } + + @Test + public void testParseDataType(){ + XMLStreamReader reader = null; + try{ + + // Test while search outside from the dataType node: + reader = buildReader("char"); + reader.next(); // START ELEMENT("column") + try{ + parser.parseDataType(reader); + fail("This test should have failed: the reader has not just read the \"dataType\" START ELEMENT tag."); + }catch(Exception e){ + if (e instanceof IllegalStateException) + assertEquals("[l.1,c.408] Illegal usage of TableSetParser.parseDataType(XMLStreamParser)! This function can be called only when the reader has just read the START ELEMENT tag \"dataType\".", e.getMessage()); + else + throw e; + }finally{ + close(reader); + } + + // Test with a correct TAP type: + reader = buildReader("varchar"); + reader.next(); // START ELEMENT("column") + reader.next(); // START ELEMENT("dataType") + DBType dt = parser.parseDataType(reader); + assertEquals(DBDatatype.VARCHAR, dt.type); + assertEquals(-1, dt.length); + close(reader); + + // Test with a correct VOTable type: + reader = buildReader("char"); + reader.next(); // START ELEMENT("dataType") + dt = parser.parseDataType(reader); + assertEquals(DBDatatype.VARCHAR, dt.type); + assertEquals(-1, dt.length); + close(reader); + + // Test with a missing xsi:type: + reader = buildReader("char"); + reader.next(); // START ELEMENT("dataType") + try{ + parser.parseDataType(reader); + fail("This test should have failed: the attribute xsi:type is missing!"); + }catch(Exception e){ + if (e instanceof TAPException) + assertEquals("[l.1,c.424] Missing attribute \"xsi:type\" (where xsi = \"" + TableSetParser.XSI_NAMESPACE + "\")! Expected attribute value: vs:VOTableType or vs:TAPType, where vs = " + TableSetParser.VODATASERVICE_NAMESPACE + ".", e.getMessage()); + else + throw e; + }finally{ + close(reader); + } + // variant with a wrong namespace prefix + reader = buildReader("char"); + try{ + reader.next(); // START ELEMENT("dataType") + fail("This test should have failed: the prefix of the xsi:type attribute is wrong!"); + }catch(Exception e){ + if (e instanceof XMLStreamException) + assertEquals("ParseError at [row,col]:[1,450]\nMessage: http://www.w3.org/TR/1999/REC-xml-names-19990114#AttributePrefixUnbound?dataType&xsj:type&xsj", e.getMessage()); + else + throw e; + }finally{ + close(reader); + } + // variant with a missing namespace prefix: + reader = buildReader("char"); + try{ + reader.next(); // START ELEMENT("dataType") + fail("This test should have failed: the namespace xsi is not defined!"); + }catch(Exception e){ + if (e instanceof XMLStreamException) + assertEquals("ParseError at [row,col]:[1,51]\nMessage: http://www.w3.org/TR/1999/REC-xml-names-19990114#AttributePrefixUnbound?dataType&xsi:type&xsi", e.getMessage()); + else + throw e; + }finally{ + close(reader); + } + + // Test with an unsupported xsi:type: + reader = buildReader("char"); + reader.next(); // START ELEMENT("dataType") + try{ + parser.parseDataType(reader); + fail("This test should have failed: the type foo is not defined in VODataService!"); + }catch(Exception e){ + if (e instanceof TAPException) + assertEquals("[l.1,c.457] Unsupported type: \"vs:foo\"! Expected: vs:VOTableType or vs:TAPType, where vs = " + TableSetParser.VODATASERVICE_NAMESPACE + ".", e.getMessage()); + else + throw e; + }finally{ + close(reader); + } + // variant with no namespace prefix in front of the wrong type: + reader = buildReader("char"); + reader.next(); // START ELEMENT("dataType") + try{ + parser.parseDataType(reader); + fail("This test should have failed: the namespace prefix is missing in the value of xsi:type!"); + }catch(Exception e){ + if (e instanceof TAPException) + assertEquals("[l.1,c.439] Unresolved type: \"foo\"! Missing namespace prefix.", e.getMessage()); + else + throw e; + }finally{ + close(reader); + } + + // Test with a missing datatype: + reader = buildReader(""); + reader.next(); // START ELEMENT("dataType") + try{ + parser.parseDataType(reader); + fail("This test should have failed: the datatype value is missing!"); + }catch(Exception e){ + if (e instanceof TAPException) + assertEquals("[l.1,c.443] Missing column datatype!", e.getMessage()); + else + throw e; + }finally{ + close(reader); + } + // variant with a wrong datatype: + reader = buildReader("foo"); + reader.next(); // START ELEMENT("dataType") + try{ + parser.parseDataType(reader); + fail("This test should have failed: the datatype value is unknown!"); + }catch(Exception e){ + if (e instanceof TAPException) + assertEquals("[l.1,c.446] Unknown TAPType: \"foo\"!", e.getMessage()); + else + throw e; + }finally{ + close(reader); + } + + }catch(Exception e){ + e.printStackTrace(); + if (e instanceof XMLStreamException) + fail("Unexpected error while reading the XML content: " + e.getMessage()); + else + fail("Unexpected error: " + e.getMessage()); + }finally{ + close(reader); + } + } + + @Test + public void testParseColumn(){ + XMLStreamReader reader = null; + try{ + + // Test while search outside from the column node: + reader = buildReader("col1Column\ndescription.SMALLINTtruc.choset.cdegnullableprimaryindexed
    "); + reader.next(); // START ELEMENT("table") + try{ + parser.parseColumn(reader); + fail("This test should have failed: the reader has not just read the \"column\" START ELEMENT tag."); + }catch(Exception e){ + if (e instanceof IllegalStateException) + assertEquals("[l.1,c.407] Illegal usage of TableSetParser.parseColumn(XMLStreamParser)! This function can be called only when the reader has just read the START ELEMENT tag \"column\".", e.getMessage()); + else + throw e; + }finally{ + close(reader); + } + + // Test with a complete and correct XML column node: + reader = buildReader("col1Column\ndescription.SMALLINTtruc.choset.cdegnullableprimaryindexed
    "); + reader.next(); // START ELEMENT("table") + reader.next(); // START ELEMENT("column") + TAPColumn col = parser.parseColumn(reader); + assertEquals("col1", col.getADQLName()); + assertEquals("Column\ndescription.", col.getDescription()); + assertEquals(DBDatatype.SMALLINT, col.getDatatype().type); + assertEquals(-1, col.getDatatype().length); + assertEquals("truc.chose", col.getUtype()); + assertEquals("t.c", col.getUcd()); + assertEquals("deg", col.getUnit()); + assertTrue(col.isIndexed()); + assertTrue(col.isPrincipal()); + assertTrue(col.isNullable()); + assertTrue(col.isStd()); + close(reader); + // variant with entering inside the foreignKey node (as it is done by TableSetParser): + reader = buildReader("col1Column\ndescription.SMALLINTtruc.choset.cdeg"); + reader.next(); // START ELEMENT("column") + col = parser.parseColumn(reader); + assertEquals("col1", col.getADQLName()); + assertEquals("Column\ndescription.", col.getDescription()); + assertEquals(DBDatatype.SMALLINT, col.getDatatype().type); + assertEquals(-1, col.getDatatype().length); + assertEquals("truc.chose", col.getUtype()); + assertEquals("t.c", col.getUcd()); + assertEquals("deg", col.getUnit()); + assertFalse(col.isIndexed()); + assertFalse(col.isPrincipal()); + assertFalse(col.isNullable()); + assertFalse(col.isStd()); + close(reader); + // variant with some comments: + reader = buildReader("col1Column\ndescription.SMALLINTtruc.choset.cdeg"); + reader.next(); // START ELEMENT("column") + col = parser.parseColumn(reader); + assertEquals("col1", col.getADQLName()); + assertEquals("Column\ndescription.", col.getDescription()); + assertEquals(DBDatatype.SMALLINT, col.getDatatype().type); + assertEquals(-1, col.getDatatype().length); + assertEquals("truc.chose", col.getUtype()); + assertEquals("t.c", col.getUcd()); + assertEquals("deg", col.getUnit()); + assertFalse(col.isIndexed()); + assertFalse(col.isPrincipal()); + assertFalse(col.isNullable()); + assertFalse(col.isStd()); + close(reader); + // variant with texts at unapropriate places: + reader = buildReader("Here we are col1Column\ndescription.SMALLINTtruc.choset.cdegNothing more!"); + reader.next(); // START ELEMENT("column") + col = parser.parseColumn(reader); + assertEquals("col1", col.getADQLName()); + assertEquals("Column\ndescription.", col.getDescription()); + assertEquals(DBDatatype.SMALLINT, col.getDatatype().type); + assertEquals(-1, col.getDatatype().length); + assertEquals("truc.chose", col.getUtype()); + assertEquals("t.c", col.getUcd()); + assertEquals("deg", col.getUnit()); + assertFalse(col.isIndexed()); + assertFalse(col.isPrincipal()); + assertFalse(col.isNullable()); + assertFalse(col.isStd()); + close(reader); + + // Test with a missing "name" node: + reader = buildReader(""); + reader.next(); // START ELEMENT("column") + try{ + parser.parseColumn(reader); + fail("This test should have failed: the \"name\" node is missing!"); + }catch(Exception e){ + if (e instanceof TAPException) + assertEquals("[l.1,c.18] Missing column \"name\"!", e.getMessage()); + }finally{ + close(reader); + } + // variant with duplicated "name": + reader = buildReader("col1colA"); + reader.next(); // START ELEMENT("column") + try{ + parser.parseColumn(reader); + fail("This test should have failed: the \"name\" node is duplicated!"); + }catch(Exception e){ + if (e instanceof TAPException) + assertEquals("[l.1,c.32] Only one \"name\" element can exist in a /tableset/schema/table/column!", e.getMessage()); + }finally{ + close(reader); + } + + // Test with a additional node: + reader = buildReader("col1colAblabla"); + reader.next(); // START ELEMENT("foreignKey") + col = parser.parseColumn(reader); + assertEquals("col1", col.getADQLName()); + assertNull(col.getDescription()); + assertEquals(DBDatatype.VARCHAR, col.getDatatype().type); + assertEquals(-1, col.getDatatype().length); + assertNull(col.getUtype()); + assertNull(col.getUcd()); + assertNull(col.getUnit()); + assertFalse(col.isIndexed()); + assertFalse(col.isPrincipal()); + assertFalse(col.isNullable()); + assertFalse(col.isStd()); + close(reader); + + }catch(Exception e){ + e.printStackTrace(); + if (e instanceof XMLStreamException) + fail("Unexpected error while reading the XML content: " + e.getMessage()); + else + fail("Unexpected error: " + e.getMessage()); + }finally{ + close(reader); + } + } + + @Test + public void testParseTable(){ + XMLStreamReader reader = null; + ArrayList fkeys = new ArrayList(0); + try{ + + // Test while search outside from the table node: + reader = buildReader("TableA
    "); + reader.next(); // START ELEMENT("schema") + try{ + parser.parseTable(reader, fkeys); + fail("This test should have failed: the reader has not just read the \"table\" START ELEMENT tag."); + }catch(Exception e){ + if (e instanceof IllegalStateException) + assertEquals("[l.1,c.9] Illegal usage of TableSetParser.parseTable(XMLStreamParser)! This function can be called only when the reader has just read the START ELEMENT tag \"table\".", e.getMessage()); + else + throw e; + }finally{ + close(reader); + fkeys.clear(); + } + + // Test with a complete and correct XML table node: + reader = buildReader("TableATable \ndescription.truc.choseTable titlecol1TBcol1col2
    "); + reader.next(); // START ELEMENT("table") + TAPTable t = parser.parseTable(reader, fkeys); + assertEquals("TableA", t.getADQLName()); + assertEquals("Table\ndescription.", t.getDescription()); + assertEquals("truc.chose", t.getUtype()); + assertEquals("Table title", t.getTitle()); + assertEquals(1, t.getNbColumns()); + assertNotNull(t.getColumn("col1")); + assertEquals(0, t.getNbForeignKeys()); + assertEquals(1, fkeys.size()); + assertEquals("TB", fkeys.get(0).targetTable); + assertEquals(t, fkeys.get(0).fromTable); + close(reader); + fkeys.clear(); + // variant with some comments: + reader = buildReader("TableATable \ndescription.truc.choseTable titlecol1TBcol1col2
    "); + reader.next(); // START ELEMENT("table") + t = parser.parseTable(reader, fkeys); + assertEquals("TableA", t.getADQLName()); + assertEquals("Table\ndescription.", t.getDescription()); + assertEquals("truc.chose", t.getUtype()); + assertEquals("Table title", t.getTitle()); + assertEquals(1, t.getNbColumns()); + assertNotNull(t.getColumn("col1")); + assertEquals(0, t.getNbForeignKeys()); + assertEquals(1, fkeys.size()); + assertEquals("TB", fkeys.get(0).targetTable); + assertEquals(t, fkeys.get(0).fromTable); + close(reader); + fkeys.clear(); + // variant with texts at unapropriate places: + reader = buildReader("Here we are TableATable \ndescription.truc.choseTable titlecol1TBcol1col2
    Nothing more!"); + reader.next(); // START ELEMENT("table") + t = parser.parseTable(reader, fkeys); + assertEquals("TableA", t.getADQLName()); + assertEquals("Table\ndescription.", t.getDescription()); + assertEquals("truc.chose", t.getUtype()); + assertEquals("Table title", t.getTitle()); + assertEquals(1, t.getNbColumns()); + assertNotNull(t.getColumn("col1")); + assertEquals(0, t.getNbForeignKeys()); + assertEquals(1, fkeys.size()); + assertEquals("TB", fkeys.get(0).targetTable); + assertEquals(t, fkeys.get(0).fromTable); + close(reader); + fkeys.clear(); + + // Test with a missing "name" node: + reader = buildReader("
    "); + reader.next(); // START ELEMENT("table") + try{ + parser.parseTable(reader, fkeys); + fail("This test should have failed: the \"name\" node is missing!"); + }catch(Exception e){ + if (e instanceof TAPException) + assertEquals("[l.1,c.16] Missing table \"name\"!", e.getMessage()); + }finally{ + close(reader); + fkeys.clear(); + } + // variant with duplicated "name": + reader = buildReader("Table1TableA
    "); + reader.next(); // START ELEMENT("table") + try{ + parser.parseTable(reader, fkeys); + fail("This test should have failed: the \"name\" node is duplicated!"); + }catch(Exception e){ + if (e instanceof TAPException) + assertEquals("[l.1,c.33] Only one \"name\" element can exist in a /tableset/schema/table!", e.getMessage()); + }finally{ + close(reader); + fkeys.clear(); + } + + // Test with a additional node: + reader = buildReader("TableASuperTableAblabla
    "); + reader.next(); // START ELEMENT("table") + t = parser.parseTable(reader, fkeys); + assertEquals("TableA", t.getADQLName()); + assertNull(t.getDescription()); + assertNull(t.getUtype()); + assertNull(t.getTitle()); + assertEquals(0, t.getNbColumns()); + assertEquals(0, t.getNbForeignKeys()); + assertEquals(0, fkeys.size()); + close(reader); + fkeys.clear(); + + }catch(Exception e){ + e.printStackTrace(); + if (e instanceof XMLStreamException) + fail("Unexpected error while reading the XML content: " + e.getMessage()); + else + fail("Unexpected error: " + e.getMessage()); + }finally{ + close(reader); + fkeys.clear(); + } + } + + @Test + public void testParseSchema(){ + XMLStreamReader reader = null; + ArrayList fkeys = new ArrayList(0); + try{ + + // Test while search outside from the schema node: + reader = buildReader("PublicSchema"); + reader.next(); // START ELEMENT("tableset") + try{ + parser.parseSchema(reader, fkeys); + fail("This test should have failed: the reader has not just read the \"schema\" START ELEMENT tag."); + }catch(Exception e){ + if (e instanceof IllegalStateException) + assertEquals("[l.1,c.11] Illegal usage of TableSetParser.parseSchema(XMLStreamParser)! This function can be called only when the reader has just read the START ELEMENT tag \"schema\".", e.getMessage()); + else + throw e; + }finally{ + close(reader); + fkeys.clear(); + } + + // Test with a complete and correct XML table node: + reader = buildReader("PublicSchemaSchema \ndescription.truc.choseSchema titleTableA
    "); + reader.next(); // START ELEMENT("schema") + TAPSchema s = parser.parseSchema(reader, fkeys); + assertEquals("PublicSchema", s.getADQLName()); + assertEquals("Schema\ndescription.", s.getDescription()); + assertEquals("truc.chose", s.getUtype()); + assertEquals("Schema title", s.getTitle()); + assertEquals(1, s.getNbTables()); + assertNotNull(s.getTable("TableA")); + close(reader); + fkeys.clear(); + // variant with some comments: + reader = buildReader("PublicSchema Schema \ndescription.truc.choseSchema titleTableA
    "); + reader.next(); // START ELEMENT("schema") + s = parser.parseSchema(reader, fkeys); + assertEquals("PublicSchema", s.getADQLName()); + assertEquals("Schema\ndescription.", s.getDescription()); + assertEquals("truc.chose", s.getUtype()); + assertEquals("Schema title", s.getTitle()); + assertEquals(1, s.getNbTables()); + assertNotNull(s.getTable("TableA")); + close(reader); + fkeys.clear(); + // variant with texts at unapropriate places: + reader = buildReader("Here we are PublicSchema Schema \ndescription.truc.choseSchema titleTableA
    Nothing more!"); + reader.next(); // START ELEMENT("schema") + s = parser.parseSchema(reader, fkeys); + assertEquals("PublicSchema", s.getADQLName()); + assertEquals("Schema\ndescription.", s.getDescription()); + assertEquals("truc.chose", s.getUtype()); + assertEquals("Schema title", s.getTitle()); + assertEquals(1, s.getNbTables()); + assertNotNull(s.getTable("TableA")); + close(reader); + fkeys.clear(); + + // Test with a missing "name" node: + reader = buildReader(""); + reader.next(); // START ELEMENT("schema") + try{ + parser.parseSchema(reader, fkeys); + fail("This test should have failed: the \"name\" node is missing!"); + }catch(Exception e){ + if (e instanceof TAPException) + assertEquals("[l.1,c.18] Missing schema \"name\"!", e.getMessage()); + }finally{ + close(reader); + fkeys.clear(); + } + // variant with duplicated "name": + reader = buildReader("PublicSchemaPrivateSchema"); + reader.next(); // START ELEMENT("schema") + try{ + parser.parseSchema(reader, fkeys); + fail("This test should have failed: the \"name\" node is duplicated!"); + }catch(Exception e){ + if (e instanceof TAPException) + assertEquals("[l.1,c.40] Only one \"name\" element can exist in a /tableset/schema!", e.getMessage()); + }finally{ + close(reader); + fkeys.clear(); + } + + // Test with a additional node: + reader = buildReader("PublicSchemapublicblabla"); + reader.next(); // START ELEMENT("schema") + s = parser.parseSchema(reader, fkeys); + assertEquals("PublicSchema", s.getADQLName()); + assertNull(s.getDescription()); + assertNull(s.getUtype()); + assertNull(s.getTitle()); + assertEquals(0, s.getNbTables()); + close(reader); + fkeys.clear(); + + }catch(Exception e){ + e.printStackTrace(); + if (e instanceof XMLStreamException) + fail("Unexpected error while reading the XML content: " + e.getMessage()); + else + fail("Unexpected error: " + e.getMessage()); + }finally{ + close(reader); + fkeys.clear(); + } + } + +} diff --git a/test/tap/parameters/ServiceConnectionOfTest.java b/test/tap/parameters/ServiceConnectionOfTest.java new file mode 100644 index 0000000..029c056 --- /dev/null +++ b/test/tap/parameters/ServiceConnectionOfTest.java @@ -0,0 +1,177 @@ +package tap.parameters; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +import tap.ServiceConnection; +import tap.TAPFactory; +import tap.TAPJob; +import tap.formatter.FITSFormat; +import tap.formatter.OutputFormat; +import tap.formatter.SVFormat; +import tap.formatter.VOTableFormat; +import tap.log.TAPLog; +import tap.metadata.TAPMetadata; +import uws.service.UserIdentifier; +import uws.service.file.UWSFileManager; +import adql.db.FunctionDef; + +public class ServiceConnectionOfTest implements ServiceConnection { + + private boolean available = true; + private String availability = "TAP Service available!"; + private int[] retentionPeriod = new int[]{-1,-1}; + private int[] executionDuration = new int[]{(int)TAPJob.UNLIMITED_DURATION,(int)TAPJob.UNLIMITED_DURATION}; + private int[] outputLimit = new int[]{TAPJob.UNLIMITED_MAX_REC,TAPJob.UNLIMITED_MAX_REC}; + private List outputFormats = Arrays.asList(new OutputFormat[]{new VOTableFormat(this),new SVFormat(this, SVFormat.COMMA_SEPARATOR),new FITSFormat(this)}); + + @Override + public String getProviderName(){ + return null; + } + + @Override + public String getProviderDescription(){ + return null; + } + + @Override + public boolean isAvailable(){ + return available; + } + + @Override + public void setAvailable(boolean isAvailable, String message){ + available = isAvailable; + if (message != null) + availability = message; + else + availability = (isAvailable ? "TAP Service available!" : "TAP Service momentarily UNavailable!"); + } + + @Override + public String getAvailability(){ + return availability; + } + + @Override + public int[] getRetentionPeriod(){ + return retentionPeriod; + } + + public void setRetentionPeriod(final int defaultVal, final int maxVal){ + retentionPeriod[0] = defaultVal; + retentionPeriod[1] = maxVal; + } + + @Override + public int[] getExecutionDuration(){ + return executionDuration; + } + + public void setExecutionDuration(final int defaultVal, final int maxVal){ + executionDuration[0] = defaultVal; + executionDuration[1] = maxVal; + } + + @Override + public int[] getOutputLimit(){ + return outputLimit; + } + + public void setOutputLimit(final int defaultVal, final int maxVal){ + outputLimit[0] = defaultVal; + outputLimit[1] = maxVal; + } + + @Override + public LimitUnit[] getOutputLimitType(){ + return new LimitUnit[]{LimitUnit.rows,LimitUnit.rows}; + } + + @Override + public UserIdentifier getUserIdentifier(){ + return null; + } + + @Override + public boolean uploadEnabled(){ + return false; + } + + @Override + public int[] getUploadLimit(){ + return null; + } + + @Override + public LimitUnit[] getUploadLimitType(){ + return null; + } + + @Override + public int getMaxUploadSize(){ + return 0; + } + + @Override + public TAPMetadata getTAPMetadata(){ + return null; + } + + @Override + public Collection getCoordinateSystems(){ + return null; + } + + @Override + public Collection getGeometries(){ + return null; + } + + @Override + public Collection getUDFs(){ + return null; + } + + @Override + public int getNbMaxAsyncJobs(){ + return 0; + } + + @Override + public TAPLog getLogger(){ + return null; + } + + @Override + public TAPFactory getFactory(){ + return null; + } + + @Override + public UWSFileManager getFileManager(){ + return null; + } + + @Override + public Iterator getOutputFormats(){ + return outputFormats.iterator(); + } + + @Override + public OutputFormat getOutputFormat(String mimeOrAlias){ + for(OutputFormat f : outputFormats) + if (f.getMimeType().equalsIgnoreCase(mimeOrAlias) || f.getShortMimeType().equalsIgnoreCase(mimeOrAlias)) + return f; + return null; + } + + @Override + public int[] getFetchSize(){ + return null; + } + +} \ No newline at end of file diff --git a/test/tap/parameters/TestFormatController.java b/test/tap/parameters/TestFormatController.java new file mode 100644 index 0000000..50d0bb4 --- /dev/null +++ b/test/tap/parameters/TestFormatController.java @@ -0,0 +1,89 @@ +package tap.parameters; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import tap.TAPJob; +import uws.UWSException; + +public class TestFormatController { + + @BeforeClass + public static void setUpBeforeClass() throws Exception{} + + @AfterClass + public static void tearDownAfterClass() throws Exception{} + + @Before + public void setUp() throws Exception{} + + @After + public void tearDown() throws Exception{} + + @Test + public void testCheck(){ + ServiceConnectionOfTest service = new ServiceConnectionOfTest(); + FormatController controller = new FormatController(service); + + try{ + assertEquals(controller.getDefault(), controller.check(null)); + assertEquals(controller.getDefault(), controller.check("")); + assertEquals(controller.getDefault(), controller.check(" ")); + assertEquals(controller.getDefault(), controller.check(" ")); + assertEquals(controller.getDefault(), controller.check(" ")); + assertEquals("votable", controller.check("votable")); + assertEquals("application/x-votable+xml", controller.check("application/x-votable+xml")); + assertEquals("csv", controller.check("csv")); + assertEquals("fits", controller.check("fits")); + }catch(Exception ex){ + ex.printStackTrace(); + fail(); + } + + try{ + controller.check("toto"); + }catch(Exception ex){ + assertTrue(ex instanceof UWSException); + assertTrue(ex.getMessage().startsWith("Unknown value for the parameter \"format\": \"toto\". It should be ")); + } + + try{ + controller.check("application/xml"); + }catch(Exception ex){ + assertTrue(ex instanceof UWSException); + assertTrue(ex.getMessage().startsWith("Unknown value for the parameter \"format\": \"application/xml\". It should be ")); + } + } + + @Test + public void testGetDefault(){ + ServiceConnectionOfTest service = new ServiceConnectionOfTest(); + FormatController controller = new FormatController(service); + + assertEquals(TAPJob.FORMAT_VOTABLE, controller.getDefault()); + } + + @Test + public void testAllowModification(){ + ServiceConnectionOfTest service = new ServiceConnectionOfTest(); + FormatController controller = new FormatController(service); + + // By default, user modification of the destruction time is allowed: + assertTrue(controller.allowModification()); + + controller.allowModification(true); + assertTrue(controller.allowModification()); + + controller.allowModification(false); + assertFalse(controller.allowModification()); + } + +} diff --git a/test/tap/parameters/TestMaxRecController.java b/test/tap/parameters/TestMaxRecController.java new file mode 100644 index 0000000..613a2f9 --- /dev/null +++ b/test/tap/parameters/TestMaxRecController.java @@ -0,0 +1,142 @@ +package tap.parameters; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import tap.TAPJob; + +public class TestMaxRecController { + + @BeforeClass + public static void setUpBeforeClass() throws Exception{} + + @AfterClass + public static void tearDownAfterClass() throws Exception{} + + @Before + public void setUp() throws Exception{} + + @After + public void tearDown() throws Exception{} + + @Test + public void testCheck(){ + ServiceConnectionOfTest service = new ServiceConnectionOfTest(); + MaxRecController controller = new MaxRecController(service); + + try{ + // A NULL limit will always return an unlimited duration: + assertEquals(TAPJob.UNLIMITED_MAX_REC, controller.check(null)); + assertEquals(TAPJob.UNLIMITED_MAX_REC, controller.check(-1)); + assertEquals(TAPJob.UNLIMITED_MAX_REC, controller.check(-123)); + + // A 0 value, means that only the metadata of the result must be returned (without executing the query); + // this value should stay like that: + assertEquals(0, controller.check(0)); + + // By default, the controller has no limit on the output limit, so let's try with a limit of 1000000 rows: + assertEquals(1000000, controller.check(1000000)); + + // With just a default output limit (of 100 rows): + service.setOutputLimit(100, -1); + assertEquals(100, controller.check(null)); + assertEquals(0, controller.check(0)); + assertEquals(TAPJob.UNLIMITED_MAX_REC, controller.check(-1)); + assertEquals(TAPJob.UNLIMITED_MAX_REC, controller.check(TAPJob.UNLIMITED_MAX_REC)); + + // With just a maximum output limit (of 10000 rows): + service.setOutputLimit(-1, 10000); + assertEquals(10000, controller.check(null)); + assertEquals(0, controller.check(0)); + assertEquals(60, controller.check(60)); + assertEquals(10000, controller.check(-1)); + assertEquals(10000, controller.check(TAPJob.UNLIMITED_MAX_REC)); + assertEquals(10000, controller.check(10001)); + + // With a default (100 rows) AND a maximum (10000 rows) output limit: + service.setOutputLimit(100, 10000); + assertEquals(100, controller.check(null)); + assertEquals(0, controller.check(0)); + assertEquals(10, controller.check(10)); + assertEquals(600, controller.check(600)); + assertEquals(10000, controller.check(10000)); + assertEquals(10000, controller.check(-1)); + assertEquals(10000, controller.check(TAPJob.UNLIMITED_MAX_REC)); + assertEquals(10000, controller.check(10001)); + + }catch(Exception t){ + t.printStackTrace(); + fail(); + } + } + + @Test + public void testGetDefault(){ + ServiceConnectionOfTest service = new ServiceConnectionOfTest(); + MaxRecController controller = new MaxRecController(service); + + // By default, when nothing is set, the default output limit is UNLIMITED: + assertEquals(TAPJob.UNLIMITED_MAX_REC, controller.getDefault()); + + // With no duration, the default output limit should remain UNLIMITED: + service.setOutputLimit(TAPJob.UNLIMITED_MAX_REC, -1); + assertEquals(TAPJob.UNLIMITED_MAX_REC, controller.getDefault()); + + // With a negative limit, the output limit should also be UNLIMITED: + service.setOutputLimit(-1, -1); + assertEquals(TAPJob.UNLIMITED_MAX_REC, controller.getDefault()); + + // With an output limit of 100 rows: + service.setOutputLimit(100, -1); + assertEquals(100, controller.getDefault()); + + // The default value must always be less than the maximum value: + service.setOutputLimit(600, 300); + assertEquals(300, controller.getDefault()); + } + + @Test + public void testGetMaxExecutionDuration(){ + ServiceConnectionOfTest service = new ServiceConnectionOfTest(); + MaxRecController controller = new MaxRecController(service); + + // By default, when nothing is set, the maximum output limit is UNLIMITED: + assertEquals(TAPJob.UNLIMITED_MAX_REC, controller.getMaxOutputLimit()); + + // With no duration, the maximum output limit should remain UNLIMITED: + service.setOutputLimit(-1, TAPJob.UNLIMITED_MAX_REC); + assertEquals(TAPJob.UNLIMITED_MAX_REC, controller.getMaxOutputLimit()); + + // With a negative limit, the output limit should also be UNLIMITED: + service.setOutputLimit(-1, -1); + assertEquals(TAPJob.UNLIMITED_MAX_REC, controller.getMaxOutputLimit()); + + // With an output limit of 10000 rows: + service.setOutputLimit(-1, 10000); + assertEquals(10000, controller.getMaxOutputLimit()); + } + + @Test + public void testAllowModification(){ + ServiceConnectionOfTest service = new ServiceConnectionOfTest(); + MaxRecController controller = new MaxRecController(service); + + // By default, user modification of the destruction time is allowed: + assertTrue(controller.allowModification()); + + controller.allowModification(true); + assertTrue(controller.allowModification()); + + controller.allowModification(false); + assertFalse(controller.allowModification()); + } + +} diff --git a/test/tap/parameters/TestTAPDestructionTimeController.java b/test/tap/parameters/TestTAPDestructionTimeController.java new file mode 100644 index 0000000..73453af --- /dev/null +++ b/test/tap/parameters/TestTAPDestructionTimeController.java @@ -0,0 +1,187 @@ +package tap.parameters; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.Calendar; +import java.util.Date; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import uws.ISO8601Format; + +public class TestTAPDestructionTimeController { + + @BeforeClass + public static void setUpBeforeClass() throws Exception{} + + @AfterClass + public static void tearDownAfterClass() throws Exception{} + + @Before + public void setUp() throws Exception{} + + @After + public void tearDown() throws Exception{} + + @Test + public void testCheck(){ + ServiceConnectionOfTest service = new ServiceConnectionOfTest(); + TAPDestructionTimeController controller = new TAPDestructionTimeController(service); + Calendar calendar = Calendar.getInstance(); + + try{ + // A NULL destruction time will always return NULL: + assertNull(controller.check(null)); + + // By default, the controller has no limit on the destruction time, so let's try with a destruction in 100 years: + calendar.add(Calendar.YEAR, 100); + checkDate(calendar.getTime(), controller.check(calendar.getTime())); + checkDate(calendar.getTime(), controller.check(ISO8601Format.format(calendar.getTimeInMillis()))); + + // With just a default destruction time (of 10 minutes): + service.setRetentionPeriod(600, -1); + Calendar defaultTime = Calendar.getInstance(); + defaultTime.add(Calendar.MINUTE, 10); + checkDate(defaultTime.getTime(), controller.check(null)); + checkDate(calendar.getTime(), controller.check(calendar.getTime())); + + // With just a maximum destruction time (of 1 hour): + service.setRetentionPeriod(0, 3600); + Calendar maxTime = Calendar.getInstance(); + maxTime.add(Calendar.HOUR, 1); + checkDate(maxTime.getTime(), controller.check(null)); + checkDate(defaultTime.getTime(), controller.check(defaultTime.getTime())); + checkDate(maxTime.getTime(), controller.check(calendar.getTime())); + + // With a default (10 minutes) AND a maximum (1 hour) destruction time: + service.setRetentionPeriod(600, 3600); + checkDate(defaultTime.getTime(), controller.check(null)); + checkDate(maxTime.getTime(), controller.check(calendar.getTime())); + calendar = Calendar.getInstance(); + calendar.add(Calendar.MINUTE, 30); + checkDate(calendar.getTime(), controller.check(calendar.getTime())); + + }catch(Exception t){ + t.printStackTrace(); + fail(); + } + } + + @Test + public void testGetDefault(){ + ServiceConnectionOfTest service = new ServiceConnectionOfTest(); + TAPDestructionTimeController controller = new TAPDestructionTimeController(service); + + // By default, when nothing is set, the default destruction time is NULL (the job will never be destroyed): + assertNull(controller.getDefault()); + + // With no interval, the default destruction time should remain NULL (the job will never be destroyed): + service.setRetentionPeriod(0, -1); + assertNull(controller.getDefault()); + + // With a negative interval, the destruction time should also be NULL: + service.setRetentionPeriod(-1, -1); + assertNull(controller.getDefault()); + + // With a destruction interval of 100 minutes: + Calendar calendar = Calendar.getInstance(); + service.setRetentionPeriod(6000, -1); + calendar.add(Calendar.MINUTE, 100); + checkDate(calendar.getTime(), controller.getDefault()); + + // With a destruction interval of 100 seconds: + service.setRetentionPeriod(100, -1); + calendar = Calendar.getInstance(); + calendar.add(Calendar.SECOND, 100); + checkDate(calendar.getTime(), controller.getDefault()); + + // With a destruction interval of 1 week: + service.setRetentionPeriod(7 * 24 * 3600, -1); + calendar = Calendar.getInstance(); + calendar.add(Calendar.DAY_OF_MONTH, 7); + checkDate(calendar.getTime(), controller.getDefault()); + } + + @Test + public void testGetMaxDestructionTime(){ + ServiceConnectionOfTest service = new ServiceConnectionOfTest(); + TAPDestructionTimeController controller = new TAPDestructionTimeController(service); + + // By default, when nothing is set, the maximum destruction time is NULL (the job will never be destroyed): + assertNull(controller.getMaxDestructionTime()); + + // With no interval, the maximum destruction time should remain NULL (the job will never be destroyed): + service.setRetentionPeriod(-1, 0); + assertNull(controller.getMaxDestructionTime()); + + // With a negative interval, the destruction time should also be NULL: + service.setRetentionPeriod(-1, -1); + assertNull(controller.getMaxDestructionTime()); + + // With a destruction interval of 100 minutes: + Calendar calendar = Calendar.getInstance(); + service.setRetentionPeriod(-1, 6000); + calendar.add(Calendar.MINUTE, 100); + checkDate(calendar.getTime(), controller.getMaxDestructionTime()); + + // With a destruction interval of 100 seconds: + service.setRetentionPeriod(-1, 100); + calendar = Calendar.getInstance(); + calendar.add(Calendar.SECOND, 100); + checkDate(calendar.getTime(), controller.getMaxDestructionTime()); + + // With a destruction interval of 1 week: + service.setRetentionPeriod(-1, 7 * 24 * 3600); + calendar = Calendar.getInstance(); + calendar.add(Calendar.DAY_OF_MONTH, 7); + checkDate(calendar.getTime(), controller.getMaxDestructionTime()); + } + + @Test + public void testAllowModification(){ + ServiceConnectionOfTest service = new ServiceConnectionOfTest(); + TAPDestructionTimeController controller = new TAPDestructionTimeController(service); + + // By default, user modification of the destruction time is allowed: + assertTrue(controller.allowModification()); + + controller.allowModification(true); + assertTrue(controller.allowModification()); + + controller.allowModification(false); + assertFalse(controller.allowModification()); + } + + private void checkDate(final Date expected, final Object val){ + assertTrue(val instanceof Date); + + if (expected != null && val != null){ + Calendar cexpected = Calendar.getInstance(), cval = Calendar.getInstance(); + cexpected.setTime(expected); + cval.setTime((Date)val); + + try{ + assertEquals(cexpected.get(Calendar.DAY_OF_MONTH), cval.get(Calendar.DAY_OF_MONTH)); + assertEquals(cexpected.get(Calendar.MONTH), cval.get(Calendar.MONTH)); + assertEquals(cexpected.get(Calendar.YEAR), cval.get(Calendar.YEAR)); + assertEquals(cexpected.get(Calendar.HOUR), cval.get(Calendar.HOUR)); + assertEquals(cexpected.get(Calendar.MINUTE), cval.get(Calendar.MINUTE)); + assertEquals(cexpected.get(Calendar.SECOND), cval.get(Calendar.SECOND)); + }catch(AssertionError e){ + fail("Expected <" + expected + "> but was <" + val + ">"); + } + }else if (expected == null && val == null) + return; + else + fail("Expected <" + expected + "> but was <" + val + ">"); + } + +} diff --git a/test/tap/parameters/TestTAPExecutionDurationController.java b/test/tap/parameters/TestTAPExecutionDurationController.java new file mode 100644 index 0000000..446c58e --- /dev/null +++ b/test/tap/parameters/TestTAPExecutionDurationController.java @@ -0,0 +1,136 @@ +package tap.parameters; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import tap.TAPJob; + +public class TestTAPExecutionDurationController { + + @BeforeClass + public static void setUpBeforeClass() throws Exception{} + + @AfterClass + public static void tearDownAfterClass() throws Exception{} + + @Before + public void setUp() throws Exception{} + + @After + public void tearDown() throws Exception{} + + @Test + public void testCheck(){ + ServiceConnectionOfTest service = new ServiceConnectionOfTest(); + TAPExecutionDurationController controller = new TAPExecutionDurationController(service); + + try{ + // A NULL duration will always return an unlimited duration: + assertEquals(TAPJob.UNLIMITED_DURATION, controller.check(null)); + assertEquals(TAPJob.UNLIMITED_DURATION, controller.check(0)); + assertEquals(TAPJob.UNLIMITED_DURATION, controller.check(-1)); + assertEquals(TAPJob.UNLIMITED_DURATION, controller.check(-123)); + + // By default, the controller has no limit on the execution duration, so let's try with a duration of 1e6 seconds: + assertEquals(1000000L, controller.check(1000000)); + + // With just a default execution duration (of 10 minutes): + service.setExecutionDuration(600, -1); + assertEquals(600L, controller.check(null)); + assertEquals(TAPJob.UNLIMITED_DURATION, controller.check(-1)); + assertEquals(TAPJob.UNLIMITED_DURATION, controller.check(TAPJob.UNLIMITED_DURATION)); + + // With just a maximum execution duration (of 1 hour): + service.setExecutionDuration(-1, 3600); + assertEquals(3600L, controller.check(null)); + assertEquals(60L, controller.check(60)); + assertEquals(3600L, controller.check(-1)); + assertEquals(3600L, controller.check(TAPJob.UNLIMITED_DURATION)); + assertEquals(3600L, controller.check(3601)); + + // With a default (10 minutes) AND a maximum (1 hour) execution duration: + service.setExecutionDuration(600, 3600); + assertEquals(600L, controller.check(null)); + assertEquals(10L, controller.check(10)); + assertEquals(600L, controller.check(600)); + assertEquals(3600L, controller.check(3600)); + assertEquals(3600L, controller.check(-1)); + assertEquals(3600L, controller.check(TAPJob.UNLIMITED_DURATION)); + assertEquals(3600L, controller.check(3601)); + + }catch(Exception t){ + t.printStackTrace(); + fail(); + } + } + + @Test + public void testGetDefault(){ + ServiceConnectionOfTest service = new ServiceConnectionOfTest(); + TAPExecutionDurationController controller = new TAPExecutionDurationController(service); + + // By default, when nothing is set, the default execution duration is UNLIMITED: + assertEquals(TAPJob.UNLIMITED_DURATION, controller.getDefault()); + + // With no duration, the default execution duration should remain UNLIMITED: + service.setExecutionDuration((int)TAPJob.UNLIMITED_DURATION, -1); + assertEquals(TAPJob.UNLIMITED_DURATION, controller.getDefault()); + + // With a negative duration, the execution duration should also be UNLIMITED: + service.setExecutionDuration(-1, -1); + assertEquals(TAPJob.UNLIMITED_DURATION, controller.getDefault()); + + // With an execution duration of 10 minutes: + service.setExecutionDuration(600, -1); + assertEquals(600L, controller.getDefault()); + + // The default value must always be less than the maximum value: + service.setExecutionDuration(600, 300); + assertEquals(300L, controller.getDefault()); + } + + @Test + public void testGetMaxExecutionDuration(){ + ServiceConnectionOfTest service = new ServiceConnectionOfTest(); + TAPExecutionDurationController controller = new TAPExecutionDurationController(service); + + // By default, when nothing is set, the maximum execution duration is UNLIMITED: + assertEquals(TAPJob.UNLIMITED_DURATION, controller.getMaxDuration()); + + // With no duration, the maximum execution duration should remain UNLIMITED: + service.setExecutionDuration(-1, (int)TAPJob.UNLIMITED_DURATION); + assertEquals(TAPJob.UNLIMITED_DURATION, controller.getMaxDuration()); + + // With a negative duration, the execution duration should also be UNLIMITED: + service.setExecutionDuration(-1, -1); + assertEquals(TAPJob.UNLIMITED_DURATION, controller.getMaxDuration()); + + // With an execution duration of 10 minutes: + service.setExecutionDuration(-1, 600); + assertEquals(600L, controller.getMaxDuration()); + } + + @Test + public void testAllowModification(){ + ServiceConnectionOfTest service = new ServiceConnectionOfTest(); + TAPExecutionDurationController controller = new TAPExecutionDurationController(service); + + // By default, user modification of the execution duration is allowed: + assertTrue(controller.allowModification()); + + controller.allowModification(true); + assertTrue(controller.allowModification()); + + controller.allowModification(false); + assertFalse(controller.allowModification()); + } + +} diff --git a/test/testtools/CommandExecute.java b/test/testtools/CommandExecute.java new file mode 100644 index 0000000..2e78978 --- /dev/null +++ b/test/testtools/CommandExecute.java @@ -0,0 +1,51 @@ +package testtools; + +import java.io.BufferedReader; +import java.io.InputStreamReader; + +/** + * Let's execute any shell command (even with pipes and redirections). + * + * @author Grégory Mantelet (ARI) + * @version 2.0 (09/2014) + */ +public final class CommandExecute { + + /** + * SINGLETON CLASS. + * No instance of this class can be created. + */ + private CommandExecute(){} + + /** + * Execute the given command (which may include pipe(s) and/or redirection(s)). + * + * @param command Command to execute in the shell. + * + * @return The string returned by the execution of the command. + */ + public final static String execute(final String command){ + + String[] shellCmd = new String[]{"/bin/sh","-c",command}; + + StringBuffer output = new StringBuffer(); + + Process p; + try{ + p = Runtime.getRuntime().exec(shellCmd); + p.waitFor(); + BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream())); + + String line = ""; + while((line = reader.readLine()) != null){ + output.append(line + "\n"); + } + + }catch(Exception e){ + e.printStackTrace(); + } + + return output.toString(); + + } +} diff --git a/test/testtools/DBTools.java b/test/testtools/DBTools.java new file mode 100644 index 0000000..ba542f9 --- /dev/null +++ b/test/testtools/DBTools.java @@ -0,0 +1,136 @@ +package testtools; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.HashMap; + +public final class DBTools { + + public static int count = 0; + + public final static void main(final String[] args) throws Throwable{ + for(int i = 0; i < 3; i++){ + Thread t = new Thread(new Runnable(){ + @Override + public void run(){ + count++; + try{ + Connection conn = DBTools.createConnection("postgresql", "127.0.0.1", null, "gmantele", "gmantele", "pwd"); + System.out.println("Start - " + count + ": "); + String query = "SELECT * FROM gums.smc WHERE magg BETWEEN " + (15 + count) + " AND " + (20 + count) + ";"; + System.out.println(query); + ResultSet rs = DBTools.select(conn, query); + try{ + rs.last(); + System.out.println("Nb rows: " + rs.getRow()); + }catch(SQLException e){ + e.printStackTrace(); + } + if (DBTools.closeConnection(conn)) + System.out.println("[DEBUG] Connection closed!"); + }catch(DBToolsException e){ + e.printStackTrace(); + } + System.out.println("End - " + count); + count--; + } + }); + t.start(); + } + } + + public static class DBToolsException extends Exception { + + private static final long serialVersionUID = 1L; + + public DBToolsException(){ + super(); + } + + public DBToolsException(String message, Throwable cause){ + super(message, cause); + } + + public DBToolsException(String message){ + super(message); + } + + public DBToolsException(Throwable cause){ + super(cause); + } + + } + + public final static HashMap VALUE_JDBC_DRIVERS = new HashMap(4); + static{ + VALUE_JDBC_DRIVERS.put("oracle", "oracle.jdbc.OracleDriver"); + VALUE_JDBC_DRIVERS.put("postgresql", "org.postgresql.Driver"); + VALUE_JDBC_DRIVERS.put("mysql", "com.mysql.jdbc.Driver"); + VALUE_JDBC_DRIVERS.put("sqlite", "org.sqlite.JDBC"); + } + + private DBTools(){} + + public final static Connection createConnection(String dbms, final String server, final String port, final String dbName, final String user, final String passwd) throws DBToolsException{ + // 1. Resolve the DBMS and get its JDBC driver: + if (dbms == null) + throw new DBToolsException("Missing DBMS (expected: oracle, postgresql, mysql or sqlite)!"); + dbms = dbms.toLowerCase(); + String jdbcDriver = VALUE_JDBC_DRIVERS.get(dbms); + if (jdbcDriver == null) + throw new DBToolsException("Unknown DBMS (\"" + dbms + "\")!"); + + // 2. Load the JDBC driver: + try{ + Class.forName(jdbcDriver); + }catch(ClassNotFoundException e){ + throw new DBToolsException("Impossible to load the JDBC driver: " + e.getMessage(), e); + } + + // 3. Establish the connection: + Connection connection = null; + try{ + connection = DriverManager.getConnection("jdbc:" + dbms + ":" + ((server != null && server.trim().length() > 0) ? "//" + server + ((port != null && port.trim().length() > 0) ? (":" + port) : "") + "/" : "") + dbName, user, passwd); + }catch(SQLException e){ + throw new DBToolsException("Connection failed: " + e.getMessage(), e); + } + + if (connection == null) + throw new DBToolsException("Failed to make connection!"); + + return connection; + } + + public final static boolean closeConnection(final Connection conn) throws DBToolsException{ + try{ + if (conn != null && !conn.isClosed()){ + conn.close(); + try{ + Thread.sleep(200); + }catch(InterruptedException e){ + System.err.println("WARNING: can't wait/sleep before testing the connection close status! [" + e.getMessage() + "]"); + } + return conn.isClosed(); + }else + return true; + }catch(SQLException e){ + throw new DBToolsException("Closing connection failed: " + e.getMessage(), e); + } + } + + public final static ResultSet select(final Connection conn, final String selectQuery) throws DBToolsException{ + if (conn == null || selectQuery == null || selectQuery.trim().length() == 0) + throw new DBToolsException("One parameter is missing!"); + + try{ + Statement stmt = conn.createStatement(); + return stmt.executeQuery(selectQuery); + }catch(SQLException e){ + throw new DBToolsException("Can't execute the given SQL query: " + e.getMessage(), e); + } + } + +} diff --git a/test/testtools/MD5Checksum.java b/test/testtools/MD5Checksum.java new file mode 100644 index 0000000..d4941c5 --- /dev/null +++ b/test/testtools/MD5Checksum.java @@ -0,0 +1,46 @@ +package testtools; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.security.MessageDigest; + +public class MD5Checksum { + + public static byte[] createChecksum(InputStream input) throws Exception{ + byte[] buffer = new byte[1024]; + MessageDigest complete = MessageDigest.getInstance("MD5"); + int numRead; + + do{ + numRead = input.read(buffer); + if (numRead > 0){ + complete.update(buffer, 0, numRead); + } + }while(numRead != -1); + return complete.digest(); + } + + // see this How-to for a faster way to convert + // a byte array to a HEX string + public static String getMD5Checksum(InputStream input) throws Exception{ + byte[] b = createChecksum(input); + String result = ""; + + for(int i = 0; i < b.length; i++){ + result += Integer.toString((b[i] & 0xff) + 0x100, 16).substring(1); + } + return result; + } + + public static String getMD5Checksum(final String content) throws Exception{ + return getMD5Checksum(new ByteArrayInputStream(content.getBytes())); + } + + public static void main(String args[]){ + try{ + System.out.println(getMD5Checksum("Blabla et Super blabla")); + }catch(Exception e){ + e.printStackTrace(); + } + } +} diff --git a/test/uws/TestISO8601Format.java b/test/uws/TestISO8601Format.java new file mode 100644 index 0000000..22c9ee9 --- /dev/null +++ b/test/uws/TestISO8601Format.java @@ -0,0 +1,163 @@ +package uws; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import java.text.ParseException; +import java.util.TimeZone; + +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +public class TestISO8601Format { + + private final long date = 1411737870325L; // Fri Sep 26 15:24:30 CEST 2014 = 2014-09-26T15:24:30.325+02:00 = 1411737870325 ms + private final long dateAlone = 1411689600000L; + + private final long oldDate = -3506029200000L; // Thu Nov 25 00:00:00 CET 1858 = 1858-11-25T00:00:00+01:00 = -3506029200000 ms + + private static boolean displayMS; + private static boolean displayTZ; + private static String targetTZ = null; + + @BeforeClass + public static void setUpBeforeClass() throws Exception{ + displayMS = ISO8601Format.displayMilliseconds; + displayTZ = ISO8601Format.displayTimeZone; + targetTZ = ISO8601Format.targetTimeZone; + } + + @Before + public void setUp() throws Exception{ + ISO8601Format.displayMilliseconds = false; + ISO8601Format.displayTimeZone = true; + ISO8601Format.targetTimeZone = "Europe/Berlin"; + } + + @AfterClass + public static void tearDownAfterClass() throws Exception{ + ISO8601Format.displayMilliseconds = displayMS; + ISO8601Format.displayTimeZone = displayTZ; + ISO8601Format.targetTimeZone = targetTZ; + } + + @Test + public void testFormatDate(){ + // Special case: reference for the millisecond representation of dates (1st January 1970): + assertEquals("1970-01-01T01:00:00+01:00", ISO8601Format.format(0)); + assertEquals("1970-01-01T00:00:00Z", ISO8601Format.formatInUTC(0)); + + // Special case: old date (25th November 1858): + assertEquals("1858-11-25T00:00:00+01:00", ISO8601Format.format(oldDate)); + assertEquals("1858-11-24T23:00:00Z", ISO8601Format.formatInUTC(oldDate)); + + // Tests of: FORMAT(Date) && FORMAT(Date, boolean withTimestamp): + assertEquals("2014-09-26T15:24:30+02:00", ISO8601Format.format(date)); + assertEquals(ISO8601Format.format(date), ISO8601Format.format(date, true)); + assertEquals("2014-09-26T15:24:30", ISO8601Format.format(date, false)); + + // Tests of: FORMAT_IN_UTC(Date) && FORMAT_IN_UTC(Date, boolean withTimestamp): + assertEquals("2014-09-26T13:24:30Z", ISO8601Format.formatInUTC(date)); + assertEquals(ISO8601Format.formatInUTC(date), ISO8601Format.formatInUTC(date, true)); + assertEquals("2014-09-26T13:24:30", ISO8601Format.formatInUTC(date, false)); + + // Test with a different time zone: + assertEquals("2014-09-26T17:24:30+04:00", ISO8601Format.format(date, "Indian/Reunion", true, false)); + + // Test with no specified different time zone (the chosen time zone should be the local one): + assertEquals(ISO8601Format.format(date, TimeZone.getDefault().getID(), true, false), ISO8601Format.format(date, null, true, false)); + + // Test with display of milliseconds: + assertEquals("2014-09-26T15:24:30.325+02:00", ISO8601Format.format(date, null, true, true)); + assertEquals("2014-09-26T15:24:30.325", ISO8601Format.format(date, null, false, true)); + + // Same tests but in the UTC time zone: + assertEquals("2014-09-26T13:24:30.325Z", ISO8601Format.format(date, "UTC", true, true)); + assertEquals("2014-09-26T13:24:30.325", ISO8601Format.format(date, "UTC", false, true)); + } + + @Test + public void testParse(){ + // Special case: NULL + try{ + ISO8601Format.parse(null); + fail("Parse can not theoretically work without a string"); + }catch(Throwable t){ + assertEquals(NullPointerException.class, t.getClass()); + } + + // Special case: "" + try{ + ISO8601Format.parse(""); + fail("Parse can not theoretically work without a non-empty string"); + }catch(Throwable t){ + assertEquals(ParseException.class, t.getClass()); + assertEquals("Invalid date format: \"\"! An ISO8601 date was expected.", t.getMessage()); + } + + // Special case: anything stupid rather than a valid date + try{ + ISO8601Format.parse("stupid thing"); + fail("Parse can not theoretically work without a valid string date"); + }catch(Throwable t){ + assertEquals(ParseException.class, t.getClass()); + assertEquals("Invalid date format: \"stupid thing\"! An ISO8601 date was expected.", t.getMessage()); + } + + try{ + // Special case: reference for the millisecond representation of dates (1st January 1970): + assertEquals(0, ISO8601Format.parse("1970-01-01T01:00:00+01:00")); + assertEquals(0, ISO8601Format.parse("1970-01-01T00:00:00Z")); + + // Special case: old date (25th November 1858): + assertEquals(oldDate, ISO8601Format.parse("1858-11-25T00:00:00+01:00")); + assertEquals(oldDate, ISO8601Format.parse("1858-11-24T23:00:00Z")); + + // Test with a perfectly valid date in ISO8601: + assertEquals(dateAlone, ISO8601Format.parse("2014-09-26")); + assertEquals(date, ISO8601Format.parse("2014-09-26T15:24:30.325+02:00")); + assertEquals(date - 325, ISO8601Format.parse("2014-09-26T15:24:30+02:00")); + + // Test with Z as time zone (UTC): + assertEquals(date, ISO8601Format.parse("2014-09-26T13:24:30.325Z")); + assertEquals(date - 325, ISO8601Format.parse("2014-09-26T13:24:30Z")); + + // If no time zone is specified, the local one should be used: + assertEquals(date, ISO8601Format.parse("2014-09-26T13:24:30.325")); + assertEquals(date - 325, ISO8601Format.parse("2014-09-26T13:24:30")); + + // All the previous tests without the _ between days, month, and years: + assertEquals(0, ISO8601Format.parse("19700101T01:00:00+01:00")); + assertEquals(0, ISO8601Format.parse("19700101T00:00:00Z")); + assertEquals(oldDate, ISO8601Format.parse("18581125T00:00:00+01:00")); + assertEquals(oldDate, ISO8601Format.parse("18581124T23:00:00Z")); + assertEquals(dateAlone, ISO8601Format.parse("20140926")); + assertEquals(date, ISO8601Format.parse("20140926T15:24:30.325+02:00")); + assertEquals(date - 325, ISO8601Format.parse("20140926T15:24:30+02:00")); + assertEquals(date, ISO8601Format.parse("20140926T13:24:30.325Z")); + assertEquals(date - 325, ISO8601Format.parse("20140926T13:24:30Z")); + assertEquals(date, ISO8601Format.parse("20140926T13:24:30.325")); + assertEquals(date - 325, ISO8601Format.parse("20140926T13:24:30")); + + // All the previous tests without the : between hours, minutes and seconds: + assertEquals(0, ISO8601Format.parse("1970-01-01T010000+0100")); + assertEquals(oldDate, ISO8601Format.parse("1858-11-25T000000+0100")); + assertEquals(date, ISO8601Format.parse("2014-09-26T152430.325+0200")); + assertEquals(date - 325, ISO8601Format.parse("2014-09-26T152430+0200")); + + // All the previous tests by replacing the T between date and time by a space: + assertEquals(0, ISO8601Format.parse("1970-01-01 00:00:00Z")); + assertEquals(oldDate, ISO8601Format.parse("1858-11-24 23:00:00Z")); + assertEquals(date, ISO8601Format.parse("2014-09-26 13:24:30.325Z")); + assertEquals(date - 325, ISO8601Format.parse("2014-09-26 13:24:30Z")); + assertEquals(date, ISO8601Format.parse("2014-09-26 13:24:30.325")); + assertEquals(date - 325, ISO8601Format.parse("2014-09-26 13:24:30")); + + }catch(ParseException ex){ + ex.printStackTrace(System.err); + } + } + +} diff --git a/test/uws/job/parameters/TestDestructionTimeController.java b/test/uws/job/parameters/TestDestructionTimeController.java new file mode 100644 index 0000000..a50d5eb --- /dev/null +++ b/test/uws/job/parameters/TestDestructionTimeController.java @@ -0,0 +1,186 @@ +package uws.job.parameters; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.Calendar; +import java.util.Date; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import uws.ISO8601Format; +import uws.job.parameters.DestructionTimeController.DateField; + +public class TestDestructionTimeController { + + @BeforeClass + public static void setUpBeforeClass() throws Exception{} + + @AfterClass + public static void tearDownAfterClass() throws Exception{} + + @Before + public void setUp() throws Exception{} + + @After + public void tearDown() throws Exception{} + + @Test + public void testCheck(){ + DestructionTimeController controller = new DestructionTimeController(); + Calendar calendar = Calendar.getInstance(); + + try{ + // A NULL destruction time will always return NULL: + assertNull(controller.check(null)); + + // By default, the controller has no limit on the destruction time, so let's try with a destruction in 100 years: + calendar.add(Calendar.YEAR, 100); + checkDate(calendar.getTime(), controller.check(calendar.getTime())); + checkDate(calendar.getTime(), controller.check(ISO8601Format.format(calendar.getTimeInMillis()))); + + // With just a default destruction time (of 10 minutes): + controller.setDefaultDestructionInterval(10); + Calendar defaultTime = Calendar.getInstance(); + defaultTime.add(Calendar.MINUTE, 10); + checkDate(defaultTime.getTime(), controller.check(null)); + checkDate(calendar.getTime(), controller.check(calendar.getTime())); + + // With just a maximum destruction time (of 1 hour): + controller.setDefaultDestructionInterval(0); + controller.setMaxDestructionInterval(1, DateField.HOUR); + Calendar maxTime = Calendar.getInstance(); + maxTime.add(Calendar.HOUR, 1); + checkDate(maxTime.getTime(), controller.check(null)); + checkDate(defaultTime.getTime(), controller.check(defaultTime.getTime())); + checkDate(maxTime.getTime(), controller.check(calendar.getTime())); + + // With a default (10 minutes) AND a maximum (1 hour) destruction time: + controller.setDefaultDestructionInterval(10); + controller.setMaxDestructionInterval(1, DateField.HOUR); + checkDate(defaultTime.getTime(), controller.check(null)); + checkDate(maxTime.getTime(), controller.check(calendar.getTime())); + calendar = Calendar.getInstance(); + calendar.add(Calendar.MINUTE, 30); + checkDate(calendar.getTime(), controller.check(calendar.getTime())); + + }catch(Exception t){ + t.printStackTrace(); + fail(); + } + } + + @Test + public void testGetDefault(){ + DestructionTimeController controller = new DestructionTimeController(); + + // By default, when nothing is set, the default destruction time is NULL (the job will never be destroyed): + assertNull(controller.getDefault()); + + // With no interval, the default destruction time should remain NULL (the job will never be destroyed): + controller.setDefaultDestructionInterval(DestructionTimeController.NO_INTERVAL); + assertNull(controller.getDefault()); + + // With a negative interval, the destruction time should also be NULL: + controller.setDefaultDestructionInterval(-1); + assertNull(controller.getDefault()); + + // With a destruction interval of 100 minutes: + Calendar calendar = Calendar.getInstance(); + controller.setDefaultDestructionInterval(100); + calendar.add(Calendar.MINUTE, 100); + checkDate(calendar.getTime(), controller.getDefault()); + + // With a destruction interval of 100 seconds: + controller.setDefaultDestructionInterval(100, DateField.SECOND); + calendar = Calendar.getInstance(); + calendar.add(Calendar.SECOND, 100); + checkDate(calendar.getTime(), controller.getDefault()); + + // With a destruction interval of 1 week: + controller.setDefaultDestructionInterval(7, DateField.DAY); + calendar = Calendar.getInstance(); + calendar.add(Calendar.DAY_OF_MONTH, 7); + checkDate(calendar.getTime(), controller.getDefault()); + } + + @Test + public void testGetMaxDestructionTime(){ + DestructionTimeController controller = new DestructionTimeController(); + + // By default, when nothing is set, the maximum destruction time is NULL (the job will never be destroyed): + assertNull(controller.getMaxDestructionTime()); + + // With no interval, the maximum destruction time should remain NULL (the job will never be destroyed): + controller.setMaxDestructionInterval(DestructionTimeController.NO_INTERVAL); + assertNull(controller.getMaxDestructionTime()); + + // With a negative interval, the destruction time should also be NULL: + controller.setMaxDestructionInterval(-1); + assertNull(controller.getMaxDestructionTime()); + + // With a destruction interval of 100 minutes: + Calendar calendar = Calendar.getInstance(); + controller.setMaxDestructionInterval(100); + calendar.add(Calendar.MINUTE, 100); + checkDate(calendar.getTime(), controller.getMaxDestructionTime()); + + // With a destruction interval of 100 seconds: + controller.setMaxDestructionInterval(100, DateField.SECOND); + calendar = Calendar.getInstance(); + calendar.add(Calendar.SECOND, 100); + checkDate(calendar.getTime(), controller.getMaxDestructionTime()); + + // With a destruction interval of 1 week: + controller.setMaxDestructionInterval(7, DateField.DAY); + calendar = Calendar.getInstance(); + calendar.add(Calendar.DAY_OF_MONTH, 7); + checkDate(calendar.getTime(), controller.getMaxDestructionTime()); + } + + @Test + public void testAllowModification(){ + DestructionTimeController controller = new DestructionTimeController(); + + // By default, user modification of the destruction time is allowed: + assertTrue(controller.allowModification()); + + controller.allowModification(true); + assertTrue(controller.allowModification()); + + controller.allowModification(false); + assertFalse(controller.allowModification()); + } + + private void checkDate(final Date expected, final Object val){ + assertTrue(val instanceof Date); + + if (expected != null && val != null){ + Calendar cexpected = Calendar.getInstance(), cval = Calendar.getInstance(); + cexpected.setTime(expected); + cval.setTime((Date)val); + + try{ + assertEquals(cexpected.get(Calendar.DAY_OF_MONTH), cval.get(Calendar.DAY_OF_MONTH)); + assertEquals(cexpected.get(Calendar.MONTH), cval.get(Calendar.MONTH)); + assertEquals(cexpected.get(Calendar.YEAR), cval.get(Calendar.YEAR)); + assertEquals(cexpected.get(Calendar.HOUR), cval.get(Calendar.HOUR)); + assertEquals(cexpected.get(Calendar.MINUTE), cval.get(Calendar.MINUTE)); + assertEquals(cexpected.get(Calendar.SECOND), cval.get(Calendar.SECOND)); + }catch(AssertionError e){ + fail("Expected <" + expected + "> but was <" + val + ">"); + } + }else if (expected == null && val == null) + return; + else + fail("Expected <" + expected + "> but was <" + val + ">"); + } + +} diff --git a/test/uws/job/parameters/TestExecutionDurationController.java b/test/uws/job/parameters/TestExecutionDurationController.java new file mode 100644 index 0000000..65eef00 --- /dev/null +++ b/test/uws/job/parameters/TestExecutionDurationController.java @@ -0,0 +1,134 @@ +package uws.job.parameters; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import uws.job.UWSJob; + +public class TestExecutionDurationController { + + @BeforeClass + public static void setUpBeforeClass() throws Exception{} + + @AfterClass + public static void tearDownAfterClass() throws Exception{} + + @Before + public void setUp() throws Exception{} + + @After + public void tearDown() throws Exception{} + + @Test + public void testCheck(){ + ExecutionDurationController controller = new ExecutionDurationController(); + + try{ + // A NULL duration will always return an unlimited duration: + assertEquals(UWSJob.UNLIMITED_DURATION, controller.check(null)); + assertEquals(UWSJob.UNLIMITED_DURATION, controller.check(0)); + assertEquals(UWSJob.UNLIMITED_DURATION, controller.check(-1)); + assertEquals(UWSJob.UNLIMITED_DURATION, controller.check(-123)); + + // By default, the controller has no limit on the execution duration, so let's try with a duration of 1e6 seconds: + assertEquals(1000000L, controller.check(1000000)); + + // With just a default execution duration (of 10 minutes): + controller.setDefaultExecutionDuration(600); + assertEquals(600L, controller.check(null)); + assertEquals(UWSJob.UNLIMITED_DURATION, controller.check(-1)); + assertEquals(UWSJob.UNLIMITED_DURATION, controller.check(UWSJob.UNLIMITED_DURATION)); + + // With just a maximum execution duration (of 1 hour): + controller.setDefaultExecutionDuration(-1); + controller.setMaxExecutionDuration(3600); + assertEquals(3600L, controller.check(null)); + assertEquals(60L, controller.check(60)); + assertEquals(3600L, controller.check(-1)); + assertEquals(3600L, controller.check(UWSJob.UNLIMITED_DURATION)); + assertEquals(3600L, controller.check(3601)); + + // With a default (10 minutes) AND a maximum (1 hour) execution duration: + controller.setDefaultExecutionDuration(600); + controller.setMaxExecutionDuration(3600); + assertEquals(600L, controller.check(null)); + assertEquals(10L, controller.check(10)); + assertEquals(600L, controller.check(600)); + assertEquals(3600L, controller.check(3600)); + assertEquals(3600L, controller.check(-1)); + assertEquals(3600L, controller.check(UWSJob.UNLIMITED_DURATION)); + assertEquals(3600L, controller.check(3601)); + + }catch(Exception t){ + t.printStackTrace(); + fail(); + } + } + + @Test + public void testGetDefault(){ + ExecutionDurationController controller = new ExecutionDurationController(); + + // By default, when nothing is set, the default execution duration is UNLIMITED: + assertEquals(UWSJob.UNLIMITED_DURATION, controller.getDefault()); + + // With no duration, the default execution duration should remain UNLIMITED: + controller.setDefaultExecutionDuration(UWSJob.UNLIMITED_DURATION); + assertEquals(UWSJob.UNLIMITED_DURATION, controller.getDefault()); + + // With a negative duration, the execution duration should also be UNLIMITED: + controller.setDefaultExecutionDuration(-1); + assertEquals(UWSJob.UNLIMITED_DURATION, controller.getDefault()); + + // With an execution duration of 10 minutes: + controller.setDefaultExecutionDuration(600); + assertEquals(600L, controller.getDefault()); + + // The default value must always be less than the maximum value: + controller.setMaxExecutionDuration(300); + assertEquals(300L, controller.getDefault()); + } + + @Test + public void testGetMaxExecutionDuration(){ + ExecutionDurationController controller = new ExecutionDurationController(); + + // By default, when nothing is set, the maximum execution duration is UNLIMITED: + assertEquals(UWSJob.UNLIMITED_DURATION, controller.getMaxExecutionDuration()); + + // With no duration, the maximum execution duration should remain UNLIMITED: + controller.setMaxExecutionDuration(UWSJob.UNLIMITED_DURATION); + assertEquals(UWSJob.UNLIMITED_DURATION, controller.getMaxExecutionDuration()); + + // With a negative duration, the execution duration should also be UNLIMITED: + controller.setMaxExecutionDuration(-1); + assertEquals(UWSJob.UNLIMITED_DURATION, controller.getMaxExecutionDuration()); + + // With an execution duration of 10 minutes: + controller.setMaxExecutionDuration(600); + assertEquals(600L, controller.getMaxExecutionDuration()); + } + + @Test + public void testAllowModification(){ + ExecutionDurationController controller = new ExecutionDurationController(); + + // By default, user modification of the execution duration is allowed: + assertTrue(controller.allowModification()); + + controller.allowModification(true); + assertTrue(controller.allowModification()); + + controller.allowModification(false); + assertFalse(controller.allowModification()); + } + +} diff --git a/test/uws/service/UWSUrlTest.java b/test/uws/service/UWSUrlTest.java new file mode 100644 index 0000000..a61db70 --- /dev/null +++ b/test/uws/service/UWSUrlTest.java @@ -0,0 +1,560 @@ +package uws.service; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.security.Principal; +import java.util.Collection; +import java.util.Enumeration; +import java.util.Locale; +import java.util.Map; + +import javax.servlet.AsyncContext; +import javax.servlet.DispatcherType; +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletInputStream; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import javax.servlet.http.Part; + +import org.junit.Before; +import org.junit.Test; + +public class UWSUrlTest { + + public static final class TestHttpServletRequest implements HttpServletRequest { + + private final String scheme; + private final String serverName; + private final int serverPort; + private final String contextPath; + private final String pathInfo; + private final String requestURI; + private final StringBuffer requestURL; + private final String servletPath; + + public TestHttpServletRequest(StringBuffer requestURL, String requestURI, String scheme, String serverName, int serverPort, String contextPath, String servletPath, String pathInfo){ + this.scheme = scheme; + this.serverName = serverName; + this.serverPort = serverPort; + this.contextPath = contextPath; + this.pathInfo = pathInfo; + this.requestURI = requestURI; + this.requestURL = requestURL; + this.servletPath = servletPath; + } + + @Override + public String getScheme(){ + return scheme; + } + + @Override + public String getServerName(){ + return serverName; + } + + @Override + public int getServerPort(){ + return serverPort; + } + + @Override + public String getPathInfo(){ + return pathInfo; + } + + @Override + public String getRequestURI(){ + return requestURI; + } + + @Override + public StringBuffer getRequestURL(){ + return requestURL; + } + + @Override + public String getContextPath(){ + return contextPath; + } + + @Override + public String getServletPath(){ + return servletPath; + } + + @Override + public AsyncContext getAsyncContext(){ + return null; + } + + @Override + public Object getAttribute(String arg0){ + return null; + } + + @Override + public Enumeration getAttributeNames(){ + return null; + } + + @Override + public String getCharacterEncoding(){ + return null; + } + + @Override + public int getContentLength(){ + return 0; + } + + @Override + public String getContentType(){ + return null; + } + + @Override + public DispatcherType getDispatcherType(){ + return null; + } + + @Override + public ServletInputStream getInputStream() throws IOException{ + return null; + } + + @Override + public String getLocalAddr(){ + return null; + } + + @Override + public String getLocalName(){ + return null; + } + + @Override + public int getLocalPort(){ + return 0; + } + + @Override + public Locale getLocale(){ + return null; + } + + @Override + public Enumeration getLocales(){ + return null; + } + + @Override + public String getParameter(String arg0){ + return null; + } + + @Override + public Map getParameterMap(){ + return null; + } + + @Override + public Enumeration getParameterNames(){ + return null; + } + + @Override + public String[] getParameterValues(String arg0){ + return null; + } + + @Override + public String getProtocol(){ + return null; + } + + @Override + public BufferedReader getReader() throws IOException{ + return null; + } + + @Override + public String getRealPath(String arg0){ + return null; + } + + @Override + public String getRemoteAddr(){ + return null; + } + + @Override + public String getRemoteHost(){ + return null; + } + + @Override + public int getRemotePort(){ + return 0; + } + + @Override + public RequestDispatcher getRequestDispatcher(String arg0){ + return null; + } + + @Override + public ServletContext getServletContext(){ + return null; + } + + @Override + public boolean isAsyncStarted(){ + return false; + } + + @Override + public boolean isAsyncSupported(){ + return false; + } + + @Override + public boolean isSecure(){ + return false; + } + + @Override + public void removeAttribute(String arg0){ + + } + + @Override + public void setAttribute(String arg0, Object arg1){ + + } + + @Override + public void setCharacterEncoding(String arg0) throws UnsupportedEncodingException{ + + } + + @Override + public AsyncContext startAsync(){ + return null; + } + + @Override + public AsyncContext startAsync(ServletRequest arg0, ServletResponse arg1){ + return null; + } + + @Override + public boolean authenticate(HttpServletResponse arg0) throws IOException, ServletException{ + return false; + } + + @Override + public String getAuthType(){ + return null; + } + + @Override + public Cookie[] getCookies(){ + return null; + } + + @Override + public long getDateHeader(String arg0){ + return 0; + } + + @Override + public String getHeader(String arg0){ + return null; + } + + @Override + public Enumeration getHeaderNames(){ + return null; + } + + @Override + public Enumeration getHeaders(String arg0){ + return null; + } + + @Override + public int getIntHeader(String arg0){ + return 0; + } + + @Override + public String getMethod(){ + return null; + } + + @Override + public Part getPart(String arg0) throws IOException, IllegalStateException, ServletException{ + return null; + } + + @Override + public Collection getParts() throws IOException, IllegalStateException, ServletException{ + return null; + } + + @Override + public String getPathTranslated(){ + return null; + } + + @Override + public String getQueryString(){ + return null; + } + + @Override + public String getRemoteUser(){ + return null; + } + + @Override + public String getRequestedSessionId(){ + return null; + } + + @Override + public HttpSession getSession(){ + return null; + } + + @Override + public HttpSession getSession(boolean arg0){ + return null; + } + + @Override + public Principal getUserPrincipal(){ + return null; + } + + @Override + public boolean isRequestedSessionIdFromCookie(){ + return false; + } + + @Override + public boolean isRequestedSessionIdFromURL(){ + return false; + } + + @Override + public boolean isRequestedSessionIdFromUrl(){ + return false; + } + + @Override + public boolean isRequestedSessionIdValid(){ + return false; + } + + @Override + public boolean isUserInRole(String arg0){ + return false; + } + + @Override + public void login(String arg0, String arg1) throws ServletException{} + + @Override + public void logout() throws ServletException{} + + } + + private HttpServletRequest requestFromRoot2root; + private HttpServletRequest requestFromRoot2async; + + private HttpServletRequest requestFromPath2root; + private HttpServletRequest requestFromPath2async; + + private HttpServletRequest requestWithServletPathNull; + + @Before + public void setUp() throws Exception{ + requestFromRoot2root = new TestHttpServletRequest(new StringBuffer("http://localhost:8080/tapTest/"), "/tapTest/", "http", "localhost", 8080, "/tapTest", "", "/"); + requestFromRoot2async = new TestHttpServletRequest(new StringBuffer("http://localhost:8080/tapTest/async"), "/tapTest/async", "http", "localhost", 8080, "/tapTest", "", "/async"); + + requestFromPath2root = new TestHttpServletRequest(new StringBuffer("http://localhost:8080/tapTest/path/"), "/tapTest/path/", "http", "localhost", 8080, "/tapTest", "/path", "/"); + requestFromPath2async = new TestHttpServletRequest(new StringBuffer("http://localhost:8080/tapTest/path/async"), "/tapTest/path/async", "http", "localhost", 8080, "/tapTest", "/path", "/async"); + + requestWithServletPathNull = new TestHttpServletRequest(new StringBuffer("http://localhost:8080/tapTest/"), "/tapTest/", "http", "localhost", 8080, "/tapTest", null, "/"); + } + + @Test + public void testExtractBaseURI(){ + // CASE 1: http://localhost:8080/tapTest/path with url-pattern = /path/* + try{ + UWSUrl uu = new UWSUrl(requestFromPath2root); + assertEquals("/path", uu.getBaseURI()); + assertEquals("", uu.getUwsURI()); + assertEquals("http://localhost:8080/tapTest/path/", uu.toString()); + }catch(Exception e){ + e.printStackTrace(System.err); + fail("This HTTP request is perfectly correct: " + requestFromPath2root.getRequestURL()); + } + + // CASE 2: http://localhost:8080/tapTest/path/async with url-pattern = /path/* + try{ + UWSUrl uu = new UWSUrl(requestFromPath2async); + assertEquals("/path", uu.getBaseURI()); + assertEquals("/async", uu.getUwsURI()); + assertEquals("http://localhost:8080/tapTest/path/async", uu.toString()); + }catch(Exception e){ + e.printStackTrace(System.err); + fail("This HTTP request is perfectly correct: " + requestFromPath2async.getRequestURL()); + } + + // CASE 3: http://localhost:8080/tapTest with url-pattern = /* + try{ + UWSUrl uu = new UWSUrl(requestFromRoot2root); + assertEquals("", uu.getBaseURI()); + assertEquals("", uu.getUwsURI()); + assertEquals("http://localhost:8080/tapTest/", uu.toString()); + }catch(Exception e){ + e.printStackTrace(System.err); + fail("This HTTP request is perfectly correct: " + requestFromRoot2root.getRequestURL()); + } + + // CASE 4: http://localhost:8080/tapTest/async with url-pattern = /* + try{ + UWSUrl uu = new UWSUrl(requestFromRoot2async); + assertEquals("", uu.getBaseURI()); + assertEquals("/async", uu.getUwsURI()); + assertEquals("http://localhost:8080/tapTest/async", uu.toString()); + }catch(Exception e){ + e.printStackTrace(System.err); + fail("This HTTP request is perfectly correct: " + requestFromRoot2async.getRequestURL()); + } + + // CASE 5: http://localhost:8080/tapTest/path/async with url-pattern = /path/* + try{ + new UWSUrl(requestWithServletPathNull); + fail("RequestURL with no servlet path: this test should have failed!"); + }catch(Exception e){ + assertTrue(e instanceof NullPointerException); + assertEquals(e.getMessage(), "The extracted base UWS URI is NULL!"); + } + } + + @Test + public void testLoadHttpServletRequest(){ + // CASE 1a: http://localhost:8080/tapTest/path with url-pattern = /path/* + try{ + UWSUrl uu = new UWSUrl(requestFromPath2root); + uu.load(requestFromPath2root); + assertEquals("", uu.getUwsURI()); + assertEquals("http://localhost:8080/tapTest/path/async/123456A", uu.jobSummary("async", "123456A").toString()); + }catch(Exception e){ + e.printStackTrace(System.err); + fail("This HTTP request is perfectly correct: " + requestFromPath2root.getRequestURL()); + } + // CASE 1b: Idem while loading http://localhost:8080/tapTest/path/async + try{ + UWSUrl uu = new UWSUrl(requestFromPath2root); + uu.load(requestFromPath2async); + assertEquals("/async", uu.getUwsURI()); + assertEquals("http://localhost:8080/tapTest/path/async/123456A", uu.jobSummary("async", "123456A").toString()); + }catch(Exception e){ + e.printStackTrace(System.err); + fail("This HTTP request is perfectly correct: " + requestFromPath2async.getRequestURL()); + } + + // CASE 2a: http://localhost:8080/tapTest/path/async with url-pattern = /path/* + try{ + UWSUrl uu = new UWSUrl(requestFromPath2async); + uu.load(requestFromPath2async); + assertEquals("/async", uu.getUwsURI()); + assertEquals("http://localhost:8080/tapTest/path/async/123456A", uu.jobSummary("async", "123456A").toString()); + }catch(Exception e){ + e.printStackTrace(System.err); + fail("This HTTP request is perfectly correct: " + requestFromPath2async.getRequestURL()); + } + + // CASE 2b: Idem while loading http://localhost:8080/tapTest/path + try{ + UWSUrl uu = new UWSUrl(requestFromPath2async); + uu.load(requestFromPath2root); + assertEquals("", uu.getUwsURI()); + assertEquals("http://localhost:8080/tapTest/path/async/123456A", uu.jobSummary("async", "123456A").toString()); + }catch(Exception e){ + e.printStackTrace(System.err); + fail("This HTTP request is perfectly correct: " + requestFromPath2root.getRequestURL()); + } + + // CASE 3a: http://localhost:8080/tapTest with url-pattern = /* + try{ + UWSUrl uu = new UWSUrl(requestFromRoot2root); + uu.load(requestFromRoot2root); + assertEquals("", uu.getUwsURI()); + assertEquals("http://localhost:8080/tapTest/async/123456A", uu.jobSummary("async", "123456A").toString()); + }catch(NullPointerException e){ + fail("This HTTP request is perfectly correct: " + requestFromRoot2root.getRequestURL()); + } + + // CASE 3b: Idem while loading http://localhost:8080/tapTest/async + try{ + UWSUrl uu = new UWSUrl(requestFromRoot2root); + uu.load(requestFromRoot2async); + assertEquals("/async", uu.getUwsURI()); + assertEquals("http://localhost:8080/tapTest/async/123456A", uu.jobSummary("async", "123456A").toString()); + }catch(Exception e){ + e.printStackTrace(System.err); + fail("This HTTP request is perfectly correct: " + requestFromRoot2async.getRequestURL()); + } + + // CASE 4a: http://localhost:8080/tapTest/async with url-pattern = /* + try{ + UWSUrl uu = new UWSUrl(requestFromRoot2async); + uu.load(requestFromRoot2async); + assertEquals("/async", uu.getUwsURI()); + assertEquals("http://localhost:8080/tapTest/async/123456A", uu.jobSummary("async", "123456A").toString()); + }catch(Exception e){ + e.printStackTrace(System.err); + fail("This HTTP request is perfectly correct: " + requestFromRoot2async.getRequestURL()); + } + + // CASE 4b: Idem while loading http://localhost:8080/tapTest + try{ + UWSUrl uu = new UWSUrl(requestFromRoot2async); + uu.load(requestFromRoot2root); + assertEquals("", uu.getUwsURI()); + assertEquals("http://localhost:8080/tapTest/async/123456A", uu.jobSummary("async", "123456A").toString()); + }catch(Exception e){ + e.printStackTrace(System.err); + fail("This HTTP request is perfectly correct: " + requestFromRoot2root.getRequestURL()); + } + + // SPECIAL CASE 1: Creation with http://localhost:8080/tapTest[/async] (/*) but loading with http://localhost:8080/tapTest/path[/async] (/path/*): + try{ + UWSUrl uu = new UWSUrl(requestFromRoot2async); + uu.load(requestFromPath2async); + assertFalse(uu.getUwsURI().equals("")); + }catch(Exception e){ + e.printStackTrace(System.err); + fail("This HTTP request is perfectly correct: " + requestFromRoot2root.getRequestURL()); + } + } + +} diff --git a/test/uws/service/file/TestLogRotation.java b/test/uws/service/file/TestLogRotation.java new file mode 100644 index 0000000..16aa1e4 --- /dev/null +++ b/test/uws/service/file/TestLogRotation.java @@ -0,0 +1,277 @@ +package uws.service.file; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.fail; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; + +import org.junit.Test; + +import uws.service.log.DefaultUWSLog; +import uws.service.log.UWSLog; +import uws.service.log.UWSLog.LogLevel; + +public class TestLogRotation { + + @Test + public void testEventFrequencyCreation(){ + EventFrequency freq; + + try{ + String DEFAULT_FREQ = "daily at 00:00"; + + // FREQ = NULL => !!! ; frequency = every day + freq = new EventFrequency(null); + assertEquals(DEFAULT_FREQ, freq.toString()); + + // FREQ = "" => !!! ; frequency = every day + freq = new EventFrequency(""); + assertEquals(DEFAULT_FREQ, freq.toString()); + + // FREQ = "blabla" => !!! + freq = new EventFrequency("blabla"); + assertEquals(DEFAULT_FREQ, freq.toString()); + + /* *********** */ + /* DAILY EVENT */ + /* *********** */ + DEFAULT_FREQ = "daily at 00:00"; + + // FREQ = "D" => ok! ; frequency = every day at midnight + freq = new EventFrequency("D"); + assertEquals(DEFAULT_FREQ, freq.toString()); + + // FREQ = "D 06" => !!! ; frequency = every day at midnight + freq = new EventFrequency("D 06"); + assertEquals(DEFAULT_FREQ, freq.toString()); + + // FREQ = "D 06 30" => ok! ; frequency = every day at 06:30 + freq = new EventFrequency("D 06 30"); + assertEquals("daily at 06:30", freq.toString()); + + // FREQ = "D 6 30" => ok! ; frequency = every day at 06:30 + freq = new EventFrequency("D 6 30"); + assertEquals("daily at 06:30", freq.toString()); + + // FREQ = "D 06 30" => ok! (with spaces and tabs inside) ; frequency = every day at 06:30 + freq = new EventFrequency("D 06 30"); + assertEquals("daily at 06:30", freq.toString()); + + // FREQ = "D 24 30" => !!! ; frequency = every day at midnight + freq = new EventFrequency("D 24 30"); + assertEquals(DEFAULT_FREQ, freq.toString()); + + // FREQ = "D 06 60" => !!! ; frequency = every day at midnight + freq = new EventFrequency("D 06 60"); + assertEquals(DEFAULT_FREQ, freq.toString()); + + // FREQ = "D 6 30 01 blabla" => ok! ; frequency = every day at 06:30 + freq = new EventFrequency("D 6 30 01 blabla"); + assertEquals("daily at 06:30", freq.toString()); + + // FREQ = "d 06 30" => !!! ; frequency = every day at midnight + freq = new EventFrequency("d 06 30"); + assertEquals(DEFAULT_FREQ, freq.toString()); + + // FREQ = "D HH mm" => !!! + freq = new EventFrequency("D HH mm"); + assertEquals(DEFAULT_FREQ, freq.toString()); + + /* ********** */ + /* WEEK EVENT */ + /* ********** */ + DEFAULT_FREQ = "weekly on Sunday at 00:00"; + + // FREQ = "W" => ok! ; frequency = every week the Sunday at midnight + freq = new EventFrequency("W"); + assertEquals(DEFAULT_FREQ, freq.toString()); + + // FREQ = "W 06" => !!! ; frequency = every week the Sunday at midnight + freq = new EventFrequency("W 06"); + assertEquals(DEFAULT_FREQ, freq.toString()); + + // FREQ = "W 06 30" => !!! ; frequency = every week the Sunday at midnight + freq = new EventFrequency("W 06 30"); + assertEquals(DEFAULT_FREQ, freq.toString()); + + // FREQ = "W 2" => !!! ; frequency = every week the Sunday at midnight + freq = new EventFrequency("W 2"); + assertEquals(DEFAULT_FREQ, freq.toString()); + + // FREQ = "W 2 06" => !!! ; frequency = every week the Sunday at midnight + freq = new EventFrequency("W 2 06"); + assertEquals(DEFAULT_FREQ, freq.toString()); + + // FREQ = "W 2 06 30" => ok! ; frequency = every week the Monday at 06:30 + freq = new EventFrequency("W 2 06 30"); + assertEquals("weekly on Monday at 06:30", freq.toString()); + + // FREQ = "W 0 06 30" => !!! ; frequency = every week the Sunday at 06:30 + freq = new EventFrequency("W 0 06 30"); + assertEquals(DEFAULT_FREQ, freq.toString()); + + // FREQ = "W 10 06 30" => !!! ; frequency = every week the Sunday at 06:30 + freq = new EventFrequency("W 10 06 30"); + assertEquals(DEFAULT_FREQ, freq.toString()); + + // FREQ = "w 2 06 30" => !!! ; frequency = every day at 00:00 + freq = new EventFrequency("w 2 06 30"); + assertEquals("daily at 00:00", freq.toString()); + + // FREQ = "W 2 6 30" => ok! ; frequency = every week the Monday at 06:30 + freq = new EventFrequency("W 2 6 30"); + assertEquals("weekly on Monday at 06:30", freq.toString()); + + // FREQ = "W 2 6 30" => ok! (with spaces and tabs inside) ; frequency = every week the Monday at 06:30 + freq = new EventFrequency("W 2 6 30"); + assertEquals("weekly on Monday at 06:30", freq.toString()); + + // FREQ = "W 2 6 30 12 blabla" => ok! ; frequency = every week the Monday at 06:30 + freq = new EventFrequency("W 2 6 30 12 blabla"); + assertEquals("weekly on Monday at 06:30", freq.toString()); + + /* ***************************************** */ + /* MONTH EVENT (same code as for WEEK EVENT) */ + /* ***************************************** */ + DEFAULT_FREQ = "monthly on the 1st at 00:00"; + + // FREQ = "M 2 06 30" => ok! ; frequency = every month on the 2nd at 06:30 + freq = new EventFrequency("M 2 06 30"); + assertEquals("monthly on the 2nd at 06:30", freq.toString()); + + // FREQ = "M 2 06 30" => ok! (with spaces and tabs inside) ; frequency = every month on the 2nd at 06:30 + freq = new EventFrequency("M 2 06 30"); + assertEquals("monthly on the 2nd at 06:30", freq.toString()); + + // FREQ = "m 2 06 30" => !!! ; frequency = every minute + freq = new EventFrequency("m 2 06 30"); + assertEquals("every minute", freq.toString()); + + // FREQ = "M 0 06 30" => !!! ; frequency = every month on the 1st at 00:00 + freq = new EventFrequency("M 0 06 30"); + assertEquals(DEFAULT_FREQ, freq.toString()); + + // FREQ = "M 32 06 30" => !!! ; frequency = every month on the 1st at 00:00 + freq = new EventFrequency("M 32 06 30"); + assertEquals(DEFAULT_FREQ, freq.toString()); + + /* ********** */ + /* HOUR EVENT */ + /* ********** */ + DEFAULT_FREQ = "hourly at 00"; + + // FREQ = "h" => ok! ; frequency = every hour at 00 + freq = new EventFrequency("h"); + assertEquals(DEFAULT_FREQ, freq.toString()); + + // FREQ = "h 10" => ok! ; frequency = every hour at 10 + freq = new EventFrequency("h 10"); + assertEquals("hourly at 10", freq.toString()); + + // FREQ = "h 10" => ok! (with spaces and tabs inside) ; frequency = every hour at 10 + freq = new EventFrequency("h 10"); + assertEquals("hourly at 10", freq.toString()); + + // FREQ = "H 10" => !!! ; frequency = every day at 00:00 + freq = new EventFrequency("H 10"); + assertEquals("daily at 00:00", freq.toString()); + + // FREQ = "h 5" => ok! ; frequency = every hour at 05 + freq = new EventFrequency("h 5"); + assertEquals("hourly at 05", freq.toString()); + + // FREQ = "h 60" => !!! ; frequency = every hour at 00 + freq = new EventFrequency("h 60"); + assertEquals("hourly at 00", freq.toString()); + + // FREQ = "h 10 12 blabla" => ok! ; frequency = every hour at 10 + freq = new EventFrequency("h 10 12 blabla"); + assertEquals("hourly at 10", freq.toString()); + + /* ********** */ + /* HOUR EVENT */ + /* ********** */ + DEFAULT_FREQ = "every minute"; + + // FREQ = "m" => ok! ; frequency = every minute + freq = new EventFrequency("m"); + assertEquals(DEFAULT_FREQ, freq.toString()); + + // FREQ = "m 10 blabla" => ok! ; frequency = every minute + freq = new EventFrequency("m 10 blabla"); + assertEquals(DEFAULT_FREQ, freq.toString()); + + // FREQ = "M" => !!! ; frequency = every month on the 1st at 00:00 + freq = new EventFrequency("M"); + assertEquals("monthly on the 1st at 00:00", freq.toString()); + + }catch(Exception e){ + e.printStackTrace(System.err); + fail("UNEXPECTED EXCEPTION: \"" + e.getMessage() + "\""); + } + } + + @Test + public void testGetLogOutput(){ + try{ + final LocalUWSFileManager fileManager = new LocalUWSFileManager(new File(".")); + fileManager.logRotation = new EventFrequency("m"); + final int MAX_TIME = 3000; // 3 seconds => 68 messages (for 5 threads) + int nbExpectedMessages = 0; + + // Delete old log file: + fileManager.getLogFile(LogLevel.DEBUG, null).delete(); + + // Log a lot of messages: + final UWSLog logger = new DefaultUWSLog(fileManager); + for(int i = 0; i < 5; i++){ + final int logFreq = i + 1; + nbExpectedMessages += 30 / logFreq; + (new Thread(new Runnable(){ + @Override + public void run(){ + try{ + final int nbMsgs = 30 / logFreq; + final int freq = MAX_TIME / nbMsgs; + for(int cnt = 0; cnt < nbMsgs; cnt++){ + logger.log(LogLevel.INFO, "TEST", "LOG MESSAGE FROM Thread-" + logFreq, null); + assertFalse(fileManager.getLogOutput(LogLevel.INFO, "UWS").checkError()); // if true, it means that at least one attempt to write something fails, and so, that write attempts have been done after a log rotation! + Thread.sleep(freq); + } + }catch(InterruptedException e){ + e.printStackTrace(System.err); + fail("ERROR WITH THE THREAD-" + logFreq); + }catch(IOException e){ + e.printStackTrace(System.err); + fail("IO ERROR WHEN RETRIEVING THE LOG OUTPUT IN THE THREAD-" + logFreq); + } + } + })).start(); + } + Thread.sleep(MAX_TIME); + + // Check that all messages have been well written: + BufferedReader input = new BufferedReader(new InputStreamReader(fileManager.getLogInput(LogLevel.DEBUG, null))); + int nbLines = 0; + while(input.readLine() != null) + nbLines++; + nbLines -= 3; // deduce the number of 3 header lines + assertEquals(nbExpectedMessages, nbLines); + + // Delete log file if no error: + fileManager.getLogFile(LogLevel.DEBUG, null).delete(); + + }catch(InterruptedException e){ + e.printStackTrace(System.err); + fail("CAN NOT WAIT 3 SECONDS!"); + }catch(Exception e){ + e.printStackTrace(System.err); + fail("CAN NOT CREATE THE FILE MANAGER!"); + } + } + +} diff --git a/test/uws/service/log/TestDefaultUWSLog.java b/test/uws/service/log/TestDefaultUWSLog.java new file mode 100644 index 0000000..2b39544 --- /dev/null +++ b/test/uws/service/log/TestDefaultUWSLog.java @@ -0,0 +1,63 @@ +package uws.service.log; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; + +import org.junit.Test; + +import uws.service.log.UWSLog.LogLevel; + +public class TestDefaultUWSLog { + + @Test + public void testCanLog(){ + OutputStream output = new ByteArrayOutputStream(); + DefaultUWSLog logger = new DefaultUWSLog(output); + + // Default value = DEBUG => ALL MESSAGES CAN BE LOGGED + assertEquals(LogLevel.DEBUG, logger.getMinLogLevel()); + for(LogLevel ll : LogLevel.values()) + assertTrue(logger.canLog(ll)); + + // Test: INFO => ALL EXCEPT DEBUG CAN BE LOGGED + logger.setMinLogLevel(LogLevel.INFO); + assertEquals(LogLevel.INFO, logger.getMinLogLevel()); + assertFalse(logger.canLog(LogLevel.DEBUG)); + assertTrue(logger.canLog(LogLevel.INFO)); + assertTrue(logger.canLog(LogLevel.WARNING)); + assertTrue(logger.canLog(LogLevel.ERROR)); + assertTrue(logger.canLog(LogLevel.FATAL)); + + // Test: WARNING => ALL EXCEPT DEBUG AND INFO CAN BE LOGGED + logger.setMinLogLevel(LogLevel.WARNING); + assertEquals(LogLevel.WARNING, logger.getMinLogLevel()); + assertFalse(logger.canLog(LogLevel.DEBUG)); + assertFalse(logger.canLog(LogLevel.INFO)); + assertTrue(logger.canLog(LogLevel.WARNING)); + assertTrue(logger.canLog(LogLevel.ERROR)); + assertTrue(logger.canLog(LogLevel.FATAL)); + + // Test: ERROR => ONLY ERROR AND FATAL CAN BE LOGGED + logger.setMinLogLevel(LogLevel.ERROR); + assertEquals(LogLevel.ERROR, logger.getMinLogLevel()); + assertFalse(logger.canLog(LogLevel.DEBUG)); + assertFalse(logger.canLog(LogLevel.INFO)); + assertFalse(logger.canLog(LogLevel.WARNING)); + assertTrue(logger.canLog(LogLevel.ERROR)); + assertTrue(logger.canLog(LogLevel.FATAL)); + + // Test: FATAL => ONLY FATAL CAN BE LOGGED + logger.setMinLogLevel(LogLevel.FATAL); + assertEquals(LogLevel.FATAL, logger.getMinLogLevel()); + assertFalse(logger.canLog(LogLevel.DEBUG)); + assertFalse(logger.canLog(LogLevel.INFO)); + assertFalse(logger.canLog(LogLevel.WARNING)); + assertFalse(logger.canLog(LogLevel.ERROR)); + assertTrue(logger.canLog(LogLevel.FATAL)); + } + +} -- GitLab

    Warning: This function will indirectly open and keep a database connection, so that the job can be started just after its call. + * If it turns out that the execution won't start just after this call, the DB connection should be closed in some way in order to save database resources.