FileDocCategorySizeDatePackage
MappingGenerator.javaAPI DocGlassfish v2 API36375Fri May 04 22:34:48 BST 2007com.sun.jdo.spi.persistence.support.ejb.ejbc

MappingGenerator

public class MappingGenerator extends com.sun.jdo.api.persistence.mapping.ejb.MappingGenerator

Fields Summary
public static final String
JAVA_TO_DB_FLAG
private static final String
DBSCHEMA_EXTENSION
private static final char
DOT
private static final com.sun.jdo.spi.persistence.utility.logging.Logger
logger
The logger
private final com.sun.enterprise.deployment.EjbBundleDescriptor
bundle
private String
dbVendorName
private boolean
isJavaToDatabaseFlag
private boolean
isVerifyFlag
private static final ResourceBundle
messages
I18N message handler
Constructors Summary
public MappingGenerator(com.sun.enterprise.deployment.EjbBundleDescriptor bundle, com.sun.jdo.api.persistence.model.Model model, NameMapper nameMapper, ClassLoader loader)
Constructor

param
bundle an ejb bundle
param
model a model containing mapping class and persistence class information
param
nameMapper a nameMapper for name lookup
param
loader a class loader


                                        
         
                
        super(new EJBBundleInfoHelper(bundle, nameMapper, model, null), loader, false);
        this.bundle = bundle;
    
Methods Summary
private voidaddAllTables(com.sun.jdo.api.persistence.mapping.ejb.beans.SunCmpMapping sunCmpMapping, java.util.Set tables)
Adds all table names referenced by this SunCmpMapping element to this Set.

param
sunCmpMapping the SunCmpMapping element to check.
param
tables the Set to update.

        EntityMapping[] beans = sunCmpMapping.getEntityMapping();
        for (int i = 0; i < beans.length; i++) {
            // Always add the table name.
            addTableName(beans[i].getTableName(), tables);

            // Check if there are table names specified in the
            // cmp-field-mapping.
            CmpFieldMapping[] cmpfields = beans[i].getCmpFieldMapping();
            for (int j = 0; j < cmpfields.length; j++) {
                // There might be more than one column-name for each cmp field.
                String[] names = cmpfields[j].getColumnName();
                for (int jj = 0; jj < names.length; jj++) {
                    addRelatedTableName(names[jj], tables);
                }
            }

            // Check the table names specified in the cmr-field-mapping.
            CmrFieldMapping[] cmrfields = beans[i].getCmrFieldMapping();
            for (int j = 0; j < cmrfields.length; j++) {
                // There might be more than one column-pair for each cmr field.
                ColumnPair[] pairs = cmrfields[j].getColumnPair();
                for (int jj = 0; jj < pairs.length; jj++) {
                    String[] names = pairs[jj].getColumnName();
                    for (int jjj = 0; jjj < names.length; jjj++) {
                        addRelatedTableName(names[jjj], tables);
                    }
                }
            }
        }
    
private voidaddRelatedTableName(java.lang.String columnName, java.util.Set tables)
Adds a table name, if it is specified as a part of the column name, to the Set of known table names.

param
columnName the name of the column to use.
param
tables the Set to update.

        if (!StringHelper.isEmpty(columnName)) {
            int l = columnName.indexOf(DOT);
            if (l > 0) {
                addTableName(columnName.substring(0, l), tables);
            }
        }
    
private voidaddTableName(java.lang.String name, java.util.Set tables)
Add a valid (not null and not all spaces) table name to the Set of known table names.

param
name the table name to add if it's a valid name.
param
tables the Set to update.

        if (!StringHelper.isEmpty(name)) {
            if (logger.isLoggable(Logger.FINE)){
                logger.fine("Adding Table to Capture Set: " + name); // NOI18N
            }

            tables.add(name);
        }
    
private com.sun.enterprise.deployment.ResourceReferenceDescriptorcheckOrCreateCMPResource(boolean mappedBeans)
Check if cmp resource is specified in the deployment descriptor. If the beans are mapped (sun-cmp-mapping.xml is present), the cmp resource must be present, otherwise (in java2db case) we will create a default one. If it's java2db, we will also parse the CLI overrides, as the cmp resource provides the default values.

param
mappedBeans true if beans are mapped in this module.
throws
GeneratorException if beans are mapped but cmp resource is not specified.

        ResourceReferenceDescriptor cmpResource = 
                bundle.getCMPResourceReference();
        if (mappedBeans) {
            if (cmpResource == null) {
                // If mapping exists, the cmpResource must specify a
                // database or a PMF JNDI name.
                throw JDOCodeGeneratorHelper.createGeneratorException( 
                        "EXC_MissingCMPResource", bundle); //NOI18N
            }
        } else {
            if (cmpResource == null) {

                // In JavaToDB case we can deploy to the default jdbc-resource.
                cmpResource = new ResourceReferenceDescriptor();
                cmpResource.setJndiName("jdbc/__default");
                cmpResource.setDatabaseVendorName(DBVendorTypeHelper.DERBY);
                cmpResource.setCreateTablesAtDeploy(true);
                cmpResource.setDropTablesAtUndeploy(true);
                bundle.setCMPResourceReference(cmpResource);
            }
        }
        return cmpResource;
    
private voidensureDBSchemaExistence(com.sun.enterprise.deployment.ResourceReferenceDescriptor cmpResource, com.sun.jdo.api.persistence.mapping.ejb.beans.SunCmpMappings sunCmpMappings, java.lang.String inputFilesPath, java.io.File classout)
Check that there is a dbschema for each element of the SunCmpMappings. For those which are missing, create a corresponding .dbschema file.

param
cmpResource Provides JNDI name for getting database connection
param
sunCmpMappings SunCmpMappings which is checked for having schema
param
inputFilesPath the directory where this bundle's files are located
param
classout the directory where the classes are located
exception
DBException Thrown if database model throws it
exception
IOException Thrown if .dbschema file cannot be created.
exception
SQLException Thrown if we cannot get get required info from the database.


        String generatedSchemaName = getInfoHelper().getSchemaNameToGenerate();
        Set tables = new HashSet();
        int size = sunCmpMappings.sizeSunCmpMapping();

        // Sweep through the mappings to check dbschema existence.  If a
        // mapping does not have a dbschema, get a list of tables to be
        // captured, then capture them, create the corresponding dbschema,
        // and save it.
        for (int i = 0; i < size; i++) {
            SunCmpMapping sunCmpMapping = sunCmpMappings.getSunCmpMapping(i);

            String schemaName = sunCmpMapping.getSchema();
            if (StringHelper.isEmpty(schemaName)) {
                if (!isVerifyFlag) {
                    // The tables in this section need to be captured.
                    addAllTables(sunCmpMapping, tables);
                    sunCmpMapping.setSchema(generatedSchemaName);
                } else {
                    // If it is from verifier, capture schema internally
                    // to perform sun-cmp-mappings.xml and EJB validation
                    getConversionHelper().setEnsureValidation(false);
                }
                 
            } else {
                File dbschemaFile = new File(
                        new StringBuffer(inputFilesPath)
                            .append(File.separator)
                            .append(schemaName)
                            .append(DBSCHEMA_EXTENSION).toString());
                if (! (dbschemaFile.exists()
                       && dbschemaFile.isFile()
                       && dbschemaFile.canRead())) {
                    throw new GeneratorException(
                            I18NHelper.getMessage(
                            messages, "CMG.MissingDBSchema", // NOI18N
                            bundle.getApplication().getRegistrationName(),
                            JDOCodeGeneratorHelper.getModuleName(bundle), 
                            schemaName));
                }
            }
        }

        // If there were tables to be captured, they will be in the list.
        // Now we need to go and capture those tables.
        if (tables.size() > 0) {
            String userSchema = null;
            Connection con = DeploymentHelper.getConnection(cmpResource.getJndiName());
            DatabaseMetaData dmd = con.getMetaData();
            if (DBVendorTypeHelper.requireUpperCaseSchema(dmd)) {
                userSchema = dmd.getUserName().trim().toUpperCase();
            }

            ConnectionProvider cp = new ConnectionProvider(con, dmd.getDriverName().trim());
            if (userSchema != null) {
                cp.setSchema(userSchema);
            }

            OutputStream outstream = null;

            try {
                SchemaElementImpl outSchemaImpl = new SchemaElementImpl(cp);
                SchemaElement schemaElement = new SchemaElement(outSchemaImpl);
                schemaElement.setName(DBIdentifier.create(generatedSchemaName));

                if(dmd.getDatabaseProductName().compareToIgnoreCase("MYSQL") == 0)
                    outSchemaImpl.initTables(cp, new LinkedList(tables), new LinkedList(), true);
                else
                    outSchemaImpl.initTables(cp, new LinkedList(tables), new LinkedList(), false);
                outstream = new FileOutputStream(
                        new File(classout,
                        new StringBuffer(generatedSchemaName)
                            .append(DBSCHEMA_EXTENSION).toString()));

                // XXX Unfortunately, if SchemaElement.save gets an
                // IOException, it prints the stack trace but does not
                // let us handle it :-(
                schemaElement.save(outstream);

            } catch (IOException ex) {
                // Catch FileNotFound, etc.
                throw JDOCodeGeneratorHelper.createGeneratorException( 
                        "CMG.CannotSaveDBSchema", bundle, ex); // NOI18N
            } finally {
                cp.closeConnection();
                try {
                    if (outstream != null) {
                        outstream.close();
                    } 
                } catch (IOException ex) {
                    if (logger.isLoggable(Logger.FINE))
                        logger.fine(ex.toString());
                }
            }
        }
    
public org.netbeans.modules.dbschema.SchemaElementgenerateMapping(com.sun.ejb.codegen.EjbcContext ejbcContext, java.lang.String inputFilesPath, java.lang.String generatedXmlsPath, java.io.File classout, boolean ignoreSunDeploymentDescriptors)
This method will load mapping classes if there is sun-cmp-mappings.xml, otherwise it will call the database generation backend to create mapping classes and schema. It also generates *.dbschema and sun-cmp-mappings.xml in application dir if it is in creating mapping classes mode.

param
ejbcContext an object containing CLI options for the database generation backend
param
inputFilesPath the directory where sun-cmp-mappings.xml is located
param
generatedXmlsPath the directory where the generated files are located
@param classout the directory where the classes are located
param
ignoreSunDeploymentDescriptors use java2db generation if set to true.
return
a SchemaElement for mapping classes mapped to
throws
IOException
throws
DBException
throws
ModelException
throws
Schema2BeansException
throws
SQLException
throws
GeneratorException
throws
ConversionException


        SchemaElement schema = null;
        if (ejbcContext == null)
            isVerifyFlag = true;

        File cmpMappingFile = getSunCmpMappingFile(inputFilesPath);
        boolean mappedBeans = !ignoreSunDeploymentDescriptors 
                && cmpMappingFile.exists();
        ResourceReferenceDescriptor cmpResource = checkOrCreateCMPResource(
                mappedBeans);

        // Remember whether or not this mapping was created by Java2DB.
        isJavaToDatabaseFlag = DeploymentHelper.isJavaToDatabase(
                cmpResource.getSchemaGeneratorProperties());

        // We *must* get a vendor name if either the beans are not mapped, or
        // they are mapped and the javaToDatabase flag is set.
        boolean mustHaveDBVendorName =
            !mappedBeans || (mappedBeans && isJavaToDatabaseFlag);
        
        // Read deployment settings from the deployment descriptor
        // and CLI options.
        Results deploymentArguments = getDeploymentArguments(
                ejbcContext, cmpResource, mustHaveDBVendorName);
        dbVendorName = deploymentArguments.getDatabaseVendorName();

        if (mappedBeans) {
            // If sun-cmp-mappings.xml exists and we are doing a deployment,
            // validate some arguments and make sure we have dbschema.

            // If it is from verify, skip deployment arguments check.
            if (!isVerifyFlag) {
                String warning = null; // Warning for user, if required.
            
                if (isJavaToDatabaseFlag) {

                    // If beans were already mapped, we will generate tables, but
                    // they will be as per the existing mapping.  So if the user
                    // gave --uniquetablenames, warn them that we will not take
                    // that flag into account.  I.e., the tables will be generated
                    // as per the mapping.
                    if (deploymentArguments.hasUniqueTableNames()) {
                        warning = 
                            I18NHelper.getMessage(
                                messages,
                                "EXC_DisallowJava2DBUniqueTableNames", //NOI18N
                                bundle.getApplication().getRegistrationName(),
                                JDOCodeGeneratorHelper.getModuleName(bundle));
                        logger.warning(warning);
                    }
                } else if (deploymentArguments.hasJavaToDatabaseArgs()) {

                    // If beans are already mapped but the user gave any Java2DB
                    // command line arguments, warn the user that these args
                    // should not be used when module is already mapped.
                    warning = 
                        I18NHelper.getMessage(
                            messages,
                            "EXC_DisallowJava2DBCLIOverrides", //NOI18N
                            bundle.getApplication().getRegistrationName(),
                            JDOCodeGeneratorHelper.getModuleName(bundle));
                    logger.warning(warning);
                }

                if (warning != null) {
                    DeploymentStatus status =
                        ejbcContext.getDeploymentRequest()
                        .getCurrentDeploymentStatus();
                    status.setStageStatus(DeploymentStatus.WARNING);
                    String msg = status.getStageStatusMessage();
                    msg = (msg == null) ? warning : (msg + "\n" + warning); // NOI18N
                    status.setStageStatusMessage(msg);
                }
            }
               
            // Sun-cmp-mapping.xml exists, use normal MappingClass loading
            SunCmpMappings sunCmpMappings = getSunCmpMappings(cmpMappingFile);

            // Ensure that there is a dbschema for each element of
            // sunCmpMappings.
            ensureDBSchemaExistence(cmpResource, sunCmpMappings, inputFilesPath,
                classout);

            // load real mapping model and jdo model in memory
            Map mappingClasses = loadMappingClasses(sunCmpMappings, getClassLoader());

            // Get schema from one of the mapping classes. 
            // The mapping class element may be null if there is inconsistency 
            // in sun-cmp-mappings.xml and ejb-jar.xml. For example, 
            // the bean has mapping information in sun-cmp-mappings.xml but 
            // no definition in the ejb-jar.xml.
            // So iterate over the mappings until the 1st non-null is found.
            MappingClassElement mc = null;
            Iterator iter = mappingClasses.values().iterator();
            while (iter.hasNext()) {
                mc = (MappingClassElement)iter.next();
                if (mc != null) {
                    schema = SchemaElement.forName(mc.getDatabaseRoot());
                    break;
                }
            }

            if (logger.isLoggable(Logger.FINE)){
                logger.fine("Loaded mapped beans for " // NOI18N
                            + cmpResource.getJndiName()
                            + ", isJavaToDatabase=" + isJavaToDatabaseFlag); // NOI18N
            }
        }
        else {
            // Generate mapping file and dbschema, since either
            // sun-cmp-mappings.xml does not exist (e.g. user didn't yet map)
            // or ejbcContext is null (e.g. running under auspices of AVK).
            DatabaseGenerator.Results results  = generateMappingClasses(
                    dbVendorName, deploymentArguments.getUseUniqueTableNames(), 
                    deploymentArguments.getUserPolicy(), inputFilesPath);

            // java2db from verifier should not save anything to disk
            if (!isVerifyFlag) {
                // save SunCmpMapping to sun-cmp-mappings.xml 
                // in generated XML dir
                writeSunCmpMappingFile(results.getMappingClasses(), 
                    getSunCmpMappingFile(generatedXmlsPath));

                schema = results.getSchema();

                // save schema to dbschema file in generated XML dir
                writeSchemaFile(schema, classout);

                setJavaToDatabase(cmpResource, true);
            }
        }

        return schema;
    
public java.lang.StringgetDatabaseVendorName()

        return dbVendorName;
    
private com.sun.jdo.spi.persistence.support.ejb.ejbc.MappingGenerator$ResultsgetDeploymentArguments(com.sun.ejb.codegen.EjbcContext ejbcContext, com.sun.enterprise.deployment.ResourceReferenceDescriptor cmpResource, boolean connectToDatabase)
Reads deployment settings from the deployment descriptor and CLI options and populates the corresponding variables.

param
ejbcContext CLI arguments are obtained from here.
param
cmpResource Parameters from deployment descriptor are obtained from here.
param
connectToDatabase If true, then connect to database to get database vendor name if not otherwise available.


        String useUniqueTableNames = null;
        String dbVendorName = null;
        Properties userPolicy = null;

        //Indicates that one or more Java2DB arguments were given on the command
        //line.
        boolean javaToDatabaseArgs = false;

        // If ejbcContext is not available, then use what is specified by
        // cmpResource.
        if (null == ejbcContext) {
            dbVendorName = cmpResource.getDatabaseVendorName();

        } else {
            // Otherwise, get the vendor name from one of the CLI overrides,
            // cmpResource, or the actual database (in that order).
            Properties cliOverrides = ejbcContext.getOptionalArguments();
            useUniqueTableNames = cliOverrides.getProperty(
                    Constants.CMP_UNIQUE_TABLE_NAMES);

            // In javaToDatabaseArgs, we collect whether or not we have seen
            // any of the java to database - related arguments, starting with
            // --uniquetablenames.
            javaToDatabaseArgs =isPropertyDefined(useUniqueTableNames);

            dbVendorName = cliOverrides.getProperty(Constants.CMP_DB_VENDOR_NAME);

            javaToDatabaseArgs |= isPropertyDefined(dbVendorName);

            // XXX This check can be removed when DeployCommand guarantees to
            // not return UNDEFINED.
            if (null == dbVendorName || dbVendorName.equals(Constants.UNDEFINED)) {
                dbVendorName = cmpResource.getDatabaseVendorName();
            }

            // If there is no CLI override, and nothing specified in the
            // cmp-resource, try to get the dbvendorname from the database.
            if (null == dbVendorName && connectToDatabase) {
                try {
                    Connection conn = DeploymentHelper.getConnection(
                            cmpResource.getJndiName());
                    dbVendorName = conn.getMetaData().getDatabaseProductName();
                } catch (Exception ex) {
                    // Ignore exceptions and use default.
                }
            }                    
            String createTables =
                cliOverrides.getProperty(Constants.CMP_CREATE_TABLES);
            javaToDatabaseArgs |= isPropertyDefined(createTables);

            String dropAndCreateTables =
                cliOverrides.getProperty(Constants.CMP_DROP_AND_CREATE_TABLES);
            javaToDatabaseArgs |= isPropertyDefined(dropAndCreateTables);

        }

        if (null == dbVendorName) {
            dbVendorName = DBVendorTypeHelper.DEFAULT_DB;
        } else {
            dbVendorName = DBVendorTypeHelper.getDBType(dbVendorName);
        }

        userPolicy = cmpResource.getSchemaGeneratorProperties();

        return new Results(useUniqueTableNames, dbVendorName, userPolicy, javaToDatabaseArgs);
    
private static java.io.FilegetSunCmpMappingFile(java.lang.String filesPath)
Gets sun-cmp-mappings.xml file

param
filesPath a string consisting file path
return
a file of sun-cmp-mappings.xml

        String cmpMappingFile = (new StringBuffer(filesPath).
                append(File.separator).
                append(MappingFile.DEFAULT_LOCATION_IN_EJB_JAR)).toString();

        // if the file contains directory structure, we need
        // to create those directories if they do not exist.
        if (cmpMappingFile.lastIndexOf(File.separatorChar) != -1) {
            String dirs = cmpMappingFile.substring(
                0, cmpMappingFile.lastIndexOf(File.separatorChar));
            File fileDirs = new File(dirs);
            if (!fileDirs.exists())
                fileDirs.mkdirs();
        }

        return new File(cmpMappingFile);
    
private com.sun.jdo.api.persistence.mapping.ejb.beans.SunCmpMappingsgetSunCmpMappings(java.io.File cmpMappingFile)
Loads sun-cmp-mapping.xml into memory as SunCmpMappings

param
cmpMappingFile a file of sun-cmp-mappings.xml
return
a SunCmpMappings object
throws
IOException
throws
Schema2BeansException

        InputStream is = null;
        BufferedInputStream iasMapping = null;
        SunCmpMappings sunCmpMapping = null;

        if (cmpMappingFile.length() == 0) {
            throw JDOCodeGeneratorHelper.createGeneratorException(
                    "CMG.BeansFileSizeIsZero", bundle); // NOI18N
        }

        try {
            is = new FileInputStream(cmpMappingFile);
            iasMapping = new BufferedInputStream(is);
            sunCmpMapping = SunCmpMappings.createGraph(iasMapping);
        } catch (IOException ex) {
            throw ex;
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch(Exception ex) {
                    if (logger.isLoggable(Logger.FINE))
                        logger.fine(ex.toString());
                }
            }
            if (iasMapping != null) {
                try {
                    iasMapping.close();
                } catch(Exception ex) {
                    if (logger.isLoggable(Logger.FINE))
                        logger.fine(ex.toString());
                }
            }
        }

        try {
            sunCmpMapping.validate();
        } catch (ValidateException ex) {
            throw JDOCodeGeneratorHelper.createGeneratorException(
                    "CMG.InvalidSunCmpMappingsFile", bundle, ex); // NOI18N
        }

        return sunCmpMapping;
    
public booleanisJavaToDatabase()
Returns javatodb flag in cmpResource.

return
true if there is name as "javatodb" and value as "true"

        return isJavaToDatabaseFlag;
    
protected booleanisPropertyDefined(java.lang.String propertyValue)
Returns true if the specified propertyValue represents a defined value, false otherwise. This implementation returns true if the value is not empty and does not equal Constants.UNDEFINED.

param
propertyValue the value to be tested for defined
return
true if the specified propertyValue represents a defined value, false otherwise

        return (super.isPropertyDefined(propertyValue) &&
			!Constants.UNDEFINED.equals(propertyValue));
    
private voidsetJavaToDatabase(com.sun.enterprise.deployment.ResourceReferenceDescriptor cmpResource, boolean value)
Set javatodb flag into SchemaGeneratorProperties

param
cmpResource a ResourceReferenceDescriptor
param
value a string containing true or false


        if (logger.isLoggable(Logger.FINE)) {
            logger.fine("set javatodb flag to " + value + " in cmpResource"); // NOI18N
        }

        Properties schemaGeneratorProperties = cmpResource.
                getSchemaGeneratorProperties();
        if (schemaGeneratorProperties == null) {
            schemaGeneratorProperties = new Properties();
            cmpResource.setSchemaGeneratorProperties(schemaGeneratorProperties);
        }

        schemaGeneratorProperties.setProperty(DatabaseConstants.JAVA_TO_DB_FLAG, 
                String.valueOf(value));

        isJavaToDatabaseFlag = value;
    
private static voidwriteSchemaFile(org.netbeans.modules.dbschema.SchemaElement schema, java.io.File filePath)
Writes to *.dbschema file from schema

param
schema a SchemaElement
param
filePath a directory where *.dbschema is located
throws
IOException

        OutputStream schemaStream = null;
        try {
            schemaStream = new FileOutputStream(
                new File(filePath, NameUtil.getSchemaResourceName(
                schema.getName().getName())));
            schema.save(schemaStream);
        } catch (IOException ex) {
           throw ex;
        } finally {
            try {
                if (schemaStream != null) {
                    schemaStream.close();
                }
            } catch (IOException ex) {
               if (logger.isLoggable(Logger.FINE))
                    logger.fine(ex.toString());
            }
        }
    
private voidwriteSunCmpMappingFile(java.util.Set mappingClasses, java.io.File cmpMappingFile)
Writes to sun-cmp-mappings.xml from mappings classes

param
mappingClasses a set of mapping classes
param
cmpMappingFile corresponds to sun-cmp-mappings.xml
throws
IOException
throws
ConversionException
throws
Schema2BeansException

        // Construct the input to MappingFile.fromMappingClasses(): a Map
        // object containing ejbName and MappingClassElement.  Use the
        // elements of iteration and NameMapper to create the input for
        // MappingFile.
        Map mappingMap = new HashMap();
        AbstractNameMapper nameMapper = getNameMapper();
        Iterator iter = mappingClasses.iterator();
        while (iter.hasNext()) {
            MappingClassElement mappingClass = (MappingClassElement)iter.next();
            String ejbName = nameMapper.getEjbNameForPersistenceClass(
                    mappingClass.getName());
            mappingMap.put(ejbName, mappingClass);
        }
        MappingFile mf = new MappingFile();
        OutputStream sunCmpMapping = null; 
        try {
            sunCmpMapping = new FileOutputStream(
                cmpMappingFile);
            mf.fromMappingClasses(sunCmpMapping, mappingMap, 
                getConversionHelper());
        } catch (IOException ex) {
            throw ex;
        } finally {
            try {
                if (sunCmpMapping != null) {
                    sunCmpMapping.close();
                }
            } catch (IOException ex) {
                if (logger.isLoggable(Logger.FINE))
                    logger.fine(ex.toString());
            }
        }