diff --git a/buildADQL.xml b/buildADQL.xml index d2ae0630c216a88be631b1701a69bd8ad061feef..7903db53c6c67c71fdbb1b094c884331f02eaaf1 100644 --- a/buildADQL.xml +++ b/buildADQL.xml @@ -97,4 +97,4 @@ <jar destfile="${javadocJarFile}" basedir="${javadocDir}" /> </target> -</project> \ No newline at end of file +</project> diff --git a/buildTAP.xml b/buildTAP.xml index 795b0c2ae7c307c8d0d19ecc803e3abd42e815eb..6f7ec0a810055ae4e78251e336fb2f5b21d3a8ac 100644 --- a/buildTAP.xml +++ b/buildTAP.xml @@ -34,11 +34,28 @@ <condition><not><isset property="SERVLET-API"/></not></condition> </fail> + <fail message="The property JUNIT-API must be set! It provides the path toward a directory or a JAR which contains all classes needed to use JUnit."> + <condition><not><isset property="JUNIT-API"/></not></condition> + </fail> + <path id="tap.classpath"> <pathelement location="${cosJar}" /> + <pathelement location="${jsonJar}" /> + <pathelement location="${stilJar}" /> + <pathelement location="${POSTGRES}" /> + <pathelement location="${SERVLET-API}" /> + </path> + + <!-- Define the classpath which includes the junit.jar and the classes after compiling--> + <path id="junit.class.path"> + <pathelement location="${cosJar}" /> + <pathelement location="${jsonJar}" /> <pathelement location="${stilJar}" /> <pathelement location="${POSTGRES}" /> <pathelement location="${SERVLET-API}" /> + + <pathelement path="${JUNIT-API}" /> + <pathelement location="bin" /> </path> <echo>TAP LIBRARY VERSION = ${version}</echo> @@ -52,7 +69,16 @@ <target name="cleanAll" depends="clean,cleanJavadoc" description="Delete all files generated by this ANT file for the set version." /> <!-- LIB & SOURCES --> - <target name="clean" description="Delete the JARs for the library (classes) and for its sources for the set version."> + <target name="junitValidation" description="Executes all JUnit tests before building the library and stop ANT at any error."> + <junit printsummary="on" fork="yes" haltonfailure="yes"> + <classpath refid="junit.class.path" /> + <test name="tap.config.AllTests" outfile="testReports"> + <formatter type="plain" usefile="yes" /> + </test> + </junit> + </target> + + <target name="clean" depends="junitValidation" description="Delete the JARs for the library (classes) and for its sources for the set version."> <delete file="${libJarFile}" failonerror="false" /> <delete file="${srcJarFile}" failonerror="false" /> <delete dir="${compileDir}" failonerror="false" /> diff --git a/src/tap/ServiceConnection.java b/src/tap/ServiceConnection.java index 0da13656782e74779f9d9d4ca48e55c022072165..a24782657dc0d5dd45cce53a6401eb3ced3dfb95 100644 --- a/src/tap/ServiceConnection.java +++ b/src/tap/ServiceConnection.java @@ -50,10 +50,10 @@ public interface ServiceConnection { * List of possible limit units. * * @author Grégory Mantelet (CDS;ARI) - * @version 2.0 (10/2014) + * @version 2.0 (01/2015) */ public static enum LimitUnit{ - rows("row"), bytes("byte"); + rows("row"), bytes("byte"), kilobytes("kilobyte"), megabytes("megabyte"), gigabytes("gigabyte"); private final String str; @@ -61,6 +61,119 @@ public interface ServiceConnection { 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. + * <i>Note: if this unit is not a factor of bytes, 1 is returned (so that the factor does not affect the value).</i> + * + * @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 #isCompatibleWith(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()); + } + + /** + * <p><i>(Strict copy of Integer.compare(int,int) of Java 1.7)</i></p> + * <p> + * Compares two {@code int} values numerically. + * The value returned is identical to what would be returned by: + * </p> + * <pre> + * Integer.valueOf(x).compareTo(Integer.valueOf(y)) + * </pre> + * + * @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); + } + + /** + * <p><i>(Strict copy of Integer.compare(long,long) of Java 1.7)</i></p> + * <p> + * Compares two {@code long} values numerically. + * The value returned is identical to what would be returned by: + * </p> + * <pre> + * Long.valueOf(x).compareTo(Long.valueOf(y)) + * </pre> + * + * @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; diff --git a/src/tap/config/DefaultServiceConnection.java b/src/tap/config/DefaultServiceConnection.java new file mode 100644 index 0000000000000000000000000000000000000000..e6c9dbc07b1063f65f262774bffeb0df9e428152 --- /dev/null +++ b/src/tap/config/DefaultServiceConnection.java @@ -0,0 +1,585 @@ +package tap.config; + +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_IS_AVAILABLE; +import static tap.config.TAPConfiguration.DEFAULT_RETENTION_PERIOD; +import static tap.config.TAPConfiguration.DEFAULT_UPLOAD_MAX_FILE_SIZE; +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_DISABILITY_REASON; +import static tap.config.TAPConfiguration.KEY_FILE_MANAGER; +import static tap.config.TAPConfiguration.KEY_FILE_ROOT_PATH; +import static tap.config.TAPConfiguration.KEY_GROUP_USER_DIRECTORIES; +import static tap.config.TAPConfiguration.KEY_IS_AVAILABLE; +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_OUTPUT_FORMATS; +import static tap.config.TAPConfiguration.KEY_PROVIDER_NAME; +import static tap.config.TAPConfiguration.KEY_SERVICE_DESCRIPTION; +import static tap.config.TAPConfiguration.KEY_UPLOAD_ENABLED; +import static tap.config.TAPConfiguration.KEY_UPLOAD_MAX_FILE_SIZE; +import static tap.config.TAPConfiguration.VALUE_CSV; +import static tap.config.TAPConfiguration.VALUE_JSON; +import static tap.config.TAPConfiguration.VALUE_LOCAL; +import static tap.config.TAPConfiguration.VALUE_SV; +import static tap.config.TAPConfiguration.VALUE_TSV; +import static tap.config.TAPConfiguration.fetchClass; +import static tap.config.TAPConfiguration.getProperty; +import static tap.config.TAPConfiguration.isClassPath; +import static tap.config.TAPConfiguration.parseLimit; + +import java.io.File; +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.formatter.JSONFormat; +import tap.formatter.OutputFormat; +import tap.formatter.SVFormat; +import tap.formatter.VOTableFormat; +import tap.log.DefaultTAPLog; +import tap.log.TAPLog; +import tap.metadata.TAPMetadata; +import uws.UWSException; +import uws.service.UserIdentifier; +import uws.service.file.LocalUWSFileManager; +import uws.service.file.UWSFileManager; +import adql.db.FunctionDef; + +public final class DefaultServiceConnection implements ServiceConnection { + + private UWSFileManager fileManager; + + private TAPLog logger; + + private DefaultTAPFactory tapFactory; + + private final String providerName; + private final String serviceDescription; + + private boolean isAvailable = false; // the TAP service must be disabled until the end of its connection initialization + private String availability = null; + + private int[] executionDuration = new int[2]; + private int[] retentionPeriod = new int[2]; + + private final ArrayList<OutputFormat> outputFormats; + + private int[] outputLimits = new int[]{-1,-1}; + private LimitUnit[] outputLimitTypes = new LimitUnit[2]; + + private boolean isUploadEnabled = false; + private int[] uploadLimits = new int[]{-1,-1}; + private LimitUnit[] uploadLimitTypes = new LimitUnit[2]; + private int maxUploadSize = DEFAULT_UPLOAD_MAX_FILE_SIZE; + + private final Collection<FunctionDef> udfs = new ArrayList<FunctionDef>(0); + + public DefaultServiceConnection(final Properties tapConfig) throws NullPointerException, TAPException, UWSException{ + // 1. INITIALIZE THE FILE MANAGER: + initFileManager(tapConfig); + + // 2. CREATE THE LOGGER: + logger = new DefaultTAPLog(fileManager); + + // 3. BUILD THE TAP FACTORY: + tapFactory = new DefaultTAPFactory(this, tapConfig); + + // 4. SET ALL GENERAL SERVICE CONNECTION INFORMATION: + providerName = getProperty(tapConfig, KEY_PROVIDER_NAME); + serviceDescription = getProperty(tapConfig, KEY_SERVICE_DESCRIPTION); + availability = getProperty(tapConfig, KEY_DISABILITY_REASON); + initRetentionPeriod(tapConfig); + initExecutionDuration(tapConfig); + + // 5. CONFIGURE OUTPUT: + // default output format = VOTable: + outputFormats = new ArrayList<OutputFormat>(1); + outputFormats.add(new VOTableFormat(this)); + // set additional output formats: + addOutputFormats(tapConfig); + // set output limits: + initOutputLimits(tapConfig); + + // 6. 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); + + // 7. MAKE THE SERVICE AVAILABLE (or not, depending on the property value): + String propValue = getProperty(tapConfig, KEY_IS_AVAILABLE); + isAvailable = (propValue == null) ? DEFAULT_IS_AVAILABLE : Boolean.parseBoolean(propValue); + } + + private void initFileManager(final Properties tapConfig) 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 path 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 = new File(rootPath); + + // 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{ + Class<? extends UWSFileManager> classObj = fetchClass(fileManagerType, KEY_FILE_MANAGER, UWSFileManager.class); + if (classObj == null) + throw new TAPException("Unknown value for the property \"" + KEY_FILE_MANAGER + "\": \"" + fileManagerType + "\". Only two possible values: " + VALUE_LOCAL + " or a class path between {...}."); + + try{ + fileManager = classObj.getConstructor(Properties.class).newInstance(tapConfig); + }catch(Exception e){ + if (e instanceof TAPException) + throw (TAPException)e; + else + throw new TAPException("Impossible to create a TAPFileManager instance with the constructor (java.util.Properties tapConfig) of \"" + classObj.getName() + "\" for the following reason: " + e.getMessage()); + } + } + } + + private void initRetentionPeriod(final Properties tapConfig){ + 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){ + retentionPeriod[0] = DEFAULT_RETENTION_PERIOD; + } + + // 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){ + retentionPeriod[1] = DEFAULT_RETENTION_PERIOD; + } + + // 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]; + } + + private void initExecutionDuration(final Properties tapConfig){ + 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){ + executionDuration[0] = DEFAULT_EXECUTION_DURATION; + } + + // 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){ + executionDuration[1] = DEFAULT_EXECUTION_DURATION; + } + + // 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]; + } + + private void addOutputFormats(final Properties tapConfig) throws TAPException{ + // Fetch the value of the property for additional output formats: + String formats = TAPConfiguration.getProperty(tapConfig, KEY_OUTPUT_FORMATS); + + // Since it is a comma separated list of output formats, a loop will parse this list comma by comma: + String f; + int indexSep; + while(formats != null && formats.length() > 0){ + // Get a format item from the list: + indexSep = formats.indexOf(','); + // 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: + // JSON + if (f.equalsIgnoreCase(VALUE_JSON)) + outputFormats.add(new JSONFormat(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 + "\"!"); + } + // custom OutputFormat + else if (isClassPath(f)){ + Class<? extends OutputFormat> userOutputFormatClass = fetchClass(f, KEY_OUTPUT_FORMATS, OutputFormat.class); + try{ + OutputFormat userOutputFormat = userOutputFormatClass.getConstructor(ServiceConnection.class).newInstance(this); + outputFormats.add(userOutputFormat); + }catch(Exception e){ + if (e instanceof TAPException) + throw (TAPException)e; + else + throw new TAPException("Impossible to create an OutputFormat<ResultSet> instance with the constructor (ServiceConnection<ResultSet>) of \"" + userOutputFormatClass.getName() + "\" (see the property output_add_format) for the following reason: " + e.getMessage()); + } + } + // unknown format + else + throw new TAPException("Unknown output format: " + f); + } + } + + 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 + + if (!setMaxOutputLimit((Integer)limit[0])) + throw new TAPException("The default output limit (here: " + outputLimits[0] + ") MUST be less or equal to the maximum output limit (here: " + limit[0] + ")!"); + } + + 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]; + + if (!setMaxUploadLimit((Integer)limit[0])) + throw new TAPException("The default upload limit (here: " + getProperty(tapConfig, KEY_DEFAULT_UPLOAD_LIMIT) + ") MUST be less or equal to the maximum upload limit (here: " + getProperty(tapConfig, KEY_MAX_UPLOAD_LIMIT) + ")!"); + } + + 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); + // ...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); + } + } + + @Override + public String getProviderName(){ + return providerName; + } + + @Override + public String getProviderDescription(){ + return serviceDescription; + } + + @Override + public boolean isAvailable(){ + return isAvailable; + } + + public void setAvailability(final boolean isAvailable){ + this.isAvailable = isAvailable; + } + + @Override + public String getAvailability(){ + return availability; + } + + public void setDisabilityReason(final String disabilityReason){ + availability = disabilityReason; + } + + @Override + public int[] getRetentionPeriod(){ + return retentionPeriod; + } + + public boolean setDefaultRetentionPeriod(final int period){ + if ((retentionPeriod[1] <= 0) || (period > 0 && period <= retentionPeriod[1])){ + retentionPeriod[0] = period; + return true; + }else + return false; + } + + public boolean setMaxRetentionPeriod(final int period){ + if (period <= 0 || (retentionPeriod[0] > 0 && period >= retentionPeriod[0])){ + retentionPeriod[1] = period; + return true; + }else + return false; + } + + @Override + public int[] getExecutionDuration(){ + return executionDuration; + } + + public boolean setDefaultExecutionDuration(final int period){ + if ((executionDuration[1] <= 0) || (period > 0 && period <= executionDuration[1])){ + executionDuration[0] = period; + return true; + }else + return false; + } + + public boolean setMaxExecutionDuration(final int period){ + if (period <= 0 || (executionDuration[0] > 0 && period >= executionDuration[0])){ + executionDuration[1] = period; + return true; + }else + return false; + } + + @Override + public Iterator<OutputFormat> 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; + } + + public void addOutputFormat(final OutputFormat newOutputFormat){ + outputFormats.add(newOutputFormat); + } + + 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; + } + + public boolean setDefaultOutputLimit(final int limit){ + if ((outputLimits[1] <= 0) || (limit > 0 && limit <= outputLimits[1])){ + outputLimits[0] = limit; + return true; + }else + return false; + } + + public boolean setMaxOutputLimit(final int limit){ + if (limit > 0 && outputLimits[0] > 0 && limit < outputLimits[0]) + return false; + else{ + outputLimits[1] = limit; + return true; + } + } + + @Override + public final LimitUnit[] getOutputLimitType(){ + return new LimitUnit[]{LimitUnit.rows,LimitUnit.rows}; + } + + @Override + public Collection<String> getCoordinateSystems(){ + return null; + } + + @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; + } + + public void setUploadLimitType(final LimitUnit type){ + if (type != null) + uploadLimitTypes = new LimitUnit[]{type,type}; + } + + 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; + } + + public boolean setMaxUploadLimit(final int limit){ + try{ + if (limit > 0 && uploadLimits[0] > 0 && LimitUnit.compare(limit, uploadLimitTypes[1], uploadLimits[0], uploadLimitTypes[0]) < 0) + return false; + else{ + uploadLimits[1] = limit; + return true; + } + }catch(TAPException e){ + return false; + } + } + + @Override + public int getMaxUploadSize(){ + return maxUploadSize; + } + + 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 UserIdentifier getUserIdentifier(){ + return null; // NO USER IDENTIFICATION + } + + @Override + public TAPMetadata getTAPMetadata(){ + // TODO GET METADATA + return null; + } + + @Override + public void setAvailable(boolean isAvailable, String message){ + this.isAvailable = isAvailable; + availability = message; + } + + @Override + public Collection<String> getGeometries(){ + return null; // ALL GEOMETRIES ALLOWED + } + + @Override + public Collection<FunctionDef> getUDFs(){ + return udfs; // FORBID ANY UNKNOWN FUNCTION + } + + @Override + public int getNbMaxAsyncJobs(){ + return -1; // UNLIMITED + } + +} diff --git a/src/tap/config/DefaultTAPFactory.java b/src/tap/config/DefaultTAPFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..ee3e3d24909b0eff16ec4ef749e66c2b5ab587d1 --- /dev/null +++ b/src/tap/config/DefaultTAPFactory.java @@ -0,0 +1,206 @@ +package tap.config; + +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_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_DRIVERS; +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.DriverManager; +import java.sql.SQLException; +import java.util.Properties; + +import tap.AbstractTAPFactory; +import tap.ServiceConnection; +import tap.TAPException; +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 adql.translator.JDBCTranslator; +import adql.translator.PgSphereTranslator; +import adql.translator.PostgreSQLTranslator; + +public final class DefaultTAPFactory extends AbstractTAPFactory { + + private Class<? extends JDBCTranslator> translator; + + private final String driverPath; + private final String dbUrl; + private final String dbUser; + private final String dbPassword; + + private boolean backupByUser; + private long backupFrequency; + + public DefaultTAPFactory(ServiceConnection service, final Properties tapConfig) throws NullPointerException, TAPException{ + super(service); + + /* 0. 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("JDBC URL missing."); + 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 + "\"!"); + } + } + + /* 1. 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 || sqlTranslator.isEmpty()) + throw new TAPException("No SQL translator specified !"); + + // 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 path) + else if (TAPConfiguration.isClassPath(sqlTranslator)) + translator = TAPConfiguration.fetchClass(sqlTranslator, KEY_SQL_TRANSLATOR, JDBCTranslator.class); + + // case e.) unsupported value + else + throw new TAPException("Unsupported value for the property sql_translator: \"" + sqlTranslator + "\" !"); + + /* 2. Test the construction of the ADQLTranslator */ + createADQLTranslator(); + + /* 3. Store the DB connection parameters */ + this.driverPath = jdbcDriver; + this.dbUrl = dbUrl; + this.dbUser = getProperty(tapConfig, KEY_DB_USERNAME);; + this.dbPassword = getProperty(tapConfig, KEY_DB_PASSWORD); + + /* 4. Test the DB connection */ + DBConnection dbConn = getConnection("0"); + freeConnection(dbConn); + + /* 5. Set the UWS Backup Parameter */ + // BACKUP FREQUENCY: + String propValue = getProperty(tapConfig, KEY_BACKUP_FREQUENCY); + boolean isTime = false; + // determine whether the value is a time period ; if yes, set the frequency: + if (propValue != null){ + try{ + backupFrequency = Long.parseLong(propValue); + if (backupFrequency > 0) + isTime = true; + }catch(NumberFormatException nfe){} + } + // if the value was not a valid numeric time period, try to identify the different textual options: + if (!isTime){ + if (propValue != null && propValue.equalsIgnoreCase(VALUE_USER_ACTION)) + backupFrequency = DefaultTAPBackupManager.AT_USER_ACTION; + else + backupFrequency = DEFAULT_BACKUP_FREQUENCY; + } + // BACKUP BY USER: + 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 DefaultTAPFactory} + * 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 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 tap.TAPFactory#createDBConnection(java.lang.String) + * @see JDBCConnection + */ + @Override + public DBConnection getConnection(String jobID) throws TAPException{ + 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 int countFreeConnections(){ + return 2; // 1 for /sync + 1 for /async + } + + @Override + public void destroy(){ + // Unregister the JDBC driver: + try{ + DriverManager.deregisterDriver(DriverManager.getDriver(dbUrl)); + }catch(SQLException e){ + service.getLogger().warning("Can not deregister the JDBC driver manager!"); + } + + // TODO Nothing else to do! + } + + /** + * 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/TAPConfiguration.java b/src/tap/config/TAPConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..4a7e6e9f16adf3432abfaef4b02b51451760f9b2 --- /dev/null +++ b/src/tap/config/TAPConfiguration.java @@ -0,0 +1,271 @@ +package tap.config; + +import java.io.File; +import java.io.FileInputStream; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Properties; + +import tap.ServiceConnection.LimitUnit; +import tap.TAPException; +import tap.backup.DefaultTAPBackupManager; + +public final class TAPConfiguration { + + /* FILE MANAGER KEYS */ + public final static String KEY_FILE_MANAGER = "file_manager"; + public final static String VALUE_LOCAL = "local"; + public final static String DEFAULT_FILE_MANAGER = VALUE_LOCAL; + public final static String KEY_FILE_ROOT_PATH = "file_root_path"; + public final static String KEY_DIRECTORY_PER_USER = "directory_per_user"; + public final static boolean DEFAULT_DIRECTORY_PER_USER = false; + public final static String KEY_GROUP_USER_DIRECTORIES = "group_user_directories"; + public final static boolean DEFAULT_GROUP_USER_DIRECTORIES = false; + public final static String KEY_DEFAULT_RETENTION_PERIOD = "default_retention_period"; + public final static String KEY_MAX_RETENTION_PERIOD = "max_retention_period"; + public final static int DEFAULT_RETENTION_PERIOD = 0; + + /* UWS BACKUP */ + public final static String KEY_BACKUP_FREQUENCY = "backup_frequency"; + public final static String VALUE_USER_ACTION = "user_action"; + public final static long DEFAULT_BACKUP_FREQUENCY = DefaultTAPBackupManager.MANUAL; // = "never" => no UWS backup manager + public final static String KEY_BACKUP_BY_USER = "backup_by_user"; + public final static boolean DEFAULT_BACKUP_BY_USER = false; + + /* EXECUTION DURATION */ + public final static String KEY_DEFAULT_EXECUTION_DURATION = "default_execution_duration"; + public final static String KEY_MAX_EXECUTION_DURATION = "max_execution_duration"; + public final static int DEFAULT_EXECUTION_DURATION = 0; + + /* DATABASE KEYS */ + public final static String KEY_JDBC_DRIVER = "jdbc_driver"; + public final static HashMap<String,String> VALUE_JDBC_DRIVERS = new HashMap<String,String>(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"); + } + public final static String KEY_SQL_TRANSLATOR = "sql_translator"; + public final static String VALUE_POSTGRESQL = "postgres"; + public final static String VALUE_PGSPHERE = "pgsphere"; + public final static String KEY_JDBC_URL = "jdbc_url"; + public final static String KEY_DB_USERNAME = "db_username"; + public final static String KEY_DB_PASSWORD = "db_password"; + + /* PROVIDER KEYS */ + public final static String KEY_PROVIDER_NAME = "provider_name"; + public final static String KEY_SERVICE_DESCRIPTION = "service_description"; + + /* AVAILABILITY KEYS */ + public final static String KEY_IS_AVAILABLE = "is_available"; + public final static boolean DEFAULT_IS_AVAILABLE = true; + public final static String KEY_DISABILITY_REASON = "disability_reason"; + + /* UPLOAD KEYS */ + public final static String KEY_UPLOAD_ENABLED = "upload_enabled"; + public final static String KEY_DEFAULT_UPLOAD_LIMIT = "upload_default_db_limit"; + public final static String KEY_MAX_UPLOAD_LIMIT = "upload_max_db_limit"; + public final static String KEY_UPLOAD_MAX_FILE_SIZE = "upload_max_file_size"; + public final static int DEFAULT_UPLOAD_MAX_FILE_SIZE = Integer.MAX_VALUE; + + /* OUTPUT KEYS */ + public final static String KEY_OUTPUT_FORMATS = "output_add_formats"; + public final static String VALUE_JSON = "json"; + public final static String VALUE_CSV = "csv"; + public final static String VALUE_TSV = "tsv"; + public final static String VALUE_SV = "sv"; + public final static String KEY_DEFAULT_OUTPUT_LIMIT = "output_default_limit"; + public final static String KEY_MAX_OUTPUT_LIMIT = "output_max_limit"; + + /** + * 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 path. + * Expected syntax: a non-empty string surrounded by brackets ('{' and '}'). + * + * Note: The class path itself is not checked! + * + * @param value Property value. + * + * @return <i>true</i> if the given value is formatted as a class path, <i>false</i> otherwise. + */ + public final static boolean isClassPath(final String value){ + return (value != null && value.length() > 2 && value.charAt(0) == '{' && value.charAt(value.length() - 1) == '}'); + } + + /** + * Fetch the class object corresponding to the classpath provided between brackets in the given value. + * + * @param value Value which is supposed to contain the classpath between brackets (see {@link #isClassPath(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 classpath is incorrect or if its type is not compatible with the parameterized type C (represented by the parameter "expectedType"). + * + * @see {@link #isClassPath(String)} + */ + @SuppressWarnings("unchecked") + public final static < C > Class<? extends C> fetchClass(final String value, final String propertyName, final Class<C> expectedType) throws TAPException{ + if (!isClassPath(value)) + return null; + + String classPath = value.substring(1, value.length() - 1).trim(); + if (classPath.isEmpty()) + return null; + + try{ + Class<? extends C> classObject = (Class<? extends C>)ClassLoader.getSystemClassLoader().loadClass(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() + "."); + } + } + + /** + * <p>Lets parsing a limit (for output, upload, ...) with its numeric value and its unit.</p> + * <p> + * 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. + * </p> + * + * @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("Numeric value 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}; + } + + public final static void main(final String[] args) throws Throwable{ + + FileInputStream configFileStream = null; + try{ + final File configFile = new File("src/ext/tap_min.properties"); + configFileStream = new FileInputStream(configFile); + + Properties config = new Properties(); + config.load(configFileStream); + + configFileStream.close(); + configFileStream = null; + + Enumeration<Object> keys = config.keys(); + String key; + while(keys.hasMoreElements()){ + key = keys.nextElement().toString(); + System.out.println("* " + key + " = " + config.getProperty(key)); + } + }finally{ + if (configFileStream != null) + configFileStream.close(); + } + } + +} diff --git a/src/tap/config/gums_table.txt b/src/tap/config/gums_table.txt new file mode 100644 index 0000000000000000000000000000000000000000..9ec49684286687515fa350dd5fc6ecf65b301c53 --- /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 0000000000000000000000000000000000000000..fafb6bceaffe032bd1f714f9062a7e4d61d572fe --- /dev/null +++ b/src/tap/config/tap_configuration_file.html @@ -0,0 +1,478 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="UTF-8"> + <title>TAP configuration file</title> + <style type="text/css"> + p { text-align: justify; text-indent: 1em; } + table { + font-family:"Trebuchet MS", Arial, Helvetica, sans-serif; + width:100%; + border-collapse:collapse; + } + td, th { + font-size:1em; + border:1px solid #084B8A; + padding:3px 7px 2px 7px; + color: black; + background-color: #EFF5FB; + } + th, td[colspan="5"] { + font-size:1em; + text-align: left; + padding-top:5px; + padding-bottom:4px; + background-color:#045FB4; + color:#ffffff; + } + td[colspan="5"] { + font-style: italic; + } + tr.mandatory td { + color: black; + background-color: #CEE3F6; + } + th:nth-child(2), td:nth-child(2) { + font-weight: bold; + text-align: center; + max-width: 2em; + } + td:nth-child(5) { + font-family: monospace; + } + table ul { list-style-position:inside; padding: 0; margin: 0; } + table p { margin: 0; padding: 0; text-align: left; text-indent: 0; } + table p+p { padding-top: .5em; } + + /* JUST FOR DEVELOPMENT GUIDELINE */ + .done, .mandatory .done { color: green; } + .later, .mandatory .later { color: orange; } + .todo, .mandatory .todo {color: red; } + + </style> + </head> + <body> + <h1>TAP Configuration File</h1> + <p> + 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. + </p> + <p>Besides, you should know that any property key not listed in this table will be ignored without error or warning message.</p> + <p> + 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! + </p> + + <p>Here is an empty minimum TAP configuration file: <a href="tap_min.properties">tap_min.properties</a> and a complete one: <a href="tap_full.properties">tap_full.properties</a>.</p> + + + <p><b>Important note:</b> Any limit value is an integer and so can be at most: 2<sup>31</sup>-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.</p> + + <p><i><u>Legend:</u> <b>M</b> means that the property is mandatory. If nothing is written for the second column, the property is optional.</i> + + <table> + <tr> + <th>Property</th> + <th></th> + <th>Type</th> + <th>Description</th> + <th>Example</th> + </tr> + + <tr><td colspan="5">General</td></tr> + <tr> + <td class="later">service_home_page</td> + <td></td> + <td>text</td> + <td> + <p>Path to the page which will be the index/home page of the TAP Service.</p> + <p><i>A default home page - just listing the TAP resources - is set if none is provided.</i></p> + </td> + <td><ul><li>home.html</li><li>/home/foo/my_tap_homepage.jsp</li></ul></td> + </tr> + + <tr><td colspan="5">Provider</td></tr> + <tr> + <td class="done">provider_name</td> + <td></td> + <td>text</td> + <td>Name of the provider of the TAP Service.</td> + <td><ul><li>ARI</li><li>Mr. Smith</li></ul></td> + </tr> + <tr> + <td class="done">service_description</td> + <td></td> + <td>text</td> + <td>Description of the TAP Service.</td> + <td></td> + </tr> + + <tr><td colspan="5">Availability</td></tr> + <tr> + <td class="done">is_available</td> + <td></td> + <td>boolean</td> + <td> + <p>Tells whether the service is up or down.</p> + <p>If no value is provided, the service will be made available.</p> + <p>A value different from "true" (whatever is the case), will be considered as "false".</p> + </td> + <td><ul><li>true <i>(default)</i></li><li>false</li></ul></td> + </tr> + <tr> + <td class="done">disability_reason</td> + <td></td> + <td>text</td> + <td>Message returns to a client of this TAP Service when it is not available.</p></td> + <td>This TAP Service is temporarily unavailable for DB maintenance.</td> + </tr> + + <tr><td colspan="5">Database</td></tr> + <tr> + <td class="done">jdbc_driver</td> + <td></td> + <td>text</td> + <td> + <p>JDBC driver path. By default, it is guessed in function of the database name provided + in the jdbc_url property. It <strong>MUST be provided if</strong> another DBMS is used or if the JDBC driver path + does not match the following ones:</p> + <ul> + <li><u>Oracle :</u> oracle.jdbc.OracleDriver</li> + <li><u>PostgreSQL:</u> org.postgresql.Driver</li> + <li><u>MySQL :</u> com.mysql.jdbc.Driver</li> + <li><u>SQLite :</u> org.sqlite.JDBC</li> + </ul> + </td> + <td>oracle.jdbc.driver.OracleDriver</td> + </tr> + <tr class="mandatory"> + <td class="done">jdbc_url</td> + <td>M</td> + <td>text</td> + <td> + <p>It must be a JDBC driver URL.</p> + <p><em><u>Note:</u> 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.</em></p> + </td> + <td><ul><li>jdbc:postgresql:mydb</li><li>jdbc:postgresql://myserver:1234/mydb</li><li>jdbc:sqlite:Database.db</li></ul></td> + </tr> + <tr> + <td class="done">db_username</td> + <td></td> + <td>text</td> + <td> + <p><strong>Mandatory if</strong> the username is not already provided in jdbc_url</p> + <p>Username used to access to the database.</p> + </td> + <td></td> + </tr> + <tr> + <td class="done">db_password</td> + <td></td> + <td>text</td> + <td> + <p><strong>Mandatory if</strong> the password is not already provided in jdbc_url</p> + <p>Password used by db_username to access to the database.</p> + <p><em><u>Note:</u> No password encryption can be done in this configuration file for the moment.</em></p> + </td> + <td></td> + </tr> + <tr class="mandatory"> + <td class="later">db_tables</td> + <td>M</td> + <td>text</td> + <td> + <p>List all tables that must be accessed thanks to this TAP Service.</p> + <p>Table names must be separated by a comma. A table name may explicitly specify the schema (if not, the table will be considered as part of the "public" schema).</p> + <p>For each table, you can restrict the list of columns that you to expose via the TAP Service. + This list is not mandatory, but if provided it must be within parenthesis and comma separated.</p> + </td> + <td>schema1.*, schema2.table1, table2, table3(col1, col2, col4, ...)</td> + </tr> + <tr class="mandatory"> + <td class="done">sql_translator</td> + <td>M</td> + <td>text</td> + <td> + <p>The translator to use in order to translate ADQL to a SQL compatible with the used DBMS and its spatial extension.</p> + <p>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 a path to a class (within brackets: {...}) that implements ADQLTranslator and which have at least an empty constructor.</p> + </td> + <td><ul><li>postgres</li><li>pgsphere</li><li>{apackage.MyADQLTranslator}</li></ul></td> + </tr> + + <tr><td colspan="5">Files</td></tr> + <tr class="mandatory"> + <td class="done">file_manager</td> + <td>M</td> + <td>text</td> + <td> + <p>Type of the file manager.</p> + <p>Accepted values are: local (to manage files on the local system). + You can also add another way to manage files by providing the path (within brackets: {...}) to a class implementing TAPFileManager and having at least one constructor with only a java.util.Properties parameter.</p> + </td> + <td><ul><li>local</li><li>{apackage.MyTAPFileManager}</li></ul></td> + </tr> + <tr class="mandatory"> + <td class="done">file_root_path</td> + <td>M</td> + <td>text</td> + <td>File path of the directory in which all TAP files (logs, errors, job results, backup, ...) must be.</td> + <td></td> + </tr> + <tr> + <td class="done">directory_per_user</td> + <td></td> + <td>boolean</td> + <td> + <p>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.</p> + <p><em>The default value is: true.</em></p> + </td> + <td><ul><li>true <i>(default)</i></li><li>false</li></ul></td> + </tr> + <tr> + <td class="done">group_user_directories</td> + <td></td> + <td>boolean</td> + <td> + <p>Tells whether user directories must be grouped. If yes, directories are grouped by the first letter found in the user ID.</p> + <p><em>The default value is: false.</em></p> + </td> + <td><ul><li>true</li><li>false <i>(default)</i></li></ul></td> + </tr> + <tr> + <td class="done">default_retention_period</td> + <td></td> + <td>integer</td> + <td> + <p>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.</p> + <p>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.</p> + <p>A negative or null value means there is no restriction over the default retention period: job results will be kept forever. Float values are not allowed.</p> + <p><em>By default query results are kept forever: default_retention_period=0.</em></p></td> + <td>86400 <em>(1 day)</em></td> + </tr> + <tr> + <td class="done">max_retention_period</td> + <td></td> + <td>integer</td> + <td> + <p>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.</p> + <p>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.</p> + <p>A negative or null value means there is no restriction over the maximum retention period: the job results will be kept forever. Float values are not allowed.</p> + <p><em>By default query results are kept forever: max_retention_period=0.</em></p></td> + <td>604800 <em>(1 week)</em></td> + </tr> + <tr> + <td class="later">irods_host</td> + <td></td> + <td>text</td> + <td> + <p><strong>Mandatory if</strong> file_manager=irods</p> + <p>Host of the IRODS service.</p> + <td></td> + </tr> + <tr> + <td class="later">irods_port</td> + <td></td> + <td>integer</td> + <td> + <p><strong>Mandatory if</strong> file_manager=irods</p> + <p>Port of the IRODS service located on the irodsHost host.</p> + <td></td> + </tr> + <tr> + <td class="later">irods_user</td> + <td></td> + <td>text</td> + <td> + <p><strong>Mandatory if</strong> file_manager=irods</p> + <p>Username to use in order to access the IRODS service located at irodsHost host.</p> + <td></td> + </tr> + <tr> + <td class="later">irods_password</td> + <td></td> + <td>text</td> + <td> + <p><strong>Mandatory if</strong> file_manager=irods</p> + <p>Password associated with the username used to access the IRODS service located at irodsHost host.</p> + <td></td> + </tr> + <tr> + <td class="later">irods_zone</td> + <td></td> + <td>text</td> + <td> + <p><strong>Mandatory if</strong> file_manager=irods</p> + <p>Zone of the IRODS service in which the TAP Service must manage its files.</p> + <td></td> + </tr> + <tr> + <td class="later">irods_default_storage_resource</td> + <td></td> + <td>text</td> + <td> + <p><strong>Mandatory if</strong> file_manager=irods</p> + <p>Default storage resource of the IRODS service.</p> + <td></td> + </tr> + + <tr><td colspan="5">UWS Backup</td></tr> + <tr> + <td class="done">backup_frequency</td> + <td></td> + <td>text or integer</td> + <td> + <p>Frequency at which the UWS service (that's to say, all its users and jobs) must be backuped.</p> + <p>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.</p> + <p><em>By default, no backup is done, so: backup_frequency=never.</em></p> + </td> + <td><ul><li>never <em>(default)</em></li><li>user_action</li><li>3600000 <em>(1 hour)</em></li></ul></td> + </tr> + <tr> + <td class="done">backup_mode</td> + <td></td> + <td>text</td> + <td> + <p>Tells whether the backup must be one file for every user, or one file for each user. This second option should be chosen if your TAP Service is organizing its files by user directories ; see the property <em>directory_per_user</em>.</p> + <p>Allowed values are: user (one backup file for each user), whole (one file for all users ; may generates a big file).</p> + <p><em>The default mode is: whole.</em></p> + </td> + <td><ul><li>whole <em>(default)</em></li><li>user</li></ul></td> + </tr> + + <tr><td colspan="5">Query Execution</td></tr> + <tr> + <td class="done">default_execution_duration</td> + <td></td> + <td>integer</td> + <td> + <p>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.</p> + <p>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.</p> + <p>A negative or null value means there is no restriction over the default execution duration: the execution could never end. Float values are not allowed.</p> + <p><em>By default, there is no restriction: default_execution_duration=0.</em></p> + </td> + <td>600000 <em>(10 minutes)</em></td> + </tr> + <tr> + <td class="done">max_execution_duration</td> + <td></td> + <td>integer</td> + <td> + <p>Maximum time (in milliseconds) for query execution. The prefix "max" means here that the client can not set a time greater than this one.</p> + <p>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.</p> + <p>A negative or null value means there is no restriction over the maximum execution duration: the execution could never end. Float values are not allowed.</p> + <p><em>By default, there is no restriction: max_execution_duration=0.</em></p> + </td> + <td>3600000 <em>(1 hour)</em></td> + </tr> + + <tr><td colspan="5">Output</td></tr> + <tr> + <td class="done">output_add_formats</td> + <td></td> + <td>text</td> + <td> + <p>Comma separated list of output formats for query results, in addition to the VOTable.</p> + <p>Allowed values are: json, csv, tsv, sv(<i><separator></i>)[:<i>mime_type</i>[:<i>mime_type_alias</i>]], or a path (within brackets: {...}) to a class implementing OutputFormat<ResultSet> and having at least one constructor with only a tap.ServiceConnection<ResultSet> parameter.</p> + <p><i>Note: if no MIME type or MIME type alias is provided for the sv(...) format, defaults are: </i>text/plain<i> and </i>text<i>. So </i>sv([])<i> is equivalent to </i>sv([]):text/plain:text<i>.</i></p> + </td> + <td><ul><li>json</li><li>csv</li><li>tsv</li><li>sv(|):text/psv:psv</li><li>sv([])</li><li>{apackage.FooOutputFormat}</li></ul></td> + </tr> + <tr> + <td class="done">output_default_limit</td> + <td></td> + <td>text</td> + <td> + <p>Default limit for the result output. The prefix "default" means here that this value will be set if the client does not provide one.</p> + <p>This limit can be expressed in only one unit: rows.</p> + <p>A negative or null value means there is no restriction over this limit. Float values are not allowed.</p> + <p>Obviously this limit MUST be less or equal than output_max_limit.</p> + <p><em>By default, there is no restriction: output_default_limit=0</em></p> + </td> + <td><ul><li>0 <em>(default)</em></li><li>20</li><li>20r</li><li>20R</li></ul></td> + </tr> + <tr> + <td class="done">output_max_limit</td> + <td></td> + <td>text</td> + <td> + <p>Maximum limit for the result output. The prefix "max" means here that the client can not set a limit greater than this one.</p> + <p>This limit can be expressed in only one unit: rows.</p> + <p>A negative or null value means there is no restriction over this limit. Float values are not allowed.</p> + <p>Obviously this limit MUST be greater or equal than output_default_limit.</p> + <p><em>By default, there is no restriction: output_max_limit=0</em></p> + </td> + <td><ul><li>0 <em>(default)</em></li><li>1000</li><li>10000r</li><li>10000R</li></ul></td> + </tr> + + <tr><td colspan="5">Upload</td></tr> + <tr> + <td class="done">upload_enabled</td> + <td></td> + <td>boolean</td> + <td> + <p>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.</p> + <p><i>By default, the Upload is disabled: upload_enabled=false.</i></p> + </td> + <td><ul><li>false <em>(default)</em></li><li>true</li></ul></td> + </tr> + <tr> + <td class="done">upload_default_db_limit</td> + <td></td> + <td>text</td> + <td> + <p>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.</p> + <p>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.</p> + <p>A negative or null value means there is no restriction over this limit. Float values are not allowed.</p> + <p><b>Warning!</b> 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.</p> + <p><i>By default, there is no restriction: upload_default_db_limit=0</i></p> + </td> + <td><ul><li>0 <em>(default)</em></li><li>20</li><li>20r</li><li>20R</li><li>200kB</li></ul></td> + </tr> + <tr> + <td class="done">upload_max_db_limit</td> + <td></td> + <td>text</td> + <td> + <p>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.</p> + <p>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.</p> + <p>A negative or null value means there is no restriction over this limit. Float values are not allowed.</p> + <p><b>Warning!</b> 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.</p> + <p><i>By default, there is no restriction: upload_max_db_limit=0</i></p> + </td> + <td><ul><li>0 <em>(default)</em></li><li>10000</li><li>10000r</li><li>10000R</li><li>1MB</li></ul></td> + </tr> + <tr> + <td class="done">upload_max_file_size</td> + <td></td> + <td>text</td> + <td> + <p>Maximum allowed size for the uploaded file.</p> + <p>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.</p> + <p><b>Warning!</b> 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.</p> + <p><b>Warning!</b> 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.</p> + <p><i>By default, the maximum size is set to its maximum possible value: upload_max_file_size=2147483647B (~2GB)</i></p> + </td> + <td><ul><li>2147483647B <em>(default)</em></li><li>2MB</li></ul></td> + </tr> + </table> + </body> +</html> \ 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 0000000000000000000000000000000000000000..ad8b63ad79cbfcc4998b5d708735df2d7522c5c0 --- /dev/null +++ b/src/tap/config/tap_full.properties @@ -0,0 +1,255 @@ +########################################################## +# FULL TAP CONFIGURATION FILE # +# # +# TAP Version: 1.1 # +# Date: 20 Dec. 2013 # +# Author: Gregory Mantelet (ARI) # +# # +# See the TAP documentation for more details: ...TODO... # +########################################################## + +########### +# GENERAL # +########### + +# [OPTIONAL] +# Path to the page which will be the index/home page of the TAP Service. +service_home_page = + +############ +# PROVIDER # +############ + +# [OPTIONAL] +# Name of the provider of the TAP Service. +provider_name = ARI + +# [OPTIONAL] +# Description of the TAP Service. +tap_description = My TAP Service is so amazing! You should use it with your favorite TAP client. + +################ +# AVAILABILITY # +################ + +# [OPTIONAL] +# Tells whether the service is up or down. +# If no value is provided, the service will be made available. +# A value different from "true" (whatever is the case), will be considered as "false". +# Allowed values: true (default), false +is_available = true + +# [OPTIONAL] +# Message returns to a client of this TAP Service when it is not available. +disability_reason = + +############ +# DATABASE # +############ + +# [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] +# 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 = + +# [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 = + +# [MANDATORY] +# List all tables that must be accessed thanks to this TAP Service. +# +# Table names must be separated by a comma. A table name may explicitly specify the schema (if not, the table will be considered as part of the "public" schema). +# +# For each table, you can restrict the list of columns that you to expose via the TAP Service. This list is not mandatory, but if provided it must be +# within parenthesis and comma separated. +# +# Example: schema1.*, schema2.table1, table2, table3(col1, col2, col4, ...) +db_tables = + +# [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 a path to 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 path +sql_translator = postgres + +######### +# 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 path (within brackets: {...}) to a class implementing TAPFileManager and having at least one constructor with only a +# java.util.Properties parameter. +# +# Allowed values: local, a class path. +file_manager = local + +# [MANDATORY] +# File path of the directory in which all TAP files (logs, errors, job results, backup, ...) must be. +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 over the default retention period: job results will be kept forever. Float values are not allowed. +# Default value: 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 over the maximum retention period: the job results will be kept forever. Float values are not allowed. +# Default value: 0 (results kept forever). +max_retention_period = 0 + +# [OPTIONAL] +# Mandatory if file_manager=irods +# Host of the IRODS service. +#irods_host = + +# [OPTIONAL] +# Mandatory if file_manager=irods +# Port of the IRODS service located on the irodsHost host. +#irods_port = + +# [OPTIONAL] +# Mandatory if file_manager=irods +# Username to use in order to access the IRODS service located at irodsHost host. +#irods_user = + +# [OPTIONAL] +# Mandatory if file_manager=irods +# Password associated with the username used to access the IRODS service located at irodsHost host. +#irods_password = + +# [OPTIONAL] +# Mandatory if file_manager=irods +# Zone of the IRODS service in which the TAP Service must manage its files. +#irods_zone = + +# [OPTIONAL] +# Mandatory if file_manager=irods +# Default storage resource of the IRODS service. +#irods_default_storage_resource = + +############## +# UWS_BACKUP # +############## + +# [OPTIONAL] +# 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 ; 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. +backup_frequency = never + +# [OPTIONAL] +# Tells whether the backup must be one file for every user, or one file for each user. This second option should be chosen if your TAP Service is organizing its files by user directories ; see the property directory_per_user. +# Allowed values are: user (one backup file for each user ; default), whole (one file for all users ; may generates a big file). +backup_mode = user + +################### +# 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 over the default execution duration: the execution could never end. Float values are not allowed. +# By 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 over the maximum execution duration: the execution could never end. Float values are not allowed. +# By default, there is no restriction: max_execution_duration=0. +max_execution_duration = 0 + +########## +# OUTPUT # +########## + +# [OPTIONAL] +# Comma separated list of output formats for query results, in addition to the VOTable. +# Allowed values are: json, csv, tsv, sv(<separator>), or a path (within brackets: {...}) to a class implementing OutputFormat<ResultSet> and having at least one constructor with only a tap.ServiceConnection<ResultSet> parameter. +output_add_formats = + +# [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 or null value means there is no restriction over this limit. Float values are not allowed. +# Obviously this limit MUST be less or equal than output_max_limit. +# By default, there is no restriction: output_default_limit=0 +output_default_limit = 0 + +# [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 or null value means there is no restriction over this limit. Float values are not allowed. +# Obviously this limit MUST be greater or equal than output_default_limit. +# By default, there is no restriction: output_max_limit=0 +output_max_limit = 0 + +########## +# 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 (default), false. +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 or null value means there is no restriction over this limit. Float values are not allowed. +# Obviously this limit MUST be less or equal than upload_max_db_limit. +# By default, there is no restriction: upload_default_db_limit=0 +upload_default_db_limit = 0 + +# [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 or null value means there is no restriction over this limit. Float values are not allowed. +# Obviously this limit MUST be greater or equal than upload_default_db_limit. +# By default, there is no restriction: upload_max_db_limit=0 +upload_max_db_limit = 0 + +# [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 (except for the last character: "b"). No other storage unit is allowed. +# A negative or null value means there is no restriction over this limit. Float values are not allowed. +# In function of the chosen upload_max_db_limit type, upload_max_file_size should be greater in order to figure out the metadata part. +# By default, there is no restriction: upload_max_file_size=0 +upload_max_file_size = 0 diff --git a/src/tap/config/tap_min.properties b/src/tap/config/tap_min.properties new file mode 100644 index 0000000000000000000000000000000000000000..29de929620a46ca868480b2ad13f1ed2ed4d54a8 --- /dev/null +++ b/src/tap/config/tap_min.properties @@ -0,0 +1,66 @@ +########################################################## +# MINIMUM TAP CONFIGURATION FILE # +# # +# TAP Version: 1.1 # +# Date: 20 Nov. 2013 # +# Author: Gregory Mantelet (ARI) # +# # +# See the TAP documentation for more details: ...TODO... # +########################################################## + +############ +# DATABASE # +############ + +# 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 = + +# 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 = + +# 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 = + +# List all tables that must be accessed thanks to this TAP Service. +# +# Table names must be separated by a comma. A table name may explicitly specify the schema (if not, the table will be considered as part of the "public" schema). +# +# For each table, you can restrict the list of columns that you to expose via the TAP Service. This list is not mandatory, but if provided it must be +# within parenthesis and comma separated. +# +# Example: schema1.*, schema2.table1, table2, table3(col1, col2, col4, ...) +db_tables = + +# 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 a path to 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 path +sql_translator = postgres + +######### +# 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 path (within brackets: {...}) to a class implementing TAPFileManager and having at least one constructor with only a +# java.util.Properties parameter. +# +# Allowed values: local, a class path. +file_manager = local + +# File path of the directory in which all TAP files (logs, errors, job results, backup, ...) must be. +file_root_path = diff --git a/src/tap/db/JDBCConnection.java b/src/tap/db/JDBCConnection.java index 974a649b60f8efe37e69957d314dc16310d060d2..8a69226f308e906e6b4462102a78f1068a6607aa 100644 --- a/src/tap/db/JDBCConnection.java +++ b/src/tap/db/JDBCConnection.java @@ -22,6 +22,7 @@ package tap.db; import java.sql.Connection; import java.sql.DatabaseMetaData; +import java.sql.Driver; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -34,6 +35,7 @@ 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; @@ -119,7 +121,7 @@ import adql.translator.TranslationException; * </p> * * @author Grégory Mantelet (CDS;ARI) - * @version 2.0 (11/2014) + * @version 2.0 (01/2015) * @since 2.0 */ public class JDBCConnection implements DBConnection { @@ -278,7 +280,8 @@ public class JDBCConnection implements DBConnection { } /** - * Create a {@link Connection} instance using the specified JDBC Driver and the given database parameters. + * 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. <i><u>note</u> This URL may not be prefixed by "jdbc:". If not, the prefix will be automatically added.</i> @@ -289,20 +292,39 @@ public class JDBCConnection implements DBConnection { * * @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#getConnection(String, String, String) + * @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{ - // Load the specified JDBC driver: + // Normalize the DB URL: + String url = dbUrl.startsWith(JDBC_PREFIX) ? dbUrl : (JDBC_PREFIX + dbUrl); + + // Select the JDBDC driver: + Driver d; try{ - Class.forName(driverPath); - }catch(ClassNotFoundException cnfe){ - throw new DBException("Impossible to find the JDBC driver \"" + driverPath + "\" !", cnfe); + 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: - String url = dbUrl.startsWith(JDBC_PREFIX) ? dbUrl : (JDBC_PREFIX + dbUrl); try{ - return DriverManager.getConnection(url, dbUser, dbPassword); + Properties p = new Properties(); + p.setProperty("user", dbUser); + p.setProperty("password", dbPassword); + Connection con = d.connect(url, p); + return con; }catch(SQLException se){ throw new DBException("Impossible to establish a connection to the database \"" + url + "\" !", se); } diff --git a/src/tap/formatter/SVFormat.java b/src/tap/formatter/SVFormat.java index 8b38953ebcaa4520f211d482287d1954bfd9f181..ba0879e6b4db4277865411e8a1af977f0d1d9099 100644 --- a/src/tap/formatter/SVFormat.java +++ b/src/tap/formatter/SVFormat.java @@ -35,7 +35,7 @@ import adql.db.DBColumn; * Format any given query (table) result into CSV or TSV (or with custom separator). * * @author Grégory Mantelet (CDS;ARI) - * @version 2.0 (10/2014) + * @version 2.0 (01/2015) */ public class SVFormat implements OutputFormat { @@ -58,6 +58,14 @@ public class SVFormat implements OutputFormat { /** Indicate whether String values must be delimited by double quotes (default) or not. */ protected final boolean delimitStr; + /** 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). * @@ -80,7 +88,24 @@ public class SVFormat implements OutputFormat { * @throws NullPointerException If the given service connection is <code>null</code>. */ public SVFormat(final ServiceConnection service, char colSeparator, boolean delimitStrings) throws NullPointerException{ - this(service, colSeparator, delimitStrings, true); + this(service, colSeparator, delimitStrings, null, null, true); + } + + /** + * Build a SVFormat. + * + * @param service Description of the TAP service. + * @param colSeparator Column separator to use. + * @param delimitStrings <i>true</i> if String values must be delimited by double quotes, <i>false</i> otherwise. + * @param mime The MIME type to associate with this format. <i>note: this MIME type is then used by a user to specify the result format he wants.</i> + * @param shortMime The alias of the MIME type to associate with this format. <i>note: this short MIME type is then used by a user to specify the result format he wants.</i> + * + * @throws NullPointerException If the given service connection is <code>null</code>. + * + * @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, true); } /** @@ -89,15 +114,16 @@ public class SVFormat implements OutputFormat { * @param service Description of the TAP service. * @param colSeparator Column separator to use. * @param delimitStrings <i>true</i> if String values must be delimited by double quotes, <i>false</i> otherwise. + * @param mime The MIME type to associate with this format. <i>note: this MIME type is then used by a user to specify the result format he wants.</i> + * @param shortMime The alias of the MIME type to associate with this format. <i>note: this short MIME type is then used by a user to specify the result format he wants.</i> * @param logFormatReport <i>true</i> to write a log entry (with nb rows and columns + writing duration) each time a result is written, <i>false</i> otherwise. * * @throws NullPointerException If the given service connection is <code>null</code>. + * + * @since 2.0 */ - public SVFormat(final ServiceConnection service, char colSeparator, boolean delimitStrings, final boolean logFormatReport) throws NullPointerException{ - separator = "" + colSeparator; - delimitStr = delimitStrings; - this.service = service; - this.logFormatReport = logFormatReport; + public SVFormat(final ServiceConnection service, char colSeparator, boolean delimitStrings, final String mime, final String shortMime, final boolean logFormatReport) throws NullPointerException{ + this(service, "" + colSeparator, delimitStrings, mime, shortMime, logFormatReport); } /** @@ -122,16 +148,69 @@ public class SVFormat implements OutputFormat { * @throws NullPointerException If the given service connection is <code>null</code>. */ public SVFormat(final ServiceConnection service, String colSeparator, boolean delimitStrings) throws NullPointerException{ + this(service, colSeparator, delimitStrings, null, null, true); + } + + /** + * Build a SVFormat. + * + * @param service Description of the TAP service. + * @param colSeparator Column separator to use. + * @param delimitStrings <i>true</i> if String values must be delimited by double quotes, <i>false</i> otherwise. + * @param mime The MIME type to associate with this format. <i>note: this MIME type is then used by a user to specify the result format he wants.</i> + * @param shortMime The alias of the MIME type to associate with this format. <i>note: this short MIME type is then used by a user to specify the result format he wants.</i> + * + * @throws NullPointerException If the given service connection is <code>null</code>. + * + * @since 2.0 + */ + public SVFormat(final ServiceConnection service, String colSeparator, boolean delimitStrings, final String mime, final String shortMime) throws NullPointerException{ + this(service, colSeparator, delimitStrings, mime, shortMime, true); + } + + /** + * Build a SVFormat. + * + * @param service Description of the TAP service. + * @param colSeparator Column separator to use. + * @param delimitStrings <i>true</i> if String values must be delimited by double quotes, <i>false</i> otherwise. + * @param mime The MIME type to associate with this format. <i>note: this MIME type is then used by a user to specify the result format he wants.</i> + * @param shortMime The alias of the MIME type to associate with this format. <i>note: this short MIME type is then used by a user to specify the result format he wants.</i> + * @param logFormatReport <i>true</i> to write a log entry (with nb rows and columns + writing duration) each time a result is written, <i>false</i> otherwise. + * + * @throws NullPointerException If the given service connection is <code>null</code>. + * + * @since 2.0 + */ + public SVFormat(final ServiceConnection service, String colSeparator, boolean delimitStrings, final String mime, final String shortMime, final boolean logFormatReport) throws NullPointerException{ if (service == null) throw new NullPointerException("The given service connection is NULL!"); - separator = (colSeparator == null) ? ("" + COMMA_SEPARATOR) : colSeparator; + 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; + this.logFormatReport = logFormatReport; } - @Override - public String getMimeType(){ + /** + * <p>Try to guess the MIME type to associate with this SV format, in function of the column separator.</p> + * + * <p> + * By default, only "," or ";" (text/csv) and [TAB] (text/tab-separated-values) are supported. + * If the separator is unknown, "text/plain" will be returned. + * </p> + * + * <p><i>Note: In order to automatically guess more MIME types, you should overwrite this function.</i></p> + * + * @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: @@ -143,8 +222,23 @@ public class SVFormat implements OutputFormat { } } - @Override - public String getShortMimeType(){ + /** + * <p>Try to guess the short MIME type to associate with this SV format, in function of the column separator.</p> + * + * <p> + * By default, only "," or ";" (csv) and [TAB] (tsv) are supported. + * If the separator is unknown, "text" will be returned. + * </p> + * + * <p><i>Note: In order to automatically guess more short MIME types, you should overwrite this function.</i></p> + * + * @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: @@ -156,6 +250,16 @@ public class SVFormat implements OutputFormat { } } + @Override + public final String getMimeType(){ + return mimeType; + } + + @Override + public final String getShortMimeType(){ + return shortMimeType; + } + @Override public String getDescription(){ return null; diff --git a/src/tap/resource/TAP.java b/src/tap/resource/TAP.java index eaefb96553338e5853f50adef7258ed450abe94d..6bfc38e625ad900ddcbc0ca06c657d19cf8d6db6 100644 --- a/src/tap/resource/TAP.java +++ b/src/tap/resource/TAP.java @@ -537,10 +537,50 @@ public class TAP implements VOSIResource { if (uploadLimit != null && uploadLimit.length >= 2 && uploadLimitType != null && uploadLimitType.length >= 2){ if (uploadLimit[0] > -1 || uploadLimit[1] > -1){ xml.append("\t<uploadLimit>\n"); - if (uploadLimit[0] > -1) - xml.append("\t\t<default ").append(VOSerializer.formatAttribute("unit", uploadLimitType[0].toString())).append(">").append(uploadLimit[0]).append("</default>\n"); - if (uploadLimit[1] > -1) - xml.append("\t\t<hard ").append(VOSerializer.formatAttribute("unit", uploadLimitType[1].toString())).append(">").append(uploadLimit[1]).append("</hard>\n"); + if (uploadLimit[0] > -1){ + String limitType; + long limit = uploadLimit[0]; + switch(uploadLimitType[0]){ + case kilobytes: + limit *= 1000l; + limitType = LimitUnit.rows.toString(); + break; + case megabytes: + limit *= 1000000l; + limitType = LimitUnit.rows.toString(); + break; + case gigabytes: + limit *= 1000000000l; + limitType = LimitUnit.rows.toString(); + break; + default: + limitType = uploadLimitType[0].toString(); + break; + } + xml.append("\t\t<default ").append(VOSerializer.formatAttribute("unit", limitType)).append("\">").append(limit).append("</default>\n"); + } + if (uploadLimit[1] > -1){ + String limitType; + long limit = uploadLimit[1]; + switch(uploadLimitType[1]){ + case kilobytes: + limit *= 1000l; + limitType = LimitUnit.rows.toString(); + break; + case megabytes: + limit *= 1000000l; + limitType = LimitUnit.rows.toString(); + break; + case gigabytes: + limit *= 1000000000l; + limitType = LimitUnit.rows.toString(); + break; + default: + limitType = uploadLimitType[1].toString(); + break; + } + xml.append("\t\t<hard ").append(VOSerializer.formatAttribute("unit", limitType)).append("\">").append(limit).append("</hard>\n"); + } xml.append("\t</uploadLimit>\n"); } } diff --git a/src/tap/upload/Uploader.java b/src/tap/upload/Uploader.java index d1e8643ff22935bd63ed1fad184fdebcdb7a9b19..0d5deb704c7d6a4a088fbd6a95124a971af0d460 100644 --- a/src/tap/upload/Uploader.java +++ b/src/tap/upload/Uploader.java @@ -51,7 +51,7 @@ import com.oreilly.servlet.multipart.ExceededSizeException; * </p> * * @author Grégory Mantelet (CDS;ARI) - * @version 2.0 (11/2014) + * @version 2.0 (01/2015) * * @see LimitedTableIterator * @see VOTableIterator @@ -120,8 +120,24 @@ public class Uploader { if (this.service.uploadEnabled()){ // ...and set the rows or bytes limit: if (this.service.getUploadLimitType()[1] != null && this.service.getUploadLimit()[1] > 0){ - limit = this.service.getUploadLimit()[1]; - limitUnit = this.service.getUploadLimitType()[1]; + switch(service.getUploadLimitType()[1]){ + case kilobytes: + limit = (int)(1000l * this.service.getUploadLimit()[1]); + limitUnit = LimitUnit.bytes; + break; + case megabytes: + limit = (int)(1000000l * this.service.getUploadLimit()[1]); + limitUnit = LimitUnit.bytes; + break; + case gigabytes: + limit = (int)(1000000000l * this.service.getUploadLimit()[1]); + limitUnit = LimitUnit.bytes; + break; + default: + limit = this.service.getUploadLimit()[1]; + limitUnit = this.service.getUploadLimitType()[1]; + break; + } }else{ limit = -1; limitUnit = null; diff --git a/test/tap/config/AllTests.java b/test/tap/config/AllTests.java new file mode 100644 index 0000000000000000000000000000000000000000..e3a5b989e71ca3cd2d62f0101bcd353e3b99daa8 --- /dev/null +++ b/test/tap/config/AllTests.java @@ -0,0 +1,28 @@ +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,TestDefaultServiceConnection.class,TestDefaultTAPFactory.class,TestMaxRecController.class}) +public class AllTests { + + public final static Properties getValidProperties(){ + Properties validProp = new Properties(); + 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("db_tables", ""); + validProp.setProperty("sql_translator", "postgres"); + validProp.setProperty("file_manager", "local"); + validProp.setProperty("file_root_path", "bin/ext/test/tap"); + return validProp; + } + +} diff --git a/test/tap/config/TestDefaultServiceConnection.java b/test/tap/config/TestDefaultServiceConnection.java new file mode 100644 index 0000000000000000000000000000000000000000..03465f7bd2857cd917e27972c0afb7f0c7a2960d --- /dev/null +++ b/test/tap/config/TestDefaultServiceConnection.java @@ -0,0 +1,261 @@ +package tap.config; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +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.KEY_OUTPUT_FORMATS; +import static tap.config.TAPConfiguration.VALUE_CSV; +import static tap.config.TAPConfiguration.VALUE_JSON; +import static tap.config.TAPConfiguration.VALUE_LOCAL; +import static tap.config.TAPConfiguration.VALUE_SV; +import static tap.config.TAPConfiguration.VALUE_TSV; + +import java.io.File; +import java.util.Properties; + +import org.junit.Before; +import org.junit.Test; + +import tap.ServiceConnection; +import tap.ServiceConnection.LimitUnit; +import tap.TAPException; +import uws.UWSException; +import uws.service.file.LocalUWSFileManager; + +public class TestDefaultServiceConnection { + + private Properties validProp, noFmProp, fmClassPathProp, incorrectFmProp, + validFormatsProp, badSVFormat1Prop, badSVFormat2Prop, + unknownFormatProp, defaultOutputLimitProp, maxOutputLimitProp, + bothOutputLimitGoodProp, bothOutputLimitBadProp; + + @Before + public void setUp() throws Exception{ + // LOAD ALL PROPERTIES FILES NEEDED FOR ALL THE TESTS: + validProp = AllTests.getValidProperties(); + + noFmProp = (Properties)validProp.clone(); + noFmProp.setProperty(KEY_FILE_MANAGER, ""); + + fmClassPathProp = (Properties)validProp.clone(); + fmClassPathProp.setProperty(KEY_FILE_MANAGER, "{tap.config.TestDefaultServiceConnection$FileManagerTest}"); + + incorrectFmProp = (Properties)validProp.clone(); + incorrectFmProp.setProperty(KEY_FILE_MANAGER, "foo"); + + validFormatsProp = (Properties)validProp.clone(); + validFormatsProp.setProperty(KEY_OUTPUT_FORMATS, VALUE_JSON + "," + VALUE_CSV + " , " + VALUE_TSV + ",, , " + VALUE_SV + "([])" + ", " + VALUE_SV + "(|):text/psv:psv" + ", " + VALUE_SV + "($)::test" + ", \t " + VALUE_SV + "(@):text/arobase:"); + + badSVFormat1Prop = (Properties)validProp.clone(); + badSVFormat1Prop.setProperty(KEY_OUTPUT_FORMATS, VALUE_SV); + + badSVFormat2Prop = (Properties)validProp.clone(); + badSVFormat2Prop.setProperty(KEY_OUTPUT_FORMATS, VALUE_SV + "()"); + + unknownFormatProp = (Properties)validProp.clone(); + unknownFormatProp.setProperty(KEY_OUTPUT_FORMATS, "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"); + } + + /** + * 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 classpath 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 TestDefaultTAPFactory}. + * + * @see DefaultServiceConnection#DefaultServiceConnection(Properties) + */ + @Test + public void testDefaultServiceConnectionProperties(){ + // Valid Configuration File: + try{ + ServiceConnection connection = new DefaultServiceConnection(validProp); + assertNotNull(connection.getLogger()); + assertNotNull(connection.getFileManager()); + assertNotNull(connection.getFactory()); + assertTrue(connection.isAvailable()); + assertTrue(connection.getRetentionPeriod()[0] <= connection.getRetentionPeriod()[1]); + assertTrue(connection.getExecutionDuration()[0] <= connection.getExecutionDuration()[1]); + }catch(Exception e){ + fail("This MUST have succeeded because the property file is valid! \nCaught exception: " + getPertinentMessage(e)); + } + + // No File Manager: + try{ + new DefaultServiceConnection(noFmProp); + fail("This MUST have failed because no File Manager is specified!"); + }catch(Exception e){ + assertEquals(e.getClass(), TAPException.class); + assertEquals(e.getMessage(), "The property \"" + KEY_FILE_MANAGER + "\" is missing! It is required to create a TAP Service. Two possible values: " + VALUE_LOCAL + " or a class path between {...}."); + } + + // File Manager = Class Path: + try{ + ServiceConnection connection = new DefaultServiceConnection(fmClassPathProp); + assertNotNull(connection.getLogger()); + assertNotNull(connection.getFileManager()); + assertNotNull(connection.getFactory()); + assertTrue(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 path valid! \nCaught exception: " + getPertinentMessage(e)); + } + + // Incorrect File Manager Value: + try{ + new DefaultServiceConnection(incorrectFmProp); + fail("This MUST have failed because an incorrect File Manager value has been provided!"); + }catch(Exception e){ + assertEquals(e.getClass(), TAPException.class); + assertEquals(e.getMessage(), "Unknown value for the property \"" + KEY_FILE_MANAGER + "\": \"foo\". Only two possible values: " + VALUE_LOCAL + " or a class path between {...}."); + } + + // Valid output formats list: + try{ + ServiceConnection connection = new DefaultServiceConnection(validFormatsProp); + 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)); + } + + // Bad SV(...) format 1 = "sv": + try{ + new DefaultServiceConnection(badSVFormat1Prop); + fail("This MUST have failed because an incorrect SV output format value has been provided!"); + }catch(Exception e){ + assertEquals(e.getClass(), TAPException.class); + assertEquals(e.getMessage(), "Missing separator char/string for the SV output format: \"sv\"!"); + } + + // Bad SV(...) format 2 = "sv()": + try{ + new DefaultServiceConnection(badSVFormat2Prop); + fail("This MUST have failed because an incorrect SV output format value has been provided!"); + }catch(Exception e){ + assertEquals(e.getClass(), TAPException.class); + assertEquals(e.getMessage(), "Missing separator char/string for the SV output format: \"sv()\"!"); + } + + // Unknown output format: + try{ + new DefaultServiceConnection(unknownFormatProp); + fail("This MUST have failed because an incorrect output format value has been provided!"); + }catch(Exception e){ + assertEquals(e.getClass(), TAPException.class); + assertEquals(e.getMessage(), "Unknown output format: foo"); + } + + // Test with no output limit specified: + try{ + ServiceConnection connection = new DefaultServiceConnection(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 DefaultServiceConnection(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 DefaultServiceConnection(maxOutputLimitProp); + assertEquals(connection.getOutputLimit()[0], -1); + 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 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 DefaultServiceConnection(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: + try{ + new DefaultServiceConnection(bothOutputLimitBadProp); + fail("This MUST have failed because the default output limit is greater than the maximum one!"); + }catch(Exception e){ + assertEquals(e.getClass(), TAPException.class); + assertEquals(e.getMessage(), "The default output limit (here: 1000) MUST be less or equal to the maximum output limit (here: 100)!"); + } + } + + 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 TestDefaultServiceConnection#testDefaultServiceConnectionProperties() + */ + public static class FileManagerTest extends LocalUWSFileManager { + public FileManagerTest(Properties tapConfig) throws UWSException{ + super(new File(tapConfig.getProperty("file_root_path")), true, false); + } + } + +} diff --git a/test/tap/config/TestDefaultTAPFactory.java b/test/tap/config/TestDefaultTAPFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..895d55821f087bd8981c9d758093f50ee40e504a --- /dev/null +++ b/test/tap/config/TestDefaultTAPFactory.java @@ -0,0 +1,284 @@ +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_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 java.io.File; +import java.util.Collection; +import java.util.Iterator; +import java.util.Properties; + +import org.junit.Before; +import org.junit.Test; +import org.postgresql.util.PSQLException; + +import tap.ServiceConnection; +import tap.TAPException; +import tap.TAPFactory; +import tap.db.DBException; +import tap.formatter.OutputFormat; +import tap.log.DefaultTAPLog; +import tap.log.TAPLog; +import tap.metadata.TAPMetadata; +import uws.service.UWSService; +import uws.service.UserIdentifier; +import uws.service.file.LocalUWSFileManager; +import uws.service.file.UWSFileManager; +import adql.db.FunctionDef; + +public class TestDefaultTAPFactory { + + private Properties validProp, noJdbcProp1, noJdbcProp2, badJdbcProp, + badTranslatorProp, badDBNameProp, badUsernameProp, badPasswordProp; + + private ServiceConnection serviceConnection = null; + + @Before + public void setUp() throws Exception{ + // BUILD A FAKE SERVICE CONNECTION: + serviceConnection = new ServiceConnectionTest(); + + // LOAD ALL PROPERTIES FILES NEEDED FOR ALL THE TESTS: + validProp = AllTests.getValidProperties(); + + noJdbcProp1 = (Properties)validProp.clone(); + noJdbcProp1.remove(KEY_JDBC_DRIVER); + + noJdbcProp2 = (Properties)noJdbcProp1.clone(); + noJdbcProp2.setProperty(KEY_JDBC_URL, "jdbc:foo:gmantele"); + + badJdbcProp = (Properties)validProp.clone(); + badJdbcProp.setProperty(KEY_JDBC_DRIVER, "foo"); + badJdbcProp.setProperty(KEY_JDBC_URL, "jdbc:foo:gmantele"); + + badTranslatorProp = (Properties)validProp.clone(); + badTranslatorProp.setProperty(KEY_SQL_TRANSLATOR, "foo"); + + badDBNameProp = (Properties)validProp.clone(); + badDBNameProp.setProperty(KEY_JDBC_URL, "jdbc:postgresql:foo"); + + badUsernameProp = (Properties)validProp.clone(); + badUsernameProp.setProperty(KEY_DB_USERNAME, "foo"); + + badPasswordProp = (Properties)validProp.clone(); + badPasswordProp.setProperty(KEY_DB_PASSWORD, "foo"); + } + + @Test + public void testDefaultServiceConnection(){ + // Correct Parameters: + try{ + TAPFactory factory = new DefaultTAPFactory(serviceConnection, validProp); + assertNotNull(factory.getConnection("0")); + assertNull(factory.createUWSBackupManager(new UWSService(factory, new LocalUWSFileManager(new File("."))))); + }catch(Exception ex){ + fail(getPertinentMessage(ex)); + } + + // No JDBC Driver but the database type is known: + try{ + new DefaultTAPFactory(serviceConnection, noJdbcProp1); + }catch(Exception ex){ + fail(getPertinentMessage(ex)); + } + + // No JDBC Driver but the database type is UNKNOWN: + try{ + new DefaultTAPFactory(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 \"[^\\\"]*\"!")); + } + + // Bad JDBC Driver: + try{ + new DefaultTAPFactory(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 \"[^\\\"]*\" !")); + } + + // Bad Translator: + try{ + new DefaultTAPFactory(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 DefaultTAPFactory(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 DefaultTAPFactory(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 DefaultTAPFactory(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 \"[^\\\"]*\"")); + } + } + + 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<String> getCoordinateSystems(){ + return null; + } + + @Override + public TAPLog getLogger(){ + return logger; + } + + @Override + public TAPFactory getFactory(){ + return null; + } + + @Override + public UWSFileManager getFileManager(){ + return null; + } + + @Override + public Iterator<OutputFormat> 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<String> getGeometries(){ + return null; + } + + @Override + public Collection<FunctionDef> getUDFs(){ + return null; + } + + @Override + public int getNbMaxAsyncJobs(){ + return -1; + } + } + +} diff --git a/test/tap/config/TestTAPConfiguration.java b/test/tap/config/TestTAPConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..44a20d5faa3da2b5e59c23e188fdc45d5742f9f2 --- /dev/null +++ b/test/tap/config/TestTAPConfiguration.java @@ -0,0 +1,288 @@ +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.isClassPath; +import static tap.config.TAPConfiguration.parseLimit; + +import org.junit.Before; +import org.junit.Test; + +import tap.ServiceConnection.LimitUnit; +import tap.TAPException; + +public class TestTAPConfiguration { + + @Before + public void setUp() throws Exception{} + + /** + * TEST isClassPath(String): + * - null, "", "{}", "an incorrect syntax" => FALSE must be returned + * - "{ }", "{ }", "{class.path}", "{ class.path }" => TRUE must be returned + * + * @see DefaultServiceConnection#isClassPath(String) + */ + @Test + public void testIsClassPath(){ + // NULL and EMPTY: + assertFalse(isClassPath(null)); + assertFalse(isClassPath("")); + + // EMPTY CLASSPATH: + assertFalse(isClassPath("{}")); + + // INCORRECT CLASSPATH: + assertFalse(isClassPath("incorrect class path ; missing {}")); + + // VALID CLASSPATH: + assertTrue(isClassPath("{class.path}")); + + // CLASSPATH VALID ONLY IN THE SYNTAX: + assertTrue(isClassPath("{ }")); + assertTrue(isClassPath("{ }")); + + // NOT TRIM CLASSPATH: + assertTrue(isClassPath("{ class.path }")); + } + + /** + * 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 classpath: 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 classpath: getClass(...) MUST return null!\nCaught exception: " + getPertinentMessage(e)); + } + + // EMPTY CLASSPATH: + try{ + assertNull(fetchClass("{}", KEY_FILE_MANAGER, String.class)); + }catch(TAPException e){ + fail("If an EMPTY classpath is provided: getClass(...) MUST return null!\nCaught exception: " + getPertinentMessage(e)); + } + + // INCORRECT SYNTAX: + try{ + assertNull(fetchClass("incorrect class path ; missing {}", KEY_FILE_MANAGER, String.class)); + }catch(TAPException e){ + fail("If an incorrect classpath is provided: getClass(...) MUST return null!\nCaught exception: " + getPertinentMessage(e)); + } + + // VALID CLASSPATH: + try{ + Class<? extends String> 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 classpath is provided: getClass(...) MUST return a Class object of the wanted type!\nCaught exception: " + getPertinentMessage(e)); + } + + // INCORRECT CLASSPATH: + try{ + fetchClass("{mypackage.foo}", KEY_FILE_MANAGER, String.class); + fail("This MUST have failed because an incorrect classpath 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<? extends String> 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() + "."); + } + + // CLASSPATH VALID ONLY IN THE SYNTAX: + try{ + assertNull(fetchClass("{ }", KEY_FILE_MANAGER, String.class)); + }catch(TAPException e){ + fail("If an EMPTY classpath 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 classpath is provided: getClass(...) MUST return null!\nCaught exception: " + getPertinentMessage(e)); + } + + // NOT TRIM CLASSPATH: + 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 classpath is provided: getClass(...) MUST return a Class object of the wanted type!\nCaught exception: " + getPertinentMessage(e)); + } + } + + /** + * 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,""," ","0","-123"}; + Object[] limit; + for(String v : testValues){ + limit = parseLimit(v, propertyName, false); + assertEquals(limit[0], -1); + 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(), "Numeric value 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(); + } + +} diff --git a/test/tap/metadata/MetadataExtractionTest.java b/test/tap/metadata/MetadataExtractionTest.java new file mode 100644 index 0000000000000000000000000000000000000000..bc35a1270d951148469eb0ded360de4205e5478f --- /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 <http://www.gnu.org/licenses/>. + * + * 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 !"); + } + } + +}