/******************************************************************************* | |
* Copyright (c) 2005, 2009 SAP. All rights reserved. | |
* This program and the accompanying materials are made available under the | |
* terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0 | |
* which accompanies this distribution. | |
* The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html | |
* and the Eclipse Distribution License is available at | |
* http://www.eclipse.org/org/documents/edl-v10.php. | |
* | |
* Contributors: | |
* SAP - initial API and implementation | |
******************************************************************************/ | |
package org.eclipse.persistence.testing.tests.wdf.jpa1.query; | |
import java.util.ArrayList; | |
import java.util.Date; | |
import java.util.HashMap; | |
import java.util.HashSet; | |
import java.util.Iterator; | |
import java.util.List; | |
import java.util.Map; | |
import java.util.Random; | |
import java.util.Set; | |
import javax.persistence.EntityManager; | |
import javax.persistence.Query; | |
import org.eclipse.persistence.testing.framework.wdf.JPAEnvironment; | |
import org.eclipse.persistence.testing.framework.wdf.ToBeInvestigated; | |
import org.eclipse.persistence.testing.models.wdf.jpa1.employee.Bicycle; | |
import org.eclipse.persistence.testing.models.wdf.jpa1.employee.Department; | |
import org.eclipse.persistence.testing.models.wdf.jpa1.employee.Employee; | |
import org.eclipse.persistence.testing.models.wdf.jpa1.employee.EmploymentPeriod; | |
import org.eclipse.persistence.testing.models.wdf.jpa1.employee.Hobby; | |
import org.eclipse.persistence.testing.models.wdf.jpa1.employee.MotorVehicle; | |
import org.eclipse.persistence.testing.tests.wdf.jpa1.JPA1Base; | |
import org.junit.Test; | |
public class TestExtendedQueries extends JPA1Base { | |
// the random generator will use this final seed in order to enable reproduceable testing | |
private static final long RANDOM_SEED = 1232133213; | |
// should not be changed | |
private static final int NUMBER_OF_DEPARTMENTS = 10; | |
private static final int NUMBER_OF_EMPLOYEES = 100; | |
// number of maximum tries during random distribution | |
private static final int MAX_NUMBER_OF_TRIES = 100; | |
// since there is a one-to-one relationship for vehicle driver this must at least match the number of employees | |
private static final short NUMBER_OF_MOTORVEHICLES = NUMBER_OF_EMPLOYEES; | |
private static final short NUMBER_OF_BICYCLES = 5; | |
// this maps take note of how much entities are used in relationships | |
private static final Map<String, Department> ALL_DEPARTMENTS = new HashMap<String, Department>(); | |
private static final Map<String, Integer> NUMBER_OF_GIVEN_NAMES_USED = new HashMap<String, Integer>(); | |
private static final Map<String, Integer> NUMBER_OF_SURNAMES_USED = new HashMap<String, Integer>(); | |
private static final Map<Short, List<Integer>> BIKES_RIDERS = new HashMap<Short, List<Integer>>(); | |
private static final Map<Short, Integer> MOTORVEHICLES_DRIVERS = new HashMap<Short, Integer>(); | |
private static final Set<String> EMP_HOBBIES = new HashSet<String>(); | |
// an array of given names | |
private static final String[] GIVEN_NAMES = { "Hadmar", "Hagen", "Hakan", "Hakon", "Hannes", "Hanns", "Hans", "Hansj\u00f6rg", | |
"Hansklaus", "Hanspeter", "Harald", "Harold", "Harro", "Harry", "Hartmut", "Hartwig", "Hartwin", "Harvey", "Hasso", | |
"Hauke", "H\u00e4nel", "Heiko", "Heyko", "Heimo", "Hein", "Heiner", "Heinrich", "Heinz", "Helge", "Helmar", "Helmut", | |
"Hendrik", "Henning", "Henri", "Henrik", "Henryk", "Herbert", "Heribert", "Hermann", "Hieronymus", "Hilarius", | |
"Hilmar", "Hinrich", "Hjalmar", "Holger", "Horst", "Hubert", "Hubertus", "Hugo", "Humphrey", "Hanna", "Hannah", | |
"Hannelore", "Harriet", "Hedi", "Hedwig", "Heide", "Heidemarie", "Heidi", "Heike", "Heinke", "Helen", "Helena", | |
"Helene", "Helga", "Helgard", "Hella", "Helma", "Henrike", "Herma", "Hermine", "Hertha", "Hieu", "Hilda", "Hilde", | |
"Hildegard", "Hilke", "Hulda", "Holde", "Holda", "Hedda", "Heida", "Hazel" }; | |
// an array of surnames | |
private static final String[] SURNAMES = { "Haak", "Haas", "Habermann", "Hache", "Hackmann", "Halbach", "Haller", "Hamann", | |
"Hamburger", "Hamilton", "Hampe", "Hanisch", "Hanson", "Hanstein", "Hardenberg", "Harding", "Hardy", "Harnack", | |
"Hartenthaler", "Hartfelder", "Hartig", "Hary", "Haus Lancaster", "Hausmann", "Hausner", "Havenstein", "Hawke", | |
"Hayek", "Hecht", "Heer", "Hegemann", "Heidegger", "Heider", "Heim", "Hein", "Heinichen", "Heinrich", "Heise", | |
"Helbig", "Hellmann", "Helmig", "Helms", "Hemsterhuis", "Hendrix", "Hennig", "Henning", "Henry", "Henschel", | |
"Hense", "Hentschel", "Herbst", "Herder", "Hermann", "Hermelink", "Herold", "Herrmann", "Herzog", "Hesse", | |
"Hesselbarth", "Hettner", "Heuser", "Hewitt", "Hilbert", "Hildebrand", "Hildebrandt", "Hilgenfeld" }; | |
// list of hobbienames | |
private static final String[] HOBBY_NAMES = { "reading", "cooking", "watching tv", "cleaning the house", "swimming", | |
"computers", "stamps" }; | |
// array of all hobbies | |
private static final Hobby[] HOBBIES = new Hobby[HOBBY_NAMES.length]; | |
// array of all bicycles | |
private static final Bicycle[] BICYCLES = new Bicycle[NUMBER_OF_BICYCLES]; | |
// array of all motorvehicles | |
private static final MotorVehicle[] MOTORVEHICLES = new MotorVehicle[NUMBER_OF_MOTORVEHICLES]; | |
// randomizer for distributing relationships between Employees and other entities | |
private static final Random nameRandomizer = new Random(RANDOM_SEED); | |
protected void resetData() { | |
NUMBER_OF_GIVEN_NAMES_USED.clear(); | |
NUMBER_OF_SURNAMES_USED.clear(); | |
EMP_HOBBIES.clear(); | |
MOTORVEHICLES_DRIVERS.clear(); | |
} | |
protected String getRandomSurname() { | |
String tmpName = SURNAMES[nameRandomizer.nextInt(SURNAMES.length)]; | |
if (!NUMBER_OF_SURNAMES_USED.containsKey(tmpName)) { | |
NUMBER_OF_SURNAMES_USED.put(tmpName, new Integer(1)); | |
} else { | |
Integer tmpNum = NUMBER_OF_SURNAMES_USED.get(tmpName); | |
tmpNum = Integer.valueOf(tmpNum.intValue() + 1); | |
NUMBER_OF_SURNAMES_USED.put(tmpName, tmpNum); | |
} | |
return tmpName; | |
} | |
protected String getRandomGivenName() { | |
String tmpName = GIVEN_NAMES[nameRandomizer.nextInt(GIVEN_NAMES.length)]; | |
if (!NUMBER_OF_GIVEN_NAMES_USED.containsKey(tmpName)) { | |
NUMBER_OF_GIVEN_NAMES_USED.put(tmpName, new Integer(1)); | |
} else { | |
Integer tmpNum = NUMBER_OF_GIVEN_NAMES_USED.get(tmpName); | |
tmpNum = Integer.valueOf(tmpNum.intValue() + 1); | |
NUMBER_OF_GIVEN_NAMES_USED.put(tmpName, tmpNum); | |
} | |
return tmpName; | |
} | |
/** | |
* returns a hobby for given employee, it is not possible to assign a hobby more than once to a employee | |
* | |
* @param empID | |
* the ID of the employee | |
* @return a hobby for the given employee ID | |
* @throws Exception | |
* is thrown if there are no hobbies left which have not already been assigned to this employee | |
*/ | |
protected Hobby getRandomHobby(int empID) { | |
boolean matched = false; | |
int index = 0; | |
for (int number_of_tries = 0; !matched; number_of_tries++) { | |
index = nameRandomizer.nextInt(HOBBIES.length - 1); | |
if (!EMP_HOBBIES.contains(empID + "_" + index)) { | |
EMP_HOBBIES.add(empID + "_" + index); | |
matched = true; | |
} | |
if (number_of_tries >= MAX_NUMBER_OF_TRIES) { | |
throw new RuntimeException("Can't find anymore randomly distributed hobbies"); | |
} | |
} | |
return HOBBIES[index]; | |
} | |
@SuppressWarnings("boxing") | |
protected Bicycle getRandomBike(int empID) { | |
short index = (short) nameRandomizer.nextInt(BICYCLES.length - 1); | |
// take a note about this relation | |
if (!BIKES_RIDERS.containsKey(BICYCLES[index].getId())) { | |
ArrayList<Integer> tmpAl = new ArrayList<Integer>(); | |
tmpAl.add(empID); | |
BIKES_RIDERS.put(BICYCLES[index].getId(), tmpAl); | |
} else { | |
List<Integer> tmpAl = BIKES_RIDERS.get(BICYCLES[index].getId()); | |
tmpAl.add(empID); | |
} | |
return BICYCLES[index]; | |
} | |
@SuppressWarnings("boxing") | |
protected MotorVehicle getRandomMotorVehicle(int empID) { | |
// take note about this relation | |
boolean matched = false; | |
short index = 0; | |
for (int number_of_tries = 0; !matched; number_of_tries++) { | |
index = (short) nameRandomizer.nextInt(MOTORVEHICLES.length - 1); | |
if (!MOTORVEHICLES_DRIVERS.containsKey(MOTORVEHICLES[index].getId())) { | |
MOTORVEHICLES_DRIVERS.put(MOTORVEHICLES[index].getId(), empID); | |
matched = true; | |
} | |
if (number_of_tries >= MAX_NUMBER_OF_TRIES) { | |
throw new RuntimeException("Can't find anymore randomly distributed motorvehicles"); | |
} | |
} | |
return MOTORVEHICLES[index]; | |
} | |
@Override | |
protected void setup() { | |
resetData(); | |
JPAEnvironment env = getEnvironment(); | |
EntityManager em = env.getEntityManager(); | |
try { | |
env.beginTransaction(em); | |
// fills the hobby-array | |
for (int i = 0; i < HOBBY_NAMES.length; i++) { | |
Hobby aHobby = new Hobby(HOBBY_NAMES[i]); | |
HOBBIES[i] = aHobby; | |
em.persist(aHobby); | |
} | |
// creates & persists some departments | |
for (int i = 0; i < NUMBER_OF_DEPARTMENTS; i++) { | |
Department tmpDep = new Department(i, "Department_" + i); | |
// ALL_DEPARTMENTS.put(tmpDep.getName(), tmpDep); | |
em.persist(tmpDep); | |
} | |
// creates & persists some motorvehicles | |
for (int i = 0; i < NUMBER_OF_MOTORVEHICLES; i++) { | |
MotorVehicle tmpMVehicle = new MotorVehicle(); | |
tmpMVehicle.setBrand("foo-car"); | |
MOTORVEHICLES[i] = tmpMVehicle; | |
em.persist(tmpMVehicle); | |
} | |
// creates & persists some bicycles | |
for (int i = 0; i < NUMBER_OF_BICYCLES; i++) { | |
Bicycle tmpBike = new Bicycle(); | |
tmpBike.setBrand("foo-brand"); | |
tmpBike.setRiders(new ArrayList<Employee>()); | |
BICYCLES[i] = tmpBike; | |
em.persist(tmpBike); | |
} | |
// creates & persists some empoyees | |
for (int i = 0; i < NUMBER_OF_EMPLOYEES; i++) { | |
/* | |
* sets a department tries to set 10 employees to each department example: employees 1 to 10 are related to | |
* department 1. employees 11 to 20 are mapped to department 2 and so on | |
*/ | |
Employee tmpEmp = new Employee(i, this.getRandomGivenName(), this.getRandomSurname(), ALL_DEPARTMENTS | |
.get("Department_" + Math.ceil(i / 100) * 10)); | |
// add two hobbies | |
tmpEmp.addHobby(this.getRandomHobby(tmpEmp.getId())); | |
tmpEmp.addHobby(this.getRandomHobby(tmpEmp.getId())); | |
tmpEmp.addHobby(this.getRandomHobby(tmpEmp.getId())); | |
tmpEmp.addHobby(this.getRandomHobby(tmpEmp.getId())); | |
tmpEmp.addHobby(this.getRandomHobby(tmpEmp.getId())); | |
// every second employee gets a motorvehicle | |
if (i % 2 == 0) { | |
getRandomMotorVehicle(tmpEmp.getId()).setDriver(tmpEmp); | |
} | |
// the others get a bicycle | |
else { | |
getRandomBike(tmpEmp.getId()).getRiders().add(tmpEmp); | |
} | |
// set the period | |
tmpEmp.setEmploymentPeriod(new EmploymentPeriod()); | |
tmpEmp.getEmploymentPeriod().setStartDate(new Date(System.currentTimeMillis() - 1000000000)); | |
tmpEmp.getEmploymentPeriod().setEndDate(new Date(System.currentTimeMillis() + 1000000000)); | |
em.persist(tmpEmp); | |
} | |
env.commitTransactionAndClear(em); | |
// em.clear(); | |
} finally { | |
closeEntityManager(em); | |
} | |
} | |
/* | |
* @Test public void testQueryWithMemberOf() throws Exception { EntityManager em = getEnvironment().getEntityManager(); try | |
* { Bicycle bike = new Bicycle(); bike.setId((short)12); bike.setBrand("foo"); bike.setColor("brown"); | |
* bike.setNumberOfGears((short)1); em.persist(bike); | |
* | |
* //test string-mapping of enums Query query = em.createQuery("Select Distinct Object(emp) FROM Employee emp WHERE :param | |
* NOT MEMBER emp.bicycles"); query.setParameter("param", bike); query.getResultList(); } finally { closeEntityManager(em); | |
* } } | |
*/ | |
// @TestProperties(unsupportedDatabaseVendors = { DatabaseVendor.OPEN_SQL }) | |
@Test | |
public void testQueryWithUpdateAndSet() throws Exception { | |
JPAEnvironment env = getEnvironment(); | |
EntityManager em = env.getEntityManager(); | |
try { | |
env.beginTransaction(em); | |
Query query = em | |
.createQuery("UPDATE Employee emp SET emp.salary = emp.salary + :param1 WHERE emp.firstname = SUBSTRING(:string, :int1, :int2)"); | |
query.setParameter("param1", Integer.valueOf(13)); | |
query.setParameter("string", "moo"); | |
query.setParameter("int1", Integer.valueOf(1)); | |
query.setParameter("int2", Integer.valueOf(2)); | |
query.executeUpdate(); | |
env.commitTransaction(em); | |
} finally { | |
closeEntityManager(em); | |
} | |
} | |
// @TestProperties(unsupportedDatabaseVendors = { DatabaseVendor.OPEN_SQL }) | |
@Test | |
public void testQueryWithBuiltinAbs() throws Exception { | |
EntityManager em = getEnvironment().getEntityManager(); | |
try { | |
// test string-mapping of enums | |
Query query = em.createQuery("Select DISTINCT Object(emp) From Employee emp WHERE emp.salary > ABS(:dbl)"); | |
query.setParameter("dbl", new Double(1180D)); | |
query.getResultList(); | |
} finally { | |
closeEntityManager(em); | |
} | |
} | |
// @TestProperties(unsupportedDatabaseVendors = { DatabaseVendor.OPEN_SQL }) | |
@Test | |
public void testQueryWithMemberOf() throws Exception { | |
EntityManager em = getEnvironment().getEntityManager(); | |
try { | |
// test string-mapping of enums | |
Query query = em | |
.createQuery("Select Distinct Object(emp) FROM Employee emp WHERE emp.firstname = SUBSTRING(:string, :int1, :int2)"); | |
query.setParameter("string", "moo"); | |
query.setParameter("int1", Integer.valueOf(1)); | |
query.setParameter("int2", Integer.valueOf(2)); | |
query.getResultList(); | |
} finally { | |
closeEntityManager(em); | |
} | |
} | |
@Test | |
public void testQueryWithComparisonExpressionAndEnums() throws Exception { | |
EntityManager em = getEnvironment().getEntityManager(); | |
try { | |
// test string-mapping of enums | |
Query query4 = em | |
.createQuery("SELECT btfa.id FROM BasicTypesFieldAccess btfa WHERE btfa.enumString = org.eclipse.persistence.testing.models.wdf.jpa1.types.UserDefinedEnum.HUGO"); | |
query4.getResultList(); | |
// test ordinal-mapping of enums | |
Query query5 = em | |
.createQuery("SELECT btfa.id FROM BasicTypesFieldAccess btfa WHERE btfa.enumOrdinal = org.eclipse.persistence.testing.models.wdf.jpa1.types.UserDefinedEnum.HUGO"); | |
query5.getResultList(); | |
} finally { | |
closeEntityManager(em); | |
} | |
} | |
@ToBeInvestigated | |
@Test | |
public void testQueryWithInExpressionAndEnums() throws Exception { | |
EntityManager em = getEnvironment().getEntityManager(); | |
try { | |
// test string-mapping of enums | |
Query query4 = em | |
.createQuery("SELECT btfa.id FROM BasicTypesFieldAccess btfa WHERE btfa.enumString IN (org.eclipse.persistence.testing.models.wdf.jpa1.types.UserDefinedEnum.HUGO, org.eclipse.persistence.testing.models.wdf.jpa1.types.UserDefinedEnum.EMIL)"); | |
query4.getResultList(); | |
// test ordinal-mapping of enums | |
Query query5 = em | |
.createQuery("SELECT btfa.id FROM BasicTypesFieldAccess btfa WHERE btfa.enumOrdinal IN (com.sap.jpa.example.UserDefinedEnum.HUGO, com.sap.jpa.example.UserDefinedEnum.EMIL)"); | |
query5.getResultList(); | |
} finally { | |
closeEntityManager(em); | |
} | |
} | |
@Test | |
public void testQueryWithAggregateWithHavingExpression() throws Exception { | |
EntityManager em = getEnvironment().getEntityManager(); | |
try { | |
// this one tests aggregate expression in having condition | |
Query query2 = em | |
.createQuery("SELECT e.firstname FROM Employee e JOIN e.hobbies h GROUP BY e.firstname HAVING COUNT(DISTINCT e.id) = 1"); | |
query2.getResultList(); | |
} finally { | |
closeEntityManager(em); | |
} | |
} | |
@Test | |
public void testQueryWithInExpressionAndParameters() throws Exception { | |
EntityManager em = getEnvironment().getEntityManager(); | |
try { | |
// this one tests for the IN predicate in combination with two parameters | |
Query query1 = em.createQuery("select e.id from Employee e where e.id IN (?1, ?2)"); | |
query1.setParameter(1, Integer.valueOf(1)); | |
query1.setParameter(2, Integer.valueOf(2)); | |
query1.getResultList(); | |
} finally { | |
closeEntityManager(em); | |
} | |
} | |
@SuppressWarnings("unchecked") | |
@Test | |
public void testBasicQuery() { | |
EntityManager em = getEnvironment().getEntityManager(); | |
try { | |
Query query = em.createQuery("select e from Employee e"); | |
List result = query.getResultList(); | |
verify(result.size() == NUMBER_OF_EMPLOYEES, "wrong resultcount for employees"); | |
} finally { | |
closeEntityManager(em); | |
} | |
} | |
/** | |
* check for a thrown exception in case of an illegal OrderBy-clause | |
*/ | |
@ToBeInvestigated | |
@Test | |
public void testInvalidOrderBy() { | |
boolean passed = false; | |
EntityManager em = getEnvironment().getEntityManager(); | |
try { | |
em.createQuery("SELECT e.firstname FROM Employee e ORDER BY e.salary"); | |
} catch (IllegalArgumentException iAEx) { | |
passed = true; | |
} finally { | |
closeEntityManager(em); | |
} | |
verify(passed, "missing IllegalArgumentException"); | |
} | |
/** | |
* check for a thrown exception in case of an illegal OrderBy-clause | |
*/ | |
@ToBeInvestigated | |
@Test | |
public void testInvalidOrderByWithCmr() { | |
boolean passed = false; | |
EntityManager em = getEnvironment().getEntityManager(); | |
try { | |
em.createQuery("SELECT e.department.name FROM Employee e ORDER BY e.department.id"); | |
} catch (IllegalArgumentException iAEx) { | |
passed = true; | |
} finally { | |
closeEntityManager(em); | |
} | |
verify(passed, "missing IllegalArgumentException"); | |
} | |
@Test | |
public void testOrderBy() { | |
EntityManager em = getEnvironment().getEntityManager(); | |
try { | |
em.createQuery("SELECT e FROM Employee e ORDER BY e.salary ASC, e.firstname DESC, e.lastname DESC"); | |
} finally { | |
closeEntityManager(em); | |
} | |
} | |
@Test | |
public void testOrderByCmr() { | |
EntityManager em = getEnvironment().getEntityManager(); | |
try { | |
em.createQuery("SELECT e.department FROM Employee e ORDER BY e.department.id ASC"); | |
} finally { | |
closeEntityManager(em); | |
} | |
} | |
/** | |
* check for the right occurence of all names in the result of the query | |
*/ | |
@SuppressWarnings("unchecked") | |
@Test | |
public void testSurnamesOfEmployees() { | |
EntityManager em = getEnvironment().getEntityManager(); | |
try { | |
Query query = em.createQuery("SELECT e from Employee e WHERE e.lastname=?1"); | |
Iterator<String> iter = NUMBER_OF_SURNAMES_USED.keySet().iterator(); | |
while (iter.hasNext()) { | |
String surname = iter.next(); | |
Integer surnamesUsed = NUMBER_OF_SURNAMES_USED.get(surname); | |
query.setParameter(1, surname); | |
List result = query.getResultList(); | |
verify(result.size() == surnamesUsed.intValue(), "the number of persons with given name " + surname | |
+ " in the result does not match the number of names used when the entities where created."); | |
} | |
} finally { | |
closeEntityManager(em); | |
} | |
} | |
@SuppressWarnings("unchecked") | |
@Test | |
public void testGivenNamesOfEmployees() { | |
EntityManager em = getEnvironment().getEntityManager(); | |
try { | |
Query query = em.createQuery("SELECT e from Employee e WHERE e.firstname=?1"); | |
Iterator<String> iter = NUMBER_OF_GIVEN_NAMES_USED.keySet().iterator(); | |
while (iter.hasNext()) { | |
String givenName = iter.next(); | |
Integer givenNamesUsed = NUMBER_OF_GIVEN_NAMES_USED.get(givenName); | |
query.setParameter(1, givenName); | |
List result = query.getResultList(); | |
verify(result.size() == givenNamesUsed.intValue(), "the number of persons with given name " + givenName | |
+ " in the result does not match the number of names used when the entities where created."); | |
} | |
} finally { | |
closeEntityManager(em); | |
} | |
} | |
@SuppressWarnings("unchecked") | |
@Test | |
public void testNumberOfDrivers() { | |
EntityManager em = getEnvironment().getEntityManager(); | |
try { | |
Query query = em.createQuery("SELECT d.firstname FROM MotorVehicle mv JOIN mv.driver d WHERE mv.id = ?1"); | |
Iterator<Short> iter = MOTORVEHICLES_DRIVERS.keySet().iterator(); | |
while (iter.hasNext()) { | |
Short vehicleID = iter.next(); | |
// can only be 1 since the one-to-one relationship for drivers | |
int numberOfDrivers = 1; | |
query.setParameter(1, vehicleID); | |
List result = query.getResultList(); | |
verify(result.size() == numberOfDrivers, "the number of drivers for motorvehicle does not match " | |
+ result.size() + " != " + numberOfDrivers); | |
} | |
} finally { | |
closeEntityManager(em); | |
} | |
} | |
@SuppressWarnings("unchecked") | |
@Test | |
public void testNumberOfDrivenVehicles() { | |
EntityManager em = getEnvironment().getEntityManager(); | |
try { | |
Query query = em.createQuery("SELECT d.firstname FROM MotorVehicle mv JOIN mv.driver d"); | |
List result = query.getResultList(); | |
verify(result.size() == MOTORVEHICLES_DRIVERS.size(), "there number of driver-relationships between "); | |
} finally { | |
closeEntityManager(em); | |
} | |
} | |
} |