/*
 * 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.openjpa.persistence.access;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;

import jakarta.persistence.Query;

import org.apache.openjpa.meta.AccessCode;
import org.apache.openjpa.meta.ClassMetaData;
import org.apache.openjpa.meta.MetaDataRepository;
import org.apache.openjpa.persistence.ArgumentException;
import org.apache.openjpa.persistence.OpenJPAEntityManagerSPI;
import org.apache.openjpa.persistence.test.SingleEMFTestCase;

// -ea -Dopenjpa.ConnectionDriverName=com.mysql.jdbc.Driver -Dopenjpa.ConnectionPassword=openjpatst
// -Dopenjpa.ConnectionURL=jdbc:mysql://localhost:3306/openjpatst -Dopenjpa.ConnectionUserName=openjpatst
public class TestExplicitAccess extends SingleEMFTestCase {

    @Override
    public void setUp() {
        setUp(CLEAR_TABLES,
            "openjpa.jdbc.DBDictionary", "(dateFractionDigits=6)",
            PropAccess.class, FieldAccess.class,
            DefFieldMixedPropAccess.class , DefPropMixedFieldAccess.class,
            AbstractMappedSuperField.class, PropertySub.class,
            MappedSuperField.class, PropertySub2.class,
            SuperFieldEntity.class, PropertySub3.class,
            AbstractMappedSuperProperty.class, FieldSub.class,
            MappedSuperProperty.class, FieldSub2.class,
            SuperPropertyEntity.class, FieldSub3.class,
            MixedMappedSuper.class, MixedFieldSub.class,
            FieldEmbedEntity.class, EmbedFieldAccess.class,
            PropEmbedEntity.class, EmbedPropAccess.class,
            PropMixedEntity.class, EmbedMixedAccess.class,
            MixedNestedEmbedEntity.class, EmbedInnerProp.class,
            EmbedOuterField.class, MixedMultEmbedEntity.class,
            FieldAccessPropStratsEntity.class,
            PropAccessFieldStratsEntity.class,
            EmbedId.class, MenuItem.class, Ingredient.class, Quantity.class);
    }


    /**
     * Validates the use of field level access on an
     * entity, mappedsuperclass, and embeddable at the
     * class level.
     */
    public void testClassSpecifiedFieldAccess() {

        OpenJPAEntityManagerSPI em = emf.createEntityManager();

        FieldAccess fa = new FieldAccess();
        // Set the persistent field through a misnamed setter
        fa.setStringField("FieldAccess");

        em.getTransaction().begin();
        em.persist(fa);
        em.getTransaction().commit();
        em.clear();

        // This value of a persistent field was set using the setter
        // above, but this query will use the field name to verify that
        // field access is in use.
        Query qry = em.createNamedQuery("FieldAccess.query");
        qry.setParameter("id", fa.getId());
        qry.setParameter("strVal", "FieldAccess");
        FieldAccess fa2 = (FieldAccess)qry.getSingleResult();
        assertEquals(fa.getId(), fa2.getId());

        em.close();
    }

    /**
     * Validates the use of property level access on an
     * entity, mappedsuperclass, and embeddable at the
     * class level.
     */
    public void testClassSpecifiedPropertyAccess() {

        OpenJPAEntityManagerSPI em = emf.createEntityManager();

        PropAccess pa = new PropAccess();
        // Set the persistent field through a misnamed setter
        pa.setStrProp("PropertyAccess");

        em.getTransaction().begin();
        em.persist(pa);
        em.getTransaction().commit();
        em.clear();

        // This value of a persistent field was set using the setter
        // above, but this query will use the field name to verify that
        // field access is in use.
        Query qry = em.createNamedQuery("PropertyAccess.query");
        qry.setParameter("id", pa.getId());
        qry.setParameter("strVal", "PropertyAccess");
        PropAccess pa2 = (PropAccess)qry.getSingleResult();
        assertEquals(pa, pa2);
        em.close();
    }

    /**
     * Validates the use of explicit field access on an entity,
     * mappedsuperclass, and embeddable with property access
     * defined at the class level and field access defined
     * on specific methods.
     */
    public void testClassSpecifiedMixedSinglePCFieldAccess() {

        OpenJPAEntityManagerSPI em = emf.createEntityManager();

        DefFieldMixedPropAccess dfmpa = new DefFieldMixedPropAccess();
        // Call non-PC setter
        dfmpa.setStrField("NonPCSetter");
        // Call setter with property access
        dfmpa.setStringField("DFMPA");

        em.getTransaction().begin();
        em.persist(dfmpa);
        em.getTransaction().commit();
        em.clear();

        // This value of a persistent property was set using the setter
        // above, but this query will use the property name to verify that
        // propety access is in use.
        Query qry = em.createNamedQuery("DFMPA.query");
        qry.setParameter("id", dfmpa.getId());
        qry.setParameter("strVal", "DFMPA");
        DefFieldMixedPropAccess dfmpa2 =
            (DefFieldMixedPropAccess)qry.getSingleResult();
        assertEquals(dfmpa, dfmpa2);
        assertEquals(dfmpa2.getStringField(), "DFMPA");

        try {
            qry = em.createNamedQuery("DFMPA.badQuery");
            qry.setParameter("id", dfmpa.getId());
            qry.setParameter("strVal", "DFMPA");
            qry.getSingleResult();
            fail("Usage of this query should have thrown an exception");
        }
        catch (Exception e) {
            assertExceptionMessage(e, ArgumentException.class,
                "No field named \"strField\" in \"DefFieldMixedPropAccess\"",
                "[id, stringField, version]");
        }

        em.close();
    }

    /**
     * Validates the use of explicit property access on an entity,
     * mappedsuperclass, and embeddable with field access
     * defined at the class level and property access defined
     * on specific methods.
     */
    public void testClassSpecifiedMixedSinglePCPropertyAccess() {

        OpenJPAEntityManagerSPI em = emf.createEntityManager();

        DefPropMixedFieldAccess dpmfa = new DefPropMixedFieldAccess();
        // Call setter with underlying field access
        dpmfa.setStrProp("DPMFA");

        em.getTransaction().begin();
        em.persist(dpmfa);
        em.getTransaction().commit();
        em.clear();

        // This value of a persistent field was set using the setter
        // above, but this query will use the property name to verify that
        // propety access is in use.
        Query qry = em.createNamedQuery("DPMFA.query");
        qry.setParameter("id", dpmfa.getId());
        qry.setParameter("strVal", "DPMFA");
        DefPropMixedFieldAccess dpmfa2 =
            (DefPropMixedFieldAccess)qry.getSingleResult();
        assertEquals(dpmfa, dpmfa2);
        assertEquals(dpmfa2.getStrProp(), "DPMFA");

        try {
            qry = em.createNamedQuery("DPMFA.badQuery");
            qry.setParameter("id", dpmfa.getId());
            qry.setParameter("strVal", "DPMFA");
            qry.getSingleResult();
            fail("Usage of this query should have thrown an exception");
        }
        catch (Exception e) {
            assertExceptionMessage(e, ArgumentException.class,
                "No field named \"strProp\" in \"DefPropMixedFieldAccess\"",
                "[id, strField, version]");
        }

        em.close();
    }

    /**
     * Validates that a mapped superclass using field access and an entity
     * subclass using property access get mapped properly.
     */
    public void testAbstractMappedSuperField() {
        OpenJPAEntityManagerSPI em = emf.createEntityManager();

        PropertySub ps = new PropertySub();
        // Call super setter with underlying field access
        ps.setName("AbsMappedSuperName");
        // Call base setter with property access
        Date now = new Date();
        ps.setCreateDate(now);

        em.getTransaction().begin();
        em.persist(ps);
        em.getTransaction().commit();
        em.clear();

        // This value of a persistent field was set using the setter
        // above, but this query will use the property name to verify that
        // propety access is in use.
        Query qry = em.createNamedQuery("PropertySub.query");
        qry.setParameter("id", ps.getId());
        qry.setParameter("name", "AbsMappedSuperName");
        qry.setParameter("crtDate", now);
        PropertySub ps2 =
            (PropertySub)qry.getSingleResult();
        assertEquals(ps, ps2);
        assertEquals(ps2.getName(), "AbsMappedSuperName");
        assertEquals(ps2.getCreateDate().toString(), now.toString());

        try {
            qry = em.createNamedQuery("PropertySub.badQuery");
            qry.setParameter("id", ps.getId());
            qry.setParameter("name", "AbsMappedSuperName");
            qry.setParameter("crtDate", now);
            qry.getSingleResult();
            fail("Usage of this query should have thrown an exception");
        }
        catch (Exception e) {
            assertExceptionMessage(e, ArgumentException.class,
                "No field named \"crtDate\" in \"PropertySub\"",
                "[createDate, id, name]");
        }

        em.close();
    }

    /**
     * Validates that a mapped superclass using property access and an entity
     * subclass using field access get mapped properly.
     */
    public void testAbstractMappedSuperProperty() {
        OpenJPAEntityManagerSPI em = emf.createEntityManager();

        FieldSub fs = new FieldSub();
        // Call super setter with underlying field access
        fs.setName("AbsMappedSuperName");
        // Call base setter with property access
        Date now = new Date();
        fs.setCreateDate(now);

        em.getTransaction().begin();
        em.persist(fs);
        em.getTransaction().commit();
        em.clear();

        // This value of a persistent field was set using the setter
        // above, but this query will use the property name to verify that
        // propety access is in use.
        Query qry = em.createNamedQuery("FieldSub.query");
        qry.setParameter("id", fs.getId());
        qry.setParameter("name", "AbsMappedSuperName");
        qry.setParameter("crtDate", now);
        FieldSub fs2 =
            (FieldSub)qry.getSingleResult();
        assertEquals(fs, fs2);
        assertEquals(fs2.getName(), "AbsMappedSuperName");
        assertEquals(fs2.getCreateDate().toString(), now.toString());

        try {
            qry = em.createNamedQuery("FieldSub.badQuery");
            qry.setParameter("id", fs.getId());
            qry.setParameter("name", "AbsMappedSuperName");
            qry.setParameter("crtDate", now);
            qry.getSingleResult();
            fail("Usage of this query should have thrown an exception");
        }
        catch (Exception e) {
            assertExceptionMessage(e, ArgumentException.class,
                "No field named \"createDate\" in \"FieldSub\"",
                "[crtDate, id, name]");
        }

        em.close();
    }

    /**
     * Validates that an mapped superclass using field access and an
     * entity subclass using property access get mapped properly.
     * The subclass uses a storage field in the superclass.
     */
    public void testMappedSuperField() {
        OpenJPAEntityManagerSPI em = emf.createEntityManager();

        PropertySub2 ps = new PropertySub2();
        // Call super setter with underlying field access
        ps.setName("MappedSuperName");
        // Call base setter with property access
        Date now = new Date();
        ps.setCreateDate(now);

        em.getTransaction().begin();
        em.persist(ps);
        em.getTransaction().commit();
        em.clear();

        // This value of a persistent field was set using the setter
        // above, but this query will use the property name to verify that
        // propety access is in use.
        Query qry = em.createNamedQuery("PropertySub2.query");
        qry.setParameter("id", ps.getId());
        qry.setParameter("name", "MappedSuperName");
        qry.setParameter("crtDate", now);
        PropertySub2 ps2 =
            (PropertySub2)qry.getSingleResult();
        assertEquals(ps, ps2);
        assertEquals(ps2.getName(), "MappedSuperName");
        assertEquals(ps2.getCreateDate().toString(), now.toString());

        try {
            qry = em.createNamedQuery("PropertySub2.badQuery");
            qry.setParameter("id", ps.getId());
            qry.setParameter("name", "MappedSuperName");
            qry.setParameter("crtDate", now);
            qry.getSingleResult();
            fail("Usage of this query should have thrown an exception");
        }
        catch (Exception e) {
            assertExceptionMessage(e, ArgumentException.class,
                    "No field named \"crtDate\" in \"PropertySub2\"",
                    "[createDate, id, name]");
        }

        em.close();
    }

    /**
     * Validates that an mapped superclass using field access and an
     * entity subclass using property access get mapped properly.
     * The subclass uses a storage field in the superclass.
     */
    public void testMappedSuperProperty() {
        OpenJPAEntityManagerSPI em = emf.createEntityManager();

        FieldSub2 fs = new FieldSub2();
        // Call super setter with underlying field access
        fs.setName("MappedSuperName");
        // Call base setter with property access
        Date now = new Date();
        fs.setCreateDate(now);

        em.getTransaction().begin();
        em.persist(fs);
        em.getTransaction().commit();
        em.clear();

        // This value of a persistent field was set using the setter
        // above, but this query will use the property name to verify that
        // propety access is in use.
        Query qry = em.createNamedQuery("FieldSub2.query");
        qry.setParameter("id", fs.getId());
        qry.setParameter("name", "MappedSuperName");
        qry.setParameter("crtDate", now);
        FieldSub2 fs2 =
            (FieldSub2)qry.getSingleResult();
        assertEquals(fs, fs2);
        assertEquals(fs2.getName(), "MappedSuperName");
        assertEquals(fs2.getCreateDate().toString(), now.toString());

        try {
            qry = em.createNamedQuery("FieldSub2.badQuery");
            qry.setParameter("id", fs.getId());
            qry.setParameter("name", "MappedSuperName");
            qry.setParameter("crtDate", now);
            qry.getSingleResult();
            fail("Usage of this query should have thrown an exception");
        }
        catch (Exception e) {
            assertExceptionMessage(e, ArgumentException.class,
                    "No field named \"createDate\" in \"FieldSub2\"",
                    "[crtDate, id, name]");
        }

        em.close();
    }

    /**
     * Validates that a mix of access types can be used within multiple
     * persistent classes within an inheritance hierarchy that uses
     * MappedSuperclass.
     */
    public void testMixedMappedSuper() {
        OpenJPAEntityManagerSPI em = emf.createEntityManager();

        MixedFieldSub fs = new MixedFieldSub();
        // Call super setter with underlying field access
        fs.setName("MixedMappedSuperName");
        fs.setMyFieldProp("MyFieldName");
        // Call base setter with property access
        Date now = new Date();
        fs.setCreateDate(now);

        em.getTransaction().begin();
        em.persist(fs);
        em.getTransaction().commit();
        em.clear();

        // This value of a persistent field was set using the setter
        // above, but this query will use the property name to verify that
        // propety access is in use.
        Query qry = em.createNamedQuery("MixedFieldSub.query");
        qry.setParameter("id", fs.getId());
        qry.setParameter("name", "MixedMappedSuperName");
        qry.setParameter("crtDate", now);
        qry.setParameter("myField", "MyFieldName");
        MixedFieldSub fs2 =
            (MixedFieldSub)qry.getSingleResult();
        assertEquals(fs, fs2);
        assertEquals(fs2.getName(), "MixedMappedSuperName");
        assertEquals(fs2.getCreateDate().toString(), now.toString());

        try {
            qry = em.createNamedQuery("MixedFieldSub.badQuery");
            qry.setParameter("id", fs.getId());
            qry.setParameter("name", "MixedMappedSuperName");
            qry.setParameter("myField", "MyFieldName");
            qry.getSingleResult();
            fail("Usage of this query should have thrown an exception");
        }
        catch (Exception e) {
            assertExceptionMessage(e, ArgumentException.class,
                    "No field named \"myFieldProp\" in \"MixedFieldSub\"",
                    "[createDate, mid, myField, name]");
        }

        em.close();

    }

    /**
     * Validates that a mix of access types can be used within
     * an inheritance hierarchy which uses default Entity inheritance.
     * NOTE: be sure to test with all forms of inheritance.
     */
    public void testEntityFieldDefaultInheritance() {
        OpenJPAEntityManagerSPI em = emf.createEntityManager();

        FieldSub3 fs = new FieldSub3();
        // Call super setter with underlying field access
        fs.setName("EntitySuperName");
        // Call base setter with property access
        Date now = new Date();

        fs.setCreateDate(now);

        SuperPropertyEntity spe = new SuperPropertyEntity();
        spe.setName("SuperPropertyEntity");

        em.getTransaction().begin();
        em.persist(fs);
        em.persist(spe);
        em.getTransaction().commit();
        em.clear();

        // This value of a persistent field was set using the setter
        // above, but this query will use the property name to verify that
        // propety access is in use.
        Query qry = em.createNamedQuery("FieldSub3.query");
        qry.setParameter("id", fs.getId());
        qry.setParameter("name", "EntitySuperName");
        qry.setParameter("crtDate", now);
        FieldSub3 fs2 =
            (FieldSub3)qry.getSingleResult();
        assertEquals(fs, fs2);
        assertEquals(fs2.getName(), "EntitySuperName");
        assertEquals(fs2.getCreateDate().toString(), now.toString());

        try {
            qry = em.createNamedQuery("FieldSub3.badQuery");
            qry.setParameter("id", fs.getId());
            qry.setParameter("name", "EntitySuperName");
            qry.setParameter("crtDate", now);
            qry.getSingleResult();
            fail("Usage of this query should have thrown an exception");
        }
        catch (Exception e) {
            assertExceptionMessage(e, ArgumentException.class,
                    "No field named \"createDate\" in \"FieldSub3\"",
                    "[crtDate, id, name]");
        }

        qry = em.createNamedQuery("SuperPropertyEntity.query");
        qry.setParameter("id", spe.getId());
        qry.setParameter("name", "SuperPropertyEntity");
        SuperPropertyEntity spe2 =
            (SuperPropertyEntity)qry.getSingleResult();
        assertEquals(spe, spe2);
        assertEquals(spe2.getName(), "SuperPropertyEntity");

        try {
            // This query ensures that a subclass property didn't somehow get
            // picked up by the superclass while building field metadata using
            // explicit access.
            qry = em.createNamedQuery("SuperPropertyEntity.badQuery");
            qry.setParameter("id", spe.getId());
            qry.setParameter("name", "SuperPropertyEntity");
            qry.setParameter("crtDate", now);
            qry.getSingleResult();
            fail("Usage of this query should have thrown an exception");
        }
        catch (Exception e) {
            assertExceptionMessage(e, ArgumentException.class,
                    "No field named \"crtDate\" in \"SuperPropertyEntity\"",
                    "[id, name]");
        }

        em.close();
    }

    /**
     * Validates that a mix of access types can be used within
     * an inheritance hierarchy which uses default Entity inheritance.
     * NOTE: be sure to test with all forms of inheritance.
     */
    public void testEntityPropertyDefaultInheritance() {
        OpenJPAEntityManagerSPI em = emf.createEntityManager();

        PropertySub3 ps = new PropertySub3();
        // Call super setter with underlying field access
        ps.setName("EntitySuperName");
        // Call base setter with property access
        Date now = new Date();
        ps.setCreateDate(now);

        SuperFieldEntity sfe = new SuperFieldEntity();
        sfe.setName("SuperFieldEntity");

        em.getTransaction().begin();
        em.persist(ps);
        em.persist(sfe);
        em.getTransaction().commit();
        em.clear();

        // This value of a persistent field was set using the setter
        // above, but this query will use the property name to verify that
        // propety access is in use.
        Query qry = em.createNamedQuery("PropertySub3.query");
        qry.setParameter("id", ps.getId());
        qry.setParameter("name", "EntitySuperName");
        qry.setParameter("crtDate", now);
        PropertySub3 ps2 =
            (PropertySub3)qry.getSingleResult();
        assertEquals(ps, ps2);
        assertEquals(ps2.getName(), "EntitySuperName");
        assertEquals(ps2.getCreateDate().toString(), now.toString());

        try {
            qry = em.createNamedQuery("PropertySub3.badQuery");
            qry.setParameter("id", ps.getId());
            qry.setParameter("name", "EntitySuperName");
            qry.setParameter("crtDate", now);
            qry.getSingleResult();
            fail("Usage of this query should have thrown an exception");
        }
        catch (Exception e) {
            assertExceptionMessage(e, ArgumentException.class,
                    "No field named \"crtDate\" in \"PropertySub3\"",
                    "[createDate, id, name]");
        }

        qry = em.createNamedQuery("SuperFieldEntity.query");
        qry.setParameter("id", sfe.getId());
        qry.setParameter("name", "SuperFieldEntity");
        SuperFieldEntity sfe2 =
            (SuperFieldEntity)qry.getSingleResult();
        assertEquals(sfe2, sfe2);
        assertEquals(sfe2.getName(), "SuperFieldEntity");

        try {
            // This query ensures that a subclass property didn't somehow get
            // picked up by the superclass while building field metadata using
            // explicit access.
            qry = em.createNamedQuery("SuperFieldEntity.badQuery");
            qry.setParameter("id", sfe.getId());
            qry.setParameter("name", "SuperFieldEntity");
            qry.setParameter("crtDate", now);
            qry.getSingleResult();
            fail("Usage of this query should have thrown an exception");
        }
        catch (Exception e) {
            assertExceptionMessage(e, ArgumentException.class,
                    "No field named \"crtDate\" in \"SuperFieldEntity\"",
                    "[id, name]");
        }
        em.close();
    }

    /**
     * Validates an embeddable with field access can be used within an
     * entity with property access
     */
    public void testEmbeddablesField() {
        OpenJPAEntityManagerSPI em = emf.createEntityManager();

        EmbedFieldAccess efa = new EmbedFieldAccess();
        efa.setFirstName("J");
        efa.setLastName("Tolkien");

        PropEmbedEntity pe = new PropEmbedEntity();
        pe.setName("PropEmbedEntity");
        pe.setEmbedProp(efa);

        em.getTransaction().begin();
        em.persist(pe);
        em.getTransaction().commit();

        em.clear();

        Query qry = em.createNamedQuery("PropEmbedEntity.query");
        qry.setParameter("id", pe.getId());
        qry.setParameter("name", "PropEmbedEntity");
        qry.setParameter("firstName", "J");
        qry.setParameter("lastName", "Tolkien");
        PropEmbedEntity pe2 = (PropEmbedEntity)qry.getSingleResult();
        assertEquals(pe, pe2);
        assertEquals(efa, pe2.getEmbedProp());

        try {
            qry = em.createNamedQuery("PropEmbedEntity.badQuery");
            qry.setParameter("id", pe.getId());
            qry.setParameter("name", "PropEmbedEntity");
            qry.setParameter("firstName", "J");
            qry.setParameter("lastName", "Tolkien");
            qry.getSingleResult();
            fail("Query execution should have failed.");
        } catch (Exception e) {
            assertExceptionMessage(e, ArgumentException.class,
                    "No field named \"firstName\" in \"EmbedFieldAccess\"",
                    "[fName, lName]");
        }

        em.close();
    }

    /**
     * Validates an embeddable with property access can be used within an
     * entity with field access
     */
    public void testEmbeddablesProperty() {
        OpenJPAEntityManagerSPI em = emf.createEntityManager();

        EmbedPropAccess epa = new EmbedPropAccess();
        epa.setFirstName("Walt");
        epa.setLastName("Whitman");

        FieldEmbedEntity fe = new FieldEmbedEntity();
        fe.setName("FieldEmbedEntity");
        fe.setEPA(epa);

        em.getTransaction().begin();
        em.persist(fe);
        em.getTransaction().commit();

        em.clear();

        Query qry = em.createNamedQuery("FieldEmbedEntity.query");
        qry.setParameter("id", fe.getId());
        qry.setParameter("name", "FieldEmbedEntity");
        qry.setParameter("firstName", "Walt");
        qry.setParameter("lastName", "Whitman");
        FieldEmbedEntity fe2 = (FieldEmbedEntity)qry.getSingleResult();
        assertEquals(fe, fe2);
        assertEquals(epa, fe2.getEPA());

        try {
            qry = em.createNamedQuery("FieldEmbedEntity.badQuery");
            qry.setParameter("id", fe.getId());
            qry.setParameter("name", "FieldEmbedEntity");
            qry.setParameter("firstName", "Walt");
            qry.setParameter("lastName", "Whitman");
            qry.getSingleResult();
            fail("Query execution should have failed.");
        } catch (Exception e) {
            assertExceptionMessage(e, ArgumentException.class,
                    "No field named \"fName\" in \"EmbedPropAccess\"",
                    "[firstName, lastName]");
        }

        em.close();
    }

    /**
     * Validates an embeddable with mixed access can be used within an
     * entity with mixed access
     */
    public void testMixedEmbeddables() {
        OpenJPAEntityManagerSPI em = emf.createEntityManager();

        EmbedMixedAccess ema = new EmbedMixedAccess();
        ema.setFirstName("J");
        ema.setLastName("Tolkien");
        ema.setMiddleName("R");

        PropMixedEntity pm = new PropMixedEntity();
        pm.setName("PropMixedEntity");
        pm.setEmbedProp(ema);

        em.getTransaction().begin();
        em.persist(pm);
        em.getTransaction().commit();

        em.clear();

        Query qry = em.createNamedQuery("PropMixedEntity.query");
        qry.setParameter("id", pm.getId());
        qry.setParameter("name", "PropMixedEntity");
        qry.setParameter("firstName", "J");
        qry.setParameter("lastName", "Tolkien");
        qry.setParameter("middleName", "R");
        PropMixedEntity pm2 = (PropMixedEntity)qry.getSingleResult();
        assertEquals(pm, pm2);
        assertEquals(ema, pm2.getEmbedProp());

        try {
            qry = em.createNamedQuery("PropMixedEntity.badQuery");
            qry.setParameter("id", pm.getId());
            qry.setParameter("name", "PropMixedEntity");
            qry.setParameter("firstName", "J");
            qry.setParameter("lastName", "Tolkien");
            qry.setParameter("middleName", "R");
            qry.getSingleResult();
            fail("Query execution should have failed.");
        } catch (Exception e) {
            assertExceptionMessage(e, ArgumentException.class,
                "No field named \"middleName\" in \"EmbedMixedAccess\"",
                "[firstName, lastName, mName]");
        }

        em.close();
    }

    /**
     * Validates that a mix of access types can be used within
     * an embeddable stack.
     */
    public void testNestedEmbeddables() {
        OpenJPAEntityManagerSPI em = emf.createEntityManager();

        EmbedInnerProp eip = new EmbedInnerProp();
        eip.setInnerName("Inner");
        EmbedOuterField eof = new EmbedOuterField();
        eof.setOuterName("Outer");
        eip.setOuterField(eof);

        MixedNestedEmbedEntity pm = new MixedNestedEmbedEntity();
        pm.setName("MixedNestedEmbedEntity");
        pm.setEmbedProp(eip);

        em.getTransaction().begin();
        em.persist(pm);
        em.getTransaction().commit();

        em.clear();

        Query qry = em.createNamedQuery("MixedNestedEmbedEntity.query");
        qry.setParameter("id", pm.getId());
        qry.setParameter("name", "MixedNestedEmbedEntity");
        qry.setParameter("innerName", "Inner");
        qry.setParameter("outerName", "Outer");
        MixedNestedEmbedEntity pm2 =
            (MixedNestedEmbedEntity)qry.getSingleResult();
        assertEquals(pm, pm2);
        assertEquals(eip, pm2.getEmbedProp());

        try {
            qry = em.createNamedQuery("MixedNestedEmbedEntity.badQuery");
            qry.setParameter("id", pm.getId());
            qry.setParameter("name", "MixedNestedEmbedEntity");
            qry.setParameter("innerName", "Inner");
            qry.setParameter("outerName", "Outer");
            qry.getSingleResult();
            fail("Query execution should have failed.");
        } catch (Exception e) {
            assertExceptionMessage(e, ArgumentException.class,
                "No field named \"outerName\" in \"EmbedOuterField\"",
                "[outName]");
        }

        em.close();
    }

    /**
     * Validates that a mix of access types can be used by an
     * an entity with mulitple embeddables.
     */
    public void testMultipleEmbeddables() {
        OpenJPAEntityManagerSPI em = emf.createEntityManager();

        EmbedFieldAccess efa = new EmbedFieldAccess();
        efa.setFirstName("First");
        efa.setLastName("Last");

        EmbedPropAccess epa = new EmbedPropAccess();
        epa.setFirstName("fname");
        epa.setLastName("lname");

        MixedMultEmbedEntity pm = new MixedMultEmbedEntity();
        pm.setName("MixedMultEmbedEntity");
        pm.setEmbedProp(epa);
        pm.setEmbedField(efa);

        em.getTransaction().begin();
        em.persist(pm);
        em.getTransaction().commit();

        em.clear();

        Query qry = em.createNamedQuery("MixedMultEmbedEntity.query");
        qry.setParameter("id", pm.getId());
        qry.setParameter("name", "MixedMultEmbedEntity");
        qry.setParameter("firstName", "fname");
        qry.setParameter("lastName", "lname");
        qry.setParameter("fName", "First");
        qry.setParameter("lName", "Last");
        MixedMultEmbedEntity pm2 =
            (MixedMultEmbedEntity)qry.getSingleResult();
        assertEquals(pm, pm2);
        assertEquals(epa, pm2.getEmbedProp());
        assertEquals(efa, pm2.getEmbedField());

        try {
            qry = em.createNamedQuery("MixedMultEmbedEntity.badQuery1");
            qry.setParameter("id", pm.getId());
            qry.setParameter("name", "MixedMultEmbedEntity");
            qry.setParameter("epa", epa);
            qry.getSingleResult();
            fail("Query execution should have failed.");
        } catch (Exception e) {
            assertExceptionMessage(e, ArgumentException.class,
                "No field named \"epa\" in \"MixedMultEmbedEntity\"",
                "[embedField, embedProp, mid, name]");
        }

        try {
            qry = em.createNamedQuery("MixedMultEmbedEntity.badQuery2");
            qry.setParameter("id", pm.getId());
            qry.setParameter("name", "MixedMultEmbedEntity");
            qry.setParameter("epa", epa);
            qry.setParameter("firstName", "First");
            qry.setParameter("lastName", "Last");
            qry.getSingleResult();
            fail("Query execution should have failed.");
        } catch (Exception e) {
            // no support: conditional expressional expression over embeddable
            assertException(e, ArgumentException.class);
        }

        em.close();
    }

    /**
     * Validates explicit property access can be applied to all the access
     * strategies from within an entity with explicit field access (except for
     * the id field, which is field default)
     */
    public void testPropertyAccessStrategies() {
        OpenJPAEntityManagerSPI em = emf.createEntityManager();

        PropAccessFieldStratsEntity pa = new PropAccessFieldStratsEntity();

        // Load all persistent fields
        EmbedId eid = new EmbedId();
        eid.setId(new Random().nextInt());
        eid.setCode("IdCode");
        pa.setEmbedId(eid); // embedded id

        Collection<EmbedPropAccess> elc = new ArrayList<>();
        EmbedPropAccess epa1 = new EmbedPropAccess("Abe", "Lincoln");
        EmbedPropAccess epa2 = new EmbedPropAccess("John", "Kennedy");
        elc.add(epa1);
        elc.add(epa2);
        pa.setElementCollection(elc); // element collection of embeddables

        EmbedFieldAccess efa = new EmbedFieldAccess();
        efa.setFirstName("The");
        efa.setLastName("President");
        pa.setEmbedField(efa); // embedded

        pa.setName("PropAccessFieldStratsEntity");  // basic

        PropAccess propa = new PropAccess();
        propa.setStrProp("PropAccess");
        pa.setManyToOne(propa); // many to one

        Collection<FieldAccess> fac = new ArrayList<>();
        FieldAccess fa = new FieldAccess();
        fa.setStrField("FieldAccess");
        fac.add(fa);
        pa.setOneToMany(fac); // one to many

        PropAccess pa2 = new PropAccess();
        pa2.setStrProp("PropAccess2");
        pa.setOneToOne(pa2); // one to one

        em.getTransaction().begin();
        em.persist(pa);
        em.getTransaction().commit();

        em.clear();
        // Verify list of persistent fields
        PropAccessFieldStratsEntity newpa =
            em.find(PropAccessFieldStratsEntity.class, eid);
        assertNotNull(newpa);
        // simple key validation
        assertEquals(newpa.getEmbedId(), eid);

        // Verify the persistent member names
        MetaDataRepository mdr =
            em.getConfiguration().getMetaDataRepositoryInstance();

        ClassMetaData cmd = mdr.getMetaData(PropAccessFieldStratsEntity.class,
            null, true);
        // Assert expected persistent fields and properties were created
        assertNotNull(cmd.getField("embedId"));
        assertNotNull(cmd.getField("m2one"));
        assertNotNull(cmd.getField("one2m"));
        assertNotNull(cmd.getField("one2one"));
        assertNotNull(cmd.getField("ecoll"));
        assertNotNull(cmd.getField("embed"));
        assertNotNull(cmd.getField("ver"));
        assertNotNull(cmd.getField("m2m"));

        // Name has a matching getter/setter.  Make sure the access type
        // is field & not property
        assertNotNull(cmd.getField("name"));
        assertTrue((cmd.getField("name").getAccessType() & AccessCode.FIELD) ==
            AccessCode.FIELD);

        // Assert mappings were not created for fields or properties which
        // should not be persistent
        assertNull(cmd.getField("eid"));
        assertNull(cmd.getField("elementCollection"));
        assertNull(cmd.getField("embedField"));
        assertNull(cmd.getField("version"));
        assertNull(cmd.getField("manyToOne"));
        assertNull(cmd.getField("oneToMany"));
        assertNull(cmd.getField("oneToOne"));
        assertNull(cmd.getField("manyToMany"));

        em.close();
    }

    /**
     * Validates explicit field access can be applied to all the access
     * strategies from within an entity with explicit property access (except
     * for the id field, which is property default)
     */
    public void testFieldAccessStrategies() {
        OpenJPAEntityManagerSPI em = emf.createEntityManager();

        FieldAccessPropStratsEntity fa = new FieldAccessPropStratsEntity();

        // Load all persistent fields
        EmbedId eid = new EmbedId();
        eid.setId(new Random().nextInt());
        eid.setCode("IdCode");
        fa.setEmbedId(eid); // embedded id

        Collection<EmbedPropAccess> elc = new ArrayList<>();
        EmbedPropAccess epa1 = new EmbedPropAccess("George", "Washington");
        EmbedPropAccess epa2 = new EmbedPropAccess("James", "Carter");
        elc.add(epa1);
        elc.add(epa2);
        fa.setElementCollection(elc); // element collection of embeddables

        EmbedFieldAccess efa = new EmbedFieldAccess();
        efa.setFirstName("The");
        efa.setLastName("President");
        fa.setEmbedField(efa); // embedded

        fa.setName("FieldAccessPropStratsEntity");  // basic

        PropAccess propa = new PropAccess();
        propa.setStrProp("PropAccess");
        fa.setManyToOne(propa); // many to one

        Collection<FieldAccess> fac = new ArrayList<>();
        FieldAccess fae = new FieldAccess();
        fae.setStrField("FieldAccess");
        fac.add(fae);
        fa.setOneToMany(fac); // one to many

        PropAccess pa = new PropAccess();
        pa.setStrProp("PropAccess");
        fa.setOneToOne(pa); // one to one

        em.getTransaction().begin();
        em.persist(fa);
        em.getTransaction().commit();

        em.clear();
        // Verify list of persistent fields
        FieldAccessPropStratsEntity newpa =
            em.find(FieldAccessPropStratsEntity.class, eid);
        assertNotNull(newpa);
        // simple key validation
        assertEquals(newpa.getEmbedId(), eid);

        // Verify the persistent member names
        MetaDataRepository mdr =
            em.getConfiguration().getMetaDataRepositoryInstance();

        ClassMetaData cmd = mdr.getMetaData(FieldAccessPropStratsEntity.class,
            null, true);
        // Assert expected persistent fields and properties were created
        assertNotNull(cmd.getField("eid"));
        assertNotNull(cmd.getField("elementCollection"));
        assertNotNull(cmd.getField("embedField"));
        assertNotNull(cmd.getField("version"));
        assertNotNull(cmd.getField("manyToOne"));
        assertNotNull(cmd.getField("oneToMany"));
        assertNotNull(cmd.getField("oneToOne"));
        assertNotNull(cmd.getField("manyToMany"));

        // Name has a matching getter/setter.  Make sure the access type
        // is property & not field
        assertNotNull(cmd.getField("name"));
        assertTrue((cmd.getField("name").getAccessType() & AccessCode.PROPERTY)
            == AccessCode.PROPERTY);

        // Assert mappings were not created for fields or properties which
        // should not be persistent
        assertNull(cmd.getField("embedId"));
        assertNull(cmd.getField("m2one"));
        assertNull(cmd.getField("one2m"));
        assertNull(cmd.getField("one2one"));
        assertNull(cmd.getField("ecoll"));
        assertNull(cmd.getField("embed"));
        assertNull(cmd.getField("ver"));
        assertNull(cmd.getField("m2m"));

        em.close();
    }

    /**
     * Verifies the use of a map of embeddables containing a nested
     * mixed access embeddable.
     */
    public void testMapWithNestedEmbeddable() {
        OpenJPAEntityManagerSPI em = emf.createEntityManager();

        MenuItem mi = new MenuItem();
        mi.setName("PB & J Sandwich");

        Map<String, Ingredient> ingredients = new HashMap<>();
        mi.setIngredients(ingredients);

        Ingredient i1 = new Ingredient("Peanut Butter");
        i1.setDescription("Edible brown paste, made from peanuts");
        Quantity q1 = new Quantity(1.0, "Tbsp");
        i1.setQuantity(q1);
        ingredients.put("Peanut Butter", i1);

        Ingredient i2 = new Ingredient("Jelly");
        i2.setDescription("Sweet gel, made from fruit");
        Quantity q2 = new Quantity(1.5, "Tbsp");
        i2.setQuantity(q2);
        ingredients.put("Jelly", i2);

        Ingredient i3 = new Ingredient("Bread");
        i3.setDescription("Baked material, made from flour and water");
        Quantity q3 = new Quantity(2.0, "Slice");
        i3.setQuantity(q3);
        ingredients.put("Bread", i3);

        em.getTransaction().begin();
        em.persist(mi);
        em.getTransaction().commit();
        em.clear();

        MenuItem mi2 = em.find(MenuItem.class, mi.getId());

        assertEquals(mi2.getId(), mi.getId());
        Map<String, Ingredient> ing2 = mi2.getIngredients();
        assertTrue(ing2.containsKey("Peanut Butter"));
        Quantity q = ing2.get("Peanut Butter").getQuantity();
        assertNotNull(q);
        assertEquals(1.0d, q.getAmount(), 0.001d);
        assertEquals("Tbsp", q.getUnitOfMeasure());
        assertTrue(ing2.containsKey("Jelly"));
        q = ing2.get("Jelly").getQuantity();
        assertNotNull(q);
        assertEquals(1.5d, q.getAmount(), 0.001d);
        assertEquals("Tbsp", q.getUnitOfMeasure());
        assertTrue(ing2.containsKey("Bread"));
        q = ing2.get("Bread").getQuantity();
        assertNotNull(q);
        assertEquals(2.0d, q.getAmount(), 0.001d);
        assertEquals("Slice", q.getUnitOfMeasure());

        em.remove(mi2);

        em.close();
    }

    /*
     * Simple method to verify if an exception is of the correct type and
     * that it contains the expected message fragments.
     */
    private boolean verifyException(Exception e, Class c, String...strings) {
        if (c.isInstance(e)) {
            String exMessage = e.getMessage();
            for (String msg : strings) {
                if (!exMessage.contains(msg))
                    return false;
            }
            return true;
        }
        return false;
    }
}
