From d708278c84121f2f9b0bf7139af72fca612a4f67 Mon Sep 17 00:00:00 2001 From: gmantele <gmantele@ari.uni-heidelberg.de> Date: Thu, 22 Oct 2015 12:48:58 +0200 Subject: [PATCH] [TAP] Add the possibility to wrap a TAPMetadata instance created automatically by the TAP configuration file in order to add/remove/change some metadata or to change the output of the TAP resource '/tables'. --- .../config/ConfigurableServiceConnection.java | 54 +++++++- src/tap/config/tap_configuration_file.html | 8 +- src/tap/config/tap_full.properties | 11 +- src/tap/config/tap_min.properties | 11 +- src/tap/metadata/TAPMetadata.java | 12 +- .../TestConfigurableServiceConnection.java | 118 ++++++++++++++---- 6 files changed, 175 insertions(+), 39 deletions(-) diff --git a/src/tap/config/ConfigurableServiceConnection.java b/src/tap/config/ConfigurableServiceConnection.java index 058740a..9ed6d13 100644 --- a/src/tap/config/ConfigurableServiceConnection.java +++ b/src/tap/config/ConfigurableServiceConnection.java @@ -124,7 +124,7 @@ import adql.query.operand.function.UserDefinedFunction; * </p> * * @author Grégory Mantelet (ARI) - * @version 2.0 (04/2015) + * @version 2.1 (10/2015) * @since 2.0 */ public final class ConfigurableServiceConnection implements ServiceConnection { @@ -435,7 +435,19 @@ public final class ConfigurableServiceConnection implements ServiceConnection { // Get the fetching method to use: String metaFetchType = getProperty(tapConfig, KEY_METADATA); if (metaFetchType == null) - throw new TAPException("The property \"" + KEY_METADATA + "\" is missing! It is required to create a TAP Service. Three possible values: " + VALUE_XML + " (to get metadata from a TableSet XML document), " + VALUE_DB + " (to fetch metadata from the database schema TAP_SCHEMA) or the name (between {}) of a class extending TAPMetadata."); + throw new TAPException("The property \"" + KEY_METADATA + "\" is missing! It is required to create a TAP Service. Three possible values: " + VALUE_XML + " (to get metadata from a TableSet XML document), " + VALUE_DB + " (to fetch metadata from the database schema TAP_SCHEMA) or the name (between {}) of a class extending TAPMetadata. Only " + VALUE_XML + " and " + VALUE_DB + " can be followed by the path of a class extending TAPMetadata."); + + // Extract a custom class suffix if any for XML and DB options: + String customMetaClass = null; + if (metaFetchType.toLowerCase().matches("(" + VALUE_XML + "|" + VALUE_DB + ").*")){ + int indSep = metaFetchType.toLowerCase().startsWith(VALUE_XML) ? 3 : 2; + customMetaClass = metaFetchType.substring(indSep).trim(); + metaFetchType = metaFetchType.substring(0, indSep); + if (customMetaClass.length() == 0) + customMetaClass = null; + else if (!isClassName(customMetaClass)) + throw new TAPException("Unexpected string after the fetching method \"" + metaFetchType + "\": \"" + customMetaClass + "\"! The full name of a class extending TAPMetadata was expected. If it is a class name, then it must be specified between {}."); + } TAPMetadata metadata = null; @@ -522,7 +534,43 @@ public final class ConfigurableServiceConnection implements ServiceConnection { } // INCORRECT VALUE => ERROR! else - throw new TAPException("Unsupported value for the property \"" + KEY_METADATA + "\": \"" + metaFetchType + "\"! Only two values are allowed: " + VALUE_XML + " (to get metadata from a TableSet XML document) or " + VALUE_DB + " (to fetch metadata from the database schema TAP_SCHEMA)."); + throw new TAPException("Unsupported value for the property \"" + KEY_METADATA + "\": \"" + metaFetchType + "\"! Only two values are allowed: " + VALUE_XML + " (to get metadata from a TableSet XML document) or " + VALUE_DB + " (to fetch metadata from the database schema TAP_SCHEMA). Only " + VALUE_XML + " and " + VALUE_DB + " can be followed by the path of a class extending TAPMetadata."); + + // Create the custom TAPMetadata extension if any is provided (THEORETICALLY, JUST FOR XML and DB): + if (customMetaClass != null){ + // get the class: + Class<? extends TAPMetadata> metaClass = fetchClass(customMetaClass, KEY_METADATA, TAPMetadata.class); + if (metaClass == TAPMetadata.class) + throw new TAPException("Wrong class for the property \"" + KEY_METADATA + "\": \"" + metaClass.getName() + "\"! The class provided in this property MUST EXTEND tap.metadata.TAPMetadata."); + try{ + // get one of the expected constructors: + try{ + // (TAPMetadata, UWSFileManager, TAPFactory, TAPLog): + Constructor<? extends TAPMetadata> constructor = metaClass.getConstructor(TAPMetadata.class, UWSFileManager.class, TAPFactory.class, TAPLog.class); + // create the TAP metadata: + metadata = constructor.newInstance(metadata, fileManager, tapFactory, logger); + }catch(NoSuchMethodException nsme){ + // (TAPMetadata): + Constructor<? extends TAPMetadata> constructor = metaClass.getConstructor(TAPMetadata.class); + // create the TAP metadata: + metadata = constructor.newInstance(metadata); + } + }catch(NoSuchMethodException nsme){ + throw new TAPException("Missing constructor by copy tap.metadata.TAPMetadata(tap.metadata.TAPMetadata) or tap.metadata.TAPMetadata(tap.metadata.TAPMetadata, uws.service.file.UWSFileManager, tap.TAPFactory, tap.log.TAPLog)! See the value \"" + metaFetchType + "\" of the property \"" + KEY_METADATA + "\"."); + }catch(InstantiationException ie){ + throw new TAPException("Impossible to create an instance of an abstract class: \"" + metaClass.getName() + "\"! See the value \"" + metaFetchType + "\" of the property \"" + KEY_METADATA + "\"."); + }catch(InvocationTargetException ite){ + if (ite.getCause() != null){ + if (ite.getCause() instanceof TAPException) + throw (TAPException)ite.getCause(); + else + throw new TAPException(ite.getCause()); + }else + throw new TAPException(ite); + }catch(Exception ex){ + throw new TAPException("Impossible to create an instance of tap.metadata.TAPMetadata as specified in the property \"" + KEY_METADATA + "\": \"" + metaFetchType + "\"!", ex); + } + } return metadata; } diff --git a/src/tap/config/tap_configuration_file.html b/src/tap/config/tap_configuration_file.html index 0bebdc9..13e45ff 100644 --- a/src/tap/config/tap_configuration_file.html +++ b/src/tap/config/tap_configuration_file.html @@ -280,8 +280,14 @@ <li>Build yourself the metadata of your service by creating an extension of tap.metadata.TAPMetadata. This extension must have either an empty constructor or a constructor with exactly 3 parameters of type UWSFileManager, TAPFactory and TAPLog ; if both constructor are provided, only the one with parameters will be used.</li> </ol> + <p> + For the two first methods, it is also possible to specify an extension of tap.metadata.TAPMetadata which will wrap a default TAPMetadata objects created using the specified + methods (i.e. XML tableset or TAP_SCHEMA). In this way, it is possible to get the "default" metadata from an XML file or the database + and then add/remove/modify some of them, or to change the output of the 'tables' resource. The extension of tap.metadata.TAPMetadata must have at least + one constructor with the following parameters: (TAPMetadata) or (TAPMetadata, UWSFileManager, TAPFactory, TAPLog). + </p> </td> - <td><ul><li>xml</li><li>db</li><li>{apackage.MyTAPMetadata}</li></ul> + <td><ul><li>xml</li><li>xml {myTAPMetadata}</li><li>db</li><li>db {myTAPMetadata}</li><li>{apackage.MyTAPMetadata}</li></ul> </tr> <tr class="optional"> <td class="done">metadata_file</td> diff --git a/src/tap/config/tap_full.properties b/src/tap/config/tap_full.properties index b223d59..78c1bb7 100644 --- a/src/tap/config/tap_full.properties +++ b/src/tap/config/tap_full.properties @@ -1,8 +1,8 @@ ########################################################## # FULL TAP CONFIGURATION FILE # # # -# TAP Version: 2.0 # -# Date: 13 April 2015 # +# TAP Version: 2.1 # +# Date: 22 Oct. 2015 # # Author: Gregory Mantelet (ARI) # # # ########################################################## @@ -151,8 +151,13 @@ db_password = # 2/ Get all metadata from the database schema TAP_SCHEMA. # 3/ Build yourself the metadata of your service by creating an extension of tap.metadata.TAPMetadata. This extension must have either an empty constructor # or a constructor with exactly 3 parameters of type UWSFileManager, TAPFactory and TAPLog ; if both constructor are provided, only the one with parameters will be used. +# +# For the two first methods, it is also possible to specify an extension of tap.metadata.TAPMetadata which will wrap a default TAPMetadata objects created using the specified +# methods (i.e. XML tableset or TAP_SCHEMA). In this way, it is possible to get the "default" metadata from an XML file or the database +# and then add/remove/modify some of them, or to change the output of the 'tables' resource. The extension of tap.metadata.TAPMetadata must have at least +# one constructor with the following parameters: (TAPMetadata) or (TAPMetadata, UWSFileManager, TAPFactory, TAPLog). # -# Allowed values: xml, db or a full class name (between {}). +# Allowed values: xml, xml {myTAPMetadata}, db, db {myTAPMetadata} or a full class name (between {}). metadata = # [MANDATORY] diff --git a/src/tap/config/tap_min.properties b/src/tap/config/tap_min.properties index 2bfd5af..fd07970 100644 --- a/src/tap/config/tap_min.properties +++ b/src/tap/config/tap_min.properties @@ -1,8 +1,8 @@ ########################################################## # MINIMUM TAP CONFIGURATION FILE # # # -# TAP Version: 2.0 # -# Date: 27 Feb. 2015 # +# TAP Version: 2.1 # +# Date: 22 Oct. 2015 # # Author: Gregory Mantelet (ARI) # # # ########################################################## @@ -79,8 +79,13 @@ db_password = # 2/ Get all metadata from the database schema TAP_SCHEMA. # 3/ Build yourself the metadata of your service by creating an extension of tap.metadata.TAPMetadata. This extension must have either an empty constructor # or a constructor with exactly 3 parameters of type UWSFileManager, TAPFactory and TAPLog ; if both constructor are provided, only the one with parameters will be used. +# +# For the two first methods, it is also possible to specify an extension of tap.metadata.TAPMetadata which will wrap a default TAPMetadata objects created using the specified +# methods (i.e. XML tableset or TAP_SCHEMA). In this way, it is possible to get the "default" metadata from an XML file or the database +# and then add/remove/modify some of them, or to change the output of the 'tables' resource. The extension of tap.metadata.TAPMetadata must have at least +# one constructor with the following parameters: (TAPMetadata) or (TAPMetadata, UWSFileManager, TAPFactory, TAPLog). # -# Allowed values: xml, db or a full class name (between {}). +# Allowed values: xml, xml {myTAPMetadata}, db, db {myTAPMetadata} or a full class name (between {}). metadata = # Mandatory if the value of "metadata" is "xml". diff --git a/src/tap/metadata/TAPMetadata.java b/src/tap/metadata/TAPMetadata.java index 580b966..8173e3f 100644 --- a/src/tap/metadata/TAPMetadata.java +++ b/src/tap/metadata/TAPMetadata.java @@ -64,7 +64,7 @@ import adql.db.DBType.DBDatatype; * </p> * * @author Grégory Mantelet (CDS;ARI) - * @version 2.0 (03/2015) + * @version 2.1 (10/2015) */ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResource { @@ -514,7 +514,7 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour * * @see #writeTable(TAPTable, PrintWriter) */ - private void writeSchema(TAPSchema s, PrintWriter writer) throws IOException{ + protected void writeSchema(TAPSchema s, PrintWriter writer) throws IOException{ final String prefix = "\t\t"; writer.println("\t<schema>"); @@ -577,7 +577,7 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour * * @return The total number of written columns. */ - private int writeTable(TAPTable t, PrintWriter writer){ + protected int writeTable(TAPTable t, PrintWriter writer){ final String prefix = "\t\t\t"; writer.print("\t\t<table"); @@ -635,7 +635,7 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour * @param c The column to format and to write in XML. * @param writer Output in which the XML serialization of the given column must be written. */ - private void writeColumn(TAPColumn c, PrintWriter writer){ + protected void writeColumn(TAPColumn c, PrintWriter writer){ final String prefix = "\t\t\t\t"; writer.print("\t\t\t<column"); @@ -696,7 +696,7 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour * @param fk The foreign key to format and to write in XML. * @param writer Output in which the XML serialization of the given foreign key must be written. */ - private void writeForeignKey(TAPForeignKey fk, PrintWriter writer){ + protected void writeForeignKey(TAPForeignKey fk, PrintWriter writer){ final String prefix = "\t\t\t\t"; writer.println("\t\t\t<foreignKey>"); @@ -728,7 +728,7 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour * <i>false</i> otherwise (here, if the value is NULL or an empty string, the XML item will be written with an empty string as value). * @param writer Output in which the XML node must be written. */ - private void writeAtt(String prefix, String attributeName, String attributeValue, boolean isOptionalAttr, PrintWriter writer){ + protected final void writeAtt(String prefix, String attributeName, String attributeValue, boolean isOptionalAttr, PrintWriter writer){ if (attributeValue != null && attributeValue.trim().length() > 0){ StringBuffer xml = new StringBuffer(prefix); xml.append('<').append(attributeName).append('>').append(VOSerializer.formatText(attributeValue)).append("</").append(attributeName).append('>'); diff --git a/test/tap/config/TestConfigurableServiceConnection.java b/test/tap/config/TestConfigurableServiceConnection.java index f2058cf..ac62179 100644 --- a/test/tap/config/TestConfigurableServiceConnection.java +++ b/test/tap/config/TestConfigurableServiceConnection.java @@ -59,6 +59,8 @@ import tap.db.DBException; import tap.db.JDBCConnection; import tap.formatter.OutputFormat; import tap.formatter.VOTableFormat; +import tap.metadata.TAPMetadata; +import tap.metadata.TAPSchema; import uk.ac.starlink.votable.DataFormat; import uk.ac.starlink.votable.VOTableVersion; import uws.UWSException; @@ -82,27 +84,28 @@ public class TestConfigurableServiceConnection { private static Properties validProp, noFmProp, fmClassNameProp, incorrectFmProp, correctLogProp, incorrectLogLevelProp, - incorrectLogRotationProp, xmlMetaProp, wrongManualMetaProp, - missingMetaProp, missingMetaFileProp, wrongMetaProp, - wrongMetaFileProp, validFormatsProp, validVOTableFormatsProp, - badSVFormat1Prop, badSVFormat2Prop, badVotFormat1Prop, - badVotFormat2Prop, badVotFormat3Prop, badVotFormat4Prop, - badVotFormat5Prop, badVotFormat6Prop, unknownFormatProp, - maxAsyncProp, negativeMaxAsyncProp, notIntMaxAsyncProp, - defaultOutputLimitProp, maxOutputLimitProp, - bothOutputLimitGoodProp, bothOutputLimitBadProp, syncFetchSizeProp, - notIntSyncFetchSizeProp, negativeSyncFetchSizeProp, - notIntAsyncFetchSizeProp, negativeAsyncFetchSizeProp, - asyncFetchSizeProp, userIdentProp, notClassPathUserIdentProp, - coordSysProp, noneCoordSysProp, anyCoordSysProp, - noneInsideCoordSysProp, unknownCoordSysProp, geometriesProp, - noneGeomProp, anyGeomProp, noneInsideGeomProp, unknownGeomProp, - anyUdfsProp, noneUdfsProp, udfsProp, udfsWithClassNameProp, - udfsListWithNONEorANYProp, udfsWithWrongParamLengthProp, - udfsWithMissingBracketsProp, udfsWithMissingDefProp1, - udfsWithMissingDefProp2, emptyUdfItemProp1, emptyUdfItemProp2, - udfWithMissingEndBracketProp, customFactoryProp, - badCustomFactoryProp; + incorrectLogRotationProp, xmlMetaProp, + xmlMetaPropWithCustomMetaClass, xmlMetaPropWithBadCustomMetaClass, + xmlMetaPropWithANonMetaClass, wrongManualMetaProp, missingMetaProp, + missingMetaFileProp, wrongMetaProp, wrongMetaFileProp, + validFormatsProp, validVOTableFormatsProp, badSVFormat1Prop, + badSVFormat2Prop, badVotFormat1Prop, badVotFormat2Prop, + badVotFormat3Prop, badVotFormat4Prop, badVotFormat5Prop, + badVotFormat6Prop, unknownFormatProp, maxAsyncProp, + negativeMaxAsyncProp, notIntMaxAsyncProp, defaultOutputLimitProp, + maxOutputLimitProp, bothOutputLimitGoodProp, + bothOutputLimitBadProp, syncFetchSizeProp, notIntSyncFetchSizeProp, + negativeSyncFetchSizeProp, notIntAsyncFetchSizeProp, + negativeAsyncFetchSizeProp, asyncFetchSizeProp, userIdentProp, + notClassPathUserIdentProp, coordSysProp, noneCoordSysProp, + anyCoordSysProp, noneInsideCoordSysProp, unknownCoordSysProp, + geometriesProp, noneGeomProp, anyGeomProp, noneInsideGeomProp, + unknownGeomProp, anyUdfsProp, noneUdfsProp, udfsProp, + udfsWithClassNameProp, udfsListWithNONEorANYProp, + udfsWithWrongParamLengthProp, udfsWithMissingBracketsProp, + udfsWithMissingDefProp1, udfsWithMissingDefProp2, + emptyUdfItemProp1, emptyUdfItemProp2, udfWithMissingEndBracketProp, + customFactoryProp, badCustomFactoryProp; @BeforeClass public static void setUp() throws Exception{ @@ -132,6 +135,18 @@ public class TestConfigurableServiceConnection { xmlMetaProp.setProperty(KEY_METADATA, VALUE_XML); xmlMetaProp.setProperty(KEY_METADATA_FILE, XML_FILE); + xmlMetaPropWithCustomMetaClass = (Properties)validProp.clone(); + xmlMetaPropWithCustomMetaClass.setProperty(KEY_METADATA, VALUE_XML + " {tap.config.TestConfigurableServiceConnection$MyCustomTAPMetadata}"); + xmlMetaPropWithCustomMetaClass.setProperty(KEY_METADATA_FILE, XML_FILE); + + xmlMetaPropWithBadCustomMetaClass = (Properties)validProp.clone(); + xmlMetaPropWithBadCustomMetaClass.setProperty(KEY_METADATA, VALUE_XML + " {tap.config.TestConfigurableServiceConnection$MyBadCustomTAPMetadata}"); + xmlMetaPropWithBadCustomMetaClass.setProperty(KEY_METADATA_FILE, XML_FILE); + + xmlMetaPropWithANonMetaClass = (Properties)validProp.clone(); + xmlMetaPropWithANonMetaClass.setProperty(KEY_METADATA, VALUE_XML + " MyCustomTAPMetadata"); + xmlMetaPropWithANonMetaClass.setProperty(KEY_METADATA_FILE, XML_FILE); + wrongManualMetaProp = (Properties)validProp.clone(); wrongManualMetaProp.setProperty(KEY_METADATA, "{tap.metadata.TAPMetadata}"); @@ -395,7 +410,7 @@ public class TestConfigurableServiceConnection { fail("This MUST have failed because the property 'metadata' is missing!"); }catch(Exception e){ assertEquals(TAPException.class, e.getClass()); - assertEquals("The property \"" + KEY_METADATA + "\" is missing! It is required to create a TAP Service. Three possible values: " + VALUE_XML + " (to get metadata from a TableSet XML document), " + VALUE_DB + " (to fetch metadata from the database schema TAP_SCHEMA) or the name (between {}) of a class extending TAPMetadata.", e.getMessage()); + assertEquals("The property \"" + KEY_METADATA + "\" is missing! It is required to create a TAP Service. Three possible values: " + VALUE_XML + " (to get metadata from a TableSet XML document), " + VALUE_DB + " (to fetch metadata from the database schema TAP_SCHEMA) or the name (between {}) of a class extending TAPMetadata. Only " + VALUE_XML + " and " + VALUE_DB + " can be followed by the path of a class extending TAPMetadata.", e.getMessage()); } // Missing metadata_file property: @@ -413,7 +428,7 @@ public class TestConfigurableServiceConnection { fail("This MUST have failed because the property 'metadata' has a wrong value!"); }catch(Exception e){ assertEquals(TAPException.class, e.getClass()); - assertEquals("Unsupported value for the property \"" + KEY_METADATA + "\": \"foo\"! Only two values are allowed: " + VALUE_XML + " (to get metadata from a TableSet XML document) or " + VALUE_DB + " (to fetch metadata from the database schema TAP_SCHEMA).", e.getMessage()); + assertEquals("Unsupported value for the property \"" + KEY_METADATA + "\": \"foo\"! Only two values are allowed: " + VALUE_XML + " (to get metadata from a TableSet XML document) or " + VALUE_DB + " (to fetch metadata from the database schema TAP_SCHEMA). Only " + VALUE_XML + " and " + VALUE_DB + " can be followed by the path of a class extending TAPMetadata.", e.getMessage()); } // Wrong MANUAL metadata: @@ -425,6 +440,33 @@ public class TestConfigurableServiceConnection { assertEquals("Wrong class for the property \"" + KEY_METADATA + "\": \"tap.metadata.TAPMetadata\"! The class provided in this property MUST EXTEND tap.metadata.TAPMetadata.", e.getMessage()); } + // XML metadata method WITH a custom TAPMetadata class: + try{ + ServiceConnection sConn = new ConfigurableServiceConnection(xmlMetaPropWithCustomMetaClass); + assertEquals(MyCustomTAPMetadata.class, sConn.getTAPMetadata().getClass()); + }catch(Exception e){ + e.printStackTrace(); + fail("This MUST have succeeded because the property 'metadata' is valid! \nCaught exception: " + getPertinentMessage(e)); + } + + // XML metadata method WITH a BAD custom TAPMetadata class: + try{ + new ConfigurableServiceConnection(xmlMetaPropWithBadCustomMetaClass); + fail("This MUST have failed because the custom class specified in the property 'metadata' does not have any of the required constructor!"); + }catch(Exception e){ + assertEquals(TAPException.class, e.getClass()); + assertEquals("Missing constructor by copy tap.metadata.TAPMetadata(tap.metadata.TAPMetadata) or tap.metadata.TAPMetadata(tap.metadata.TAPMetadata, uws.service.file.UWSFileManager, tap.TAPFactory, tap.log.TAPLog)! See the value \"xml\" of the property \"" + KEY_METADATA + "\".", e.getMessage()); + } + + // XML metadata method WITH a BAD custom TAPMetadata class: + try{ + new ConfigurableServiceConnection(xmlMetaPropWithANonMetaClass); + fail("This MUST have failed because the class specified in the property 'metadata' is not a class name!"); + }catch(Exception e){ + assertEquals(TAPException.class, e.getClass()); + assertEquals("Unexpected string after the fetching method \"xml\": \"MyCustomTAPMetadata\"! The full name of a class extending TAPMetadata was expected. If it is a class name, then it must be specified between {}.", e.getMessage()); + } + // Wrong metadata_file property: try{ new ConfigurableServiceConnection(wrongMetaFileProp); @@ -1197,4 +1239,34 @@ public class TestConfigurableServiceConnection { } + /** + * TAPMetadata extension just to test whether it is possible to customize the output class of ConfigurableServiceConnection with the + * metadata fetching methods "db" and "xml". + * + * @author Grégory Mantelet (ARI) + * @version 08/2015 + */ + private static class MyCustomTAPMetadata extends TAPMetadata { + public MyCustomTAPMetadata(TAPMetadata meta){ + for(TAPSchema s : meta){ + this.addSchema(s); + } + } + } + + /** + * TAPMetadata extension just to test whether it is possible to customize the output class of ConfigurableServiceConnection with the + * metadata fetching methods "db" and "xml". + * + * <strong>This extension is however bad because it does not have any of the required constructor.</strong> + * + * @author Grégory Mantelet (ARI) + * @version 08/2015 + */ + private static class MyBadCustomTAPMetadata extends TAPMetadata { + public MyBadCustomTAPMetadata(){ + + } + } + } -- GitLab