/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.jena.reasoner.rulesys.test;

import java.io.* ;
import java.util.* ;

import org.apache.jena.ontology.OntModelSpec ;
import org.apache.jena.rdf.model.* ;
import org.apache.jena.reasoner.InfGraph ;
import org.apache.jena.reasoner.Reasoner ;
import org.apache.jena.reasoner.ReasonerException ;
import org.apache.jena.reasoner.ReasonerRegistry ;
import org.apache.jena.reasoner.rulesys.FBRuleInfGraph ;
import org.apache.jena.reasoner.test.WGReasonerTester ;
import org.apache.jena.vocabulary.* ;

/**
 * Test harness for running the WebOnt working group tests relevant 
 * to the OWL rule reasoner. See also TestOWLRules which runs the
 * core WG tests as part of the routine unit tests.
 */
public class WebOntTestHarness {

//  =======================================================================
//  Variables to control the test operations

    /** Set to true to include modified test versions */
    public static boolean includeModified = false;
    
    /** Set to true to use approved tests only */
    public static boolean approvedOnly = true;
    
    /** Set to true to print LP engine profile information */
    public static boolean printProfile = false;
    
//  =======================================================================
//  Internal state

    /** The reasoner being tested */
    Reasoner reasoner;
    
    /** The total set of known tests */
    Model testDefinitions;
    
    /** The number of tests run */
    int testCount = 0;
    
    /** The time cost in ms of the last test to be run */
    long lastTestDuration = 0;
    
    /** Number of tests passed */
    int passCount = 0;
    
    /** The model describing the results of the run */
    Model testResults;
    
    /** The resource which acts as a description for the Jena2 instance being tested */
    Resource jena2;
    
//  =======================================================================
//  Internal constants

    /** The base directory for the working group test files to use */
    public static final String BASE_TESTDIR = "testing/wg/";
    
    /** The base URI in which the files are purported to reside */
    public static String BASE_URI = "http://www.w3.org/2002/03owlt/";
    
    /** The base URI for the results file */
    public static String BASE_RESULTS_URI = "http://jena.sourceforge.net/data/owl-results.rdf";
    
    /** The list of subdirectories to process (omits the rdf/rdfs dirs) */
    public static final String[] TEST_DIRS= {"AllDifferent", "AllDistinct", 
            "AnnotationProperty", "DatatypeProperty", "FunctionalProperty",
            "I3.2", "I3.4", "I4.1", "I4.5", "I4.6", "I5.1", "I5.2", "I5.21", "I5.24",
            "I5.26", "I5.3", "I5.5", "I5.8", "InverseFunctionalProperty", "Nothing", 
            "Restriction", "SymmetricProperty", "Thing", "TransitiveProperty", 
            "allValuesFrom", "amp-in-url", "cardinality", "complementOf", "datatypes", 
            "differentFrom", "disjointWith", "distinctMembers", 
            "equivalentClass", "equivalentProperty", "imports", 
            "intersectionOf", "inverseOf", "localtests", "maxCardinality", "miscellaneous",
            "oneOf", "oneOfDistinct", "sameAs", "sameClassAs", "sameIndividualAs", 
            "samePropertyAs", "someValuesFrom", "statement-entailment", "unionOf", 
            "xmlbase",
            "description-logic", 
//            "extra-credit", 
        };
    
    /** List of tests that are blocked because they test language features beyond Lite */
    public static final String[] BLOCKED_TESTS = {
        // Explicitly testing non-lite features
        "http://www.w3.org/2002/03owlt/complementOf/Manifest001#test", 
        "http://www.w3.org/2002/03owlt/description-logic/Manifest901#test", 
        "http://www.w3.org/2002/03owlt/description-logic/Manifest903#test", 
        "http://www.w3.org/2002/03owlt/description-logic/Manifest902#test", 
        "http://www.w3.org/2002/03owlt/description-logic/Manifest904#test", 
//        "http://www.w3.org/2002/03owlt/oneOf/Manifest002#test", 
//        "http://www.w3.org/2002/03owlt/oneOf/Manifest003#test", 
        "http://www.w3.org/2002/03owlt/oneOf/Manifest004#test", 
        "http://www.w3.org/2002/03owlt/unionOf/Manifest001#test", 
        "http://www.w3.org/2002/03owlt/unionOf/Manifest002#test",
        "http://www.w3.org/2002/03owlt/unionOf/Manifest003#test",
        "http://www.w3.org/2002/03owlt/unionOf/Manifest004#test",
        "http://www.w3.org/2002/03owlt/equivalentClass/Manifest006#test",
        "http://www.w3.org/2002/03owlt/equivalentClass/Manifest007#test",
        "http://www.w3.org/2002/03owlt/description-logic/Manifest201#test",
        "http://www.w3.org/2002/03owlt/I5.8/Manifest004#test",
        "http://www.w3.org/2002/03owlt/I5.2/Manifest004#test",
        
        "http://www.w3.org/2002/03owlt/description-logic/Manifest008#test",
        "http://www.w3.org/2002/03owlt/description-logic/Manifest011#test",
        "http://www.w3.org/2002/03owlt/description-logic/Manifest015#test",
        "http://www.w3.org/2002/03owlt/description-logic/Manifest019#test",
        "http://www.w3.org/2002/03owlt/description-logic/Manifest023#test",
        "http://www.w3.org/2002/03owlt/description-logic/Manifest026#test",
        "http://www.w3.org/2002/03owlt/description-logic/Manifest027#test",
        "http://www.w3.org/2002/03owlt/description-logic/Manifest029#test",
        "http://www.w3.org/2002/03owlt/description-logic/Manifest030#test",
        "http://www.w3.org/2002/03owlt/description-logic/Manifest032#test",
        "http://www.w3.org/2002/03owlt/description-logic/Manifest033#test",
        "http://www.w3.org/2002/03owlt/description-logic/Manifest035#test",

        "http://www.w3.org/2002/03owlt/description-logic/Manifest101#test",
        "http://www.w3.org/2002/03owlt/description-logic/Manifest102#test",
        "http://www.w3.org/2002/03owlt/description-logic/Manifest103#test",
        "http://www.w3.org/2002/03owlt/description-logic/Manifest104#test",
        "http://www.w3.org/2002/03owlt/description-logic/Manifest105#test",
        "http://www.w3.org/2002/03owlt/description-logic/Manifest106#test",
        "http://www.w3.org/2002/03owlt/description-logic/Manifest107#test",
        "http://www.w3.org/2002/03owlt/description-logic/Manifest108#test",
        "http://www.w3.org/2002/03owlt/description-logic/Manifest109#test",
        "http://www.w3.org/2002/03owlt/description-logic/Manifest110#test",
        "http://www.w3.org/2002/03owlt/description-logic/Manifest111#test",
        "http://www.w3.org/2002/03owlt/description-logic/Manifest502#test",
        "http://www.w3.org/2002/03owlt/description-logic/Manifest504#test",
        
        "http://www.w3.org/2002/03owlt/description-logic/Manifest202#test",
        "http://www.w3.org/2002/03owlt/description-logic/Manifest203#test",
        "http://www.w3.org/2002/03owlt/description-logic/Manifest204#test",
        "http://www.w3.org/2002/03owlt/description-logic/Manifest205#test",
        "http://www.w3.org/2002/03owlt/description-logic/Manifest206#test",
        "http://www.w3.org/2002/03owlt/description-logic/Manifest207#test",
        "http://www.w3.org/2002/03owlt/description-logic/Manifest208#test",

        "http://www.w3.org/2002/03owlt/description-logic/Manifest209#test",
        
        "http://www.w3.org/2002/03owlt/miscellaneous/Manifest010#test",
        "http://www.w3.org/2002/03owlt/miscellaneous/Manifest011#test",
        "http://www.w3.org/2002/03owlt/SymmetricProperty/Manifest002#test",
        
        "http://www.w3.org/2002/03owlt/Thing/Manifest005#test",
        
        // Temporary block - incomplete (OOM eventually in some cases)
        "http://www.w3.org/2002/03owlt/TransitiveProperty/Manifest002#test",
        "http://www.w3.org/2002/03owlt/description-logic/Manifest661#test",
        "http://www.w3.org/2002/03owlt/description-logic/Manifest662#test",
        "http://www.w3.org/2002/03owlt/description-logic/Manifest663#test",
        
        "http://www.w3.org/2002/03owlt/description-logic/Manifest608#test",
        "http://www.w3.org/2002/03owlt/description-logic/Manifest611#test",
        "http://www.w3.org/2002/03owlt/description-logic/Manifest615#test",
        "http://www.w3.org/2002/03owlt/description-logic/Manifest623#test",
        "http://www.w3.org/2002/03owlt/description-logic/Manifest626#test",
        "http://www.w3.org/2002/03owlt/description-logic/Manifest627#test",
        "http://www.w3.org/2002/03owlt/description-logic/Manifest630#test",        
        "http://www.w3.org/2002/03owlt/description-logic/Manifest668#test",
        "http://www.w3.org/2002/03owlt/description-logic/Manifest668#test",

    };
            
    /** The list of status values to include. If approvedOnly then only the first
     *  entry is allowed */
    public static final String[] STATUS_FLAGS = { "APPROVED", "PROPOSED" };
    
//  =======================================================================
//  Constructor and associated support
    
    public WebOntTestHarness() {
        testDefinitions = loadAllTestDefinitions();
        reasoner = ReasonerRegistry.getOWLReasoner();
        initResults();
    }

    /** Load all of the known manifest files into a single model */
    public static Model loadAllTestDefinitions() {
        System.out.print("Loading manifests "); System.out.flush();
        Model testDefs = ModelFactory.createDefaultModel();
        int count = 0;
        for ( String TEST_DIR : TEST_DIRS )
        {
            File dir = new File( BASE_TESTDIR + TEST_DIR );
            String[] manifests = dir.list( new FilenameFilter()
            {
                @Override
                public boolean accept( File df, String name )
                {
                    if ( name.startsWith( "Manifest" ) && name.endsWith( ".rdf" ) )
                    {
                        return includeModified || !name.endsWith( "-mod.rdf" );
                    }
                    else
                    {
                        return false;
                    }
                }
            } );
            for ( String manifest : manifests )
            {
                File mf = new File( dir, manifest );
                try
                {
                    testDefs.read( new FileInputStream( mf ), "file:" + mf );
                    count++;
                    if ( count % 8 == 0 )
                    {
                        System.out.print( "." );
                        System.out.flush();
                    }
                }
                catch ( FileNotFoundException e )
                {
                    System.out.println( "File not readable - " + e );
                }
            }
        }
        System.out.println("loaded");
        return testDefs;
    }
    
    /** 
     * Initialize the result model.
     */
    public void initResults() {
        testResults = ModelFactory.createDefaultModel();
        jena2 = testResults.createResource(BASE_RESULTS_URI + "#jena2");
        jena2.addProperty(RDFS.comment, 
            testResults.createLiteral(
                "<a xmlns=\"http://www.w3.org/1999/xhtml\" href=\"http://jena.sourceforce.net/\">Jena2</a> includes a rule-based inference engine for RDF processing, " +
                "supporting both forward and backward chaining rules. Its OWL rule set is designed to provide sound " +
                "but not complete instance resasoning for that fragment of OWL/Full limited to the OWL/lite vocabulary. In" +
                "particular it does not support unionOf/complementOf.",
                true)
        );
        jena2.addProperty(RDFS.label, "Jena2");
        testResults.setNsPrefix("results", OWLResults.NS);
    }
    
//  =======================================================================
//  Main control methods
    
    public static void main(String[] args) throws IOException {
        String resultFile = "owl-results.rdf";
        if (args.length >= 1) {
            resultFile = args[0];
        }
        WebOntTestHarness harness = new WebOntTestHarness();
        harness.runTests();
//        harness.runTest("http://www.w3.org/2002/03owlt/AnnotationProperty/Manifest004#test");
//        harness.runTest("http://www.w3.org/2002/03owlt/AnnotationProperty/Manifest003#test");
//        harness.runTest("http://www.w3.org/2002/03owlt/Thing/Manifest001#test");
//        harness.runTest("http://www.w3.org/2002/03owlt/Thing/Manifest002#test");
//        harness.runTest("http://www.w3.org/2002/03owlt/Thing/Manifest003#test");
//        harness.runTest("http://www.w3.org/2002/03owlt/Thing/Manifest004#test");
//        harness.runTest("http://www.w3.org/2002/03owlt/Thing/Manifest005#test");
        RDFWriter writer = harness.testResults.getWriter("RDF/XML-ABBREV");
        OutputStream stream = new FileOutputStream(resultFile);
        writer.setProperty("showXmlDeclaration", "true");
        harness.testResults.setNsPrefix("", "http://www.w3.org/1999/xhtml");
        writer.write(harness.testResults, stream, BASE_RESULTS_URI);
    }
    
    /**
     * Run all relevant tests.
     */
    public void runTests() {
        System.out.println("Testing " + (approvedOnly ? "only APPROVED" : "APPROVED and PROPOSED") );
        System.out.println("Positive entailment: ");
        runTests(findTestsOfType(OWLTest.PositiveEntailmentTest));
//        System.out.println("\nNegative entailment: ");
//        runTests(findTestsOfType(OWLTest.NegativeEntailmentTest));
        System.out.println("\nTrue tests: ");
        runTests(findTestsOfType(OWLTest.TrueTest));
        System.out.println("\nOWL for OWL tests: ");
        runTests(findTestsOfType(OWLTest.OWLforOWLTest));
        System.out.println("\nImport entailment tests: ");
        runTests(findTestsOfType(OWLTest.ImportEntailmentTest));
        System.out.println("\nInconsistency tests: ");
        runTests(findTestsOfType(OWLTest.InconsistencyTest));
        System.out.println("\nPassed " + passCount + " out of " + testCount);
    }
    
    /**
     * Run all tests in the given list.
     */
    public void runTests(List<Resource> tests) {
        for ( Resource test : tests )
        {
            runTest( test );
        }
    }
    
    /**
     * Run a single test of any sort, performing any appropriate logging
     * and error reporting.
     */
    public void runTest(String test) {
        runTest(testDefinitions.getResource(test));
    }
     
    /**
     * Run a single test of any sort, performing any appropriate logging
     * and error reporting.
     */
    public void runTest(Resource test) {
        System.out.println("Running " + test);
        boolean success = false;
        boolean fail = false;
        try {
            success = doRunTest(test);
        } catch (Exception e) {
            fail = true;
            System.out.print("\nException: " + e);
            e.printStackTrace();
        }
        testCount++;
        if (success) {
            System.out.print( (testCount % 40 == 0) ? ".\n" : ".");
            System.out.flush();
            passCount++;
        } else {
            System.out.println("\nFAIL: " + test);
        }
        Resource resultType = null;
        if (fail) {
            resultType = OWLResults.FailingRun;
        } else {
            if (test.hasProperty(RDF.type, OWLTest.NegativeEntailmentTest) 
            ||  test.hasProperty(RDF.type, OWLTest.ConsistencyTest)) {
                resultType = success ? OWLResults.PassingRun : OWLResults.FailingRun;
            } else {
                resultType = success ? OWLResults.PassingRun : OWLResults.IncompleteRun;
            }
        }
        // log to the rdf result format
        Resource result = testResults.createResource()
            .addProperty(RDF.type, OWLResults.TestRun)
            .addProperty(RDF.type, resultType)
            .addProperty(OWLResults.test, test)
            .addProperty(OWLResults.system, jena2);
    }
    
    /**
     * Run a single test of any sort, return true if the test succeeds.
     */
    public boolean doRunTest(Resource test) throws IOException {
        if (test.hasProperty(RDF.type, OWLTest.PositiveEntailmentTest) 
        ||  test.hasProperty(RDF.type, OWLTest.NegativeEntailmentTest)
        ||  test.hasProperty(RDF.type, OWLTest.OWLforOWLTest)
        ||  test.hasProperty(RDF.type, OWLTest.ImportEntailmentTest)
        ||  test.hasProperty(RDF.type, OWLTest.TrueTest) ) {
            // Entailment tests
            boolean processImports = test.hasProperty(RDF.type, OWLTest.ImportEntailmentTest);
            Model premises = getDoc(test, RDFTest.premiseDocument, processImports);
            Model conclusions = getDoc(test, RDFTest.conclusionDocument);
            comprehensionAxioms(premises, conclusions);
            long t1 = System.currentTimeMillis();
            InfGraph graph = reasoner.bind(premises.getGraph());
            if (printProfile) {
                ((FBRuleInfGraph)graph).resetLPProfile(true);
            }
            Model result = ModelFactory.createModelForGraph(graph);
            boolean correct = WGReasonerTester.testConclusions(conclusions.getGraph(), result.getGraph());
            long t2 = System.currentTimeMillis();
            lastTestDuration = t2 - t1; 
            if (printProfile) {
                ((FBRuleInfGraph)graph).printLPProfile();
            }
            if (test.hasProperty(RDF.type, OWLTest.NegativeEntailmentTest)) {
                correct = !correct;
            }
            return correct;
        } else if (test.hasProperty(RDF.type, OWLTest.InconsistencyTest)) {
//            System.out.println("Starting: " + test);
            Model input = getDoc(test, RDFTest.inputDocument);
            long t1 = System.currentTimeMillis();
            InfGraph graph = reasoner.bind(input.getGraph());
            boolean correct = ! graph.validate().isValid();
            long t2 = System.currentTimeMillis();
            lastTestDuration = t2 - t1; 
            return correct;
        } else if (test.hasProperty(RDF.type, OWLTest.ConsistencyTest)) {
            // Not used normally becase we are not complete enough to prove consistency
//            System.out.println("Starting: " + test);
            Model input = getDoc(test, RDFTest.inputDocument);
            long t1 = System.currentTimeMillis();
            InfGraph graph = reasoner.bind(input.getGraph());
            boolean correct = graph.validate().isValid();
            long t2 = System.currentTimeMillis();
            lastTestDuration = t2 - t1; 
            return correct;
        } else {
            for (StmtIterator i = test.listProperties(RDF.type); i.hasNext(); ) {
                System.out.println("Test type = " + i.nextStatement().getObject());
            }
            throw new ReasonerException("Unknown test type");
        }
    }
   
    /**
     * Load the premises or conclusions for the test, optional performing
     * import processing.
     */
    public Model getDoc(Resource test, Property docType, boolean processImports) throws IOException {
        if (processImports) {
            Model result = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM, null);
            StmtIterator si = test.listProperties(docType);
            while ( si.hasNext() ) {
                String fname = si.nextStatement().getObject().toString() + ".rdf";
                loadFile(fname, result);
            }
            return result;
        } else {
            return getDoc(test, docType);
        }
    }
   
    /**
     * Load the premises or conclusions for the test.
     */
    public Model getDoc(Resource test, Property docType) throws IOException {
        Model result = ModelFactory.createDefaultModel();
        StmtIterator si = test.listProperties(docType);
        while ( si.hasNext() ) {
            String fname = si.nextStatement().getObject().toString() + ".rdf";
            loadFile(fname, result);
        }
        return result;
    }

    /**
     * Utility to load a file into a model a Model. 
     * Files are assumed to be relative to the BASE_URI.
     * @param file the file name, relative to baseDir
     * @return the loaded Model
     */
    public static Model loadFile(String file, Model model) throws IOException {
        String langType = "RDF/XML";
        if (file.endsWith(".nt")) {
            langType = "N-TRIPLE";
        } else if (file.endsWith("n3")) {
            langType = "N3";
        }
        String fname = file;
        if (fname.startsWith(BASE_URI)) {
            fname = fname.substring(BASE_URI.length());
        }
        Reader reader = new BufferedReader(new FileReader(BASE_TESTDIR + fname));
        model.read(reader, BASE_URI + fname, langType);
        return model;
    }
    
    /**
     * Example the conclusions graph for introduction of restrictions which
     * require a comprehension rewrite and declare new (anon) classes
     * for those restrictions.
     */
    public void comprehensionAxioms(Model premises, Model conclusions) {
        // Comprehend all restriction declarations and note them in a map
        Map<Resource, Resource> comprehension = new HashMap<>();
        StmtIterator ri = conclusions.listStatements(null, RDF.type, OWL.Restriction);
        while (ri.hasNext()) {
            Resource restriction = ri.nextStatement().getSubject();
            StmtIterator pi = restriction.listProperties(OWL.onProperty);
            while (pi.hasNext()) {
                Resource prop = (Resource)pi.nextStatement().getObject();
                StmtIterator vi = restriction.listProperties();
                while (vi.hasNext()) {
                    Statement rs = vi.nextStatement();
                    if ( ! rs.getPredicate().equals(OWL.onProperty)) {
                        // Have a restriction on(prop) of type rs in the conclusions
                        // So assert a premise that such a restriction could exisit
                        Resource comp = premises.createResource()
                            .addProperty(RDF.type, OWL.Restriction)
                            .addProperty(OWL.onProperty, prop)
                            .addProperty(rs.getPredicate(), rs.getObject());
                        comprehension.put(restriction, comp);
                    }
                }
            }
        }
        // Comprehend any intersectionOf lists. Introduce anon class which has the form
        // of the intersection expression.
        // Rewrite queries of the form (X intersectionOf Y) to the form
        //   (X equivalentClass ?CC) (?CC intersectionOf Y)
        StmtIterator ii = conclusions.listStatements(null, OWL.intersectionOf, (RDFNode)null);
        List<Statement> intersections = new ArrayList<>();
        while (ii.hasNext()) { 
            intersections.add(ii.nextStatement());
        }
        for ( Statement is : intersections )
        {
            // Declare in the premises that such an intersection exists
            Resource comp =
                premises.createResource().addProperty( RDF.type, OWL.Class ).addProperty( OWL.intersectionOf,
                                                                                          mapList( premises,
                                                                                                   (Resource) is.getObject(),
                                                                                                   comprehension ) );
            // Rewrite the conclusions to be a test for equivalence between the class being
            // queried and the comprehended interesection
            conclusions.remove( is );
            conclusions.add( is.getSubject(), OWL.equivalentClass, comp );
        }
        // Comprehend any oneOf lists
        StmtIterator io = conclusions.listStatements(null, OWL.oneOf, (RDFNode)null);
        while (io.hasNext()) {
            Statement s = io.nextStatement();
            Resource comp = premises.createResource()
                        .addProperty(OWL.oneOf, s.getObject());
        }
    }

    /**
     * Helper. Adds to the target model a translation of the given RDF list
     * with each element replaced according to the map.
     */
    private Resource mapList(Model target, Resource list, Map<Resource, Resource> map) {
        if (list.equals(RDF.nil)) {
            return RDF.nil;
        } else {
            Resource head = (Resource) list.getRequiredProperty(RDF.first).getObject();
            Resource rest = (Resource) list.getRequiredProperty(RDF.rest).getObject();
            Resource mapElt = target.createResource();
            Resource mapHead = map.get(head);
            if (mapHead == null) mapHead = head;
            mapElt.addProperty(RDF.first, mapHead);
            mapElt.addProperty(RDF.rest, mapList(target, rest, map));
            return mapElt;
        }
    }
    
//  =======================================================================
//  Internal helper functions
    
    /** Return a list of all tests of the given type, according to the current filters */
    public List<Resource> findTestsOfType(Resource testType) {
        ArrayList<Resource> result = new ArrayList<>();
        StmtIterator si = testDefinitions.listStatements(null, RDF.type, testType);
        while (si.hasNext()) {
            Resource test = si.nextStatement().getSubject();
            boolean accept = true;
            // Check test status
            Literal status = (Literal) test.getProperty(RDFTest.status).getObject();
            if (approvedOnly) {
                accept = status.getString().equals(STATUS_FLAGS[0]);
            } else {
                accept = false;
                for ( String STATUS_FLAG : STATUS_FLAGS )
                {
                    if ( status.getString().equals( STATUS_FLAG ) )
                    {
                        accept = true;
                        break;
                    }
                }
            }
            // Check for blocked tests
            for ( String BLOCKED_TEST : BLOCKED_TESTS )
            {
                if ( BLOCKED_TEST.equals( test.toString() ) )
                {
                    accept = false;
                }
            }
            // End of filter tests
            if (accept) {
                result.add(test);
            }
        }
        return result;
    }
}
