| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. 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. For additional information regarding |
| * copyright in this work, please see the NOTICE file in the top level |
| * directory of this distribution. |
| */ |
| package org.apache.usergrid.persistence.collection.serialization.impl; |
| |
| |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.Iterator; |
| import java.util.UUID; |
| |
| import com.datastax.driver.core.BatchStatement; |
| import com.datastax.driver.core.ConsistencyLevel; |
| import com.datastax.driver.core.Session; |
| |
| import org.junit.Assert; |
| import org.junit.Before; |
| import org.junit.Rule; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| |
| import org.apache.usergrid.persistence.collection.guice.TestCollectionModule; |
| import org.apache.usergrid.persistence.collection.serialization.UniqueValue; |
| import org.apache.usergrid.persistence.collection.serialization.UniqueValueSerializationStrategy; |
| import org.apache.usergrid.persistence.collection.serialization.UniqueValueSet; |
| import org.apache.usergrid.persistence.core.guice.MigrationManagerRule; |
| import org.apache.usergrid.persistence.core.scope.ApplicationScope; |
| import org.apache.usergrid.persistence.core.scope.ApplicationScopeImpl; |
| import org.apache.usergrid.persistence.core.test.ITRunner; |
| import org.apache.usergrid.persistence.core.test.UseModules; |
| import org.apache.usergrid.persistence.model.entity.Id; |
| import org.apache.usergrid.persistence.model.entity.SimpleId; |
| import org.apache.usergrid.persistence.model.field.Field; |
| import org.apache.usergrid.persistence.model.field.IntegerField; |
| import org.apache.usergrid.persistence.model.field.StringField; |
| import org.apache.usergrid.persistence.model.util.UUIDGenerator; |
| |
| import com.google.inject.Inject; |
| import com.netflix.astyanax.connectionpool.exceptions.ConnectionException; |
| |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertTrue; |
| |
| |
| @RunWith(ITRunner.class) |
| @UseModules(TestCollectionModule.class) |
| public abstract class UniqueValueSerializationStrategyImplTest { |
| |
| |
| @Inject |
| @Rule |
| public MigrationManagerRule migrationManagerRule; |
| |
| @Inject |
| private Session session; |
| |
| private UniqueValueSerializationStrategy strategy; |
| |
| |
| @Before |
| public void wireUniqueSerializationStrategy(){ |
| strategy = getUniqueSerializationStrategy(); |
| } |
| |
| |
| /** |
| * Get the unique value serialization |
| * @return |
| */ |
| protected abstract UniqueValueSerializationStrategy getUniqueSerializationStrategy(); |
| |
| |
| @Test |
| public void testBasicOperation() throws ConnectionException, InterruptedException { |
| |
| ApplicationScope scope = |
| new ApplicationScopeImpl( new SimpleId( "organization" ) ); |
| |
| IntegerField field = new IntegerField( "count", 5 ); |
| Id entityId = new SimpleId( UUIDGenerator.newTimeUUID(), "entity" ); |
| UUID version = UUIDGenerator.newTimeUUID(); |
| UniqueValue stored = new UniqueValueImpl( field, entityId, version ); |
| //strategy.write( scope, stored ).execute(); |
| BatchStatement batch = strategy.writeCQL(scope, stored, -1); |
| session.execute(batch); |
| |
| UniqueValueSet fields = strategy.load( scope, entityId.getType(), Collections.<Field>singleton( field ) ); |
| |
| UniqueValue retrieved = fields.getValue( field.getName() ); |
| Assert.assertNotNull( retrieved ); |
| assertEquals( stored, retrieved ); |
| |
| Iterator<UniqueValue> allFieldsWritten = strategy.getAllUniqueFields( scope, entityId ); |
| |
| assertTrue(allFieldsWritten.hasNext()); |
| |
| //test this interface. In most cases, we won't know the field name, so we want them all |
| UniqueValue allFieldsValue = allFieldsWritten.next(); |
| Assert.assertNotNull( allFieldsValue ); |
| |
| assertEquals( field, allFieldsValue.getField() ); |
| assertEquals(version, allFieldsValue.getEntityVersion()); |
| |
| assertFalse(allFieldsWritten.hasNext()); |
| |
| } |
| |
| |
| @Test |
| public void testWriteWithTTL() throws InterruptedException, ConnectionException { |
| |
| |
| ApplicationScope scope = |
| new ApplicationScopeImpl( new SimpleId( "organization" ) ); |
| |
| // write object that lives 2 seconds |
| IntegerField field = new IntegerField( "count", 5 ); |
| Id entityId = new SimpleId( UUIDGenerator.newTimeUUID(), "entity" ); |
| UUID version = UUIDGenerator.newTimeUUID(); |
| UniqueValue stored = new UniqueValueImpl( field, entityId, version ); |
| //strategy.write( scope, stored, 5 ).execute(); |
| BatchStatement batch = strategy.writeCQL(scope, stored, 5); |
| session.execute(batch); |
| |
| Thread.sleep( 1000 ); |
| |
| // waited one sec, should be still here |
| UniqueValueSet fields = strategy.load( scope, entityId.getType(), Collections.<Field>singleton( field ) ); |
| |
| UniqueValue retrieved = fields.getValue( field.getName() ); |
| |
| Assert.assertNotNull( retrieved ); |
| assertEquals( stored, retrieved ); |
| |
| Thread.sleep( 5000 ); |
| |
| // wait another second, should be gone now |
| fields = strategy.load( scope, entityId.getType(), Collections.<Field>singleton( field ) ); |
| |
| UniqueValue nullExpected = fields.getValue( field.getName() ); |
| Assert.assertNull( nullExpected ); |
| |
| |
| //we still want to retain the log entry, even if we don't retain the unique value. Deleting something |
| //that doesn't exist is a tombstone, but so is the timeout. |
| Iterator<UniqueValue> allFieldsWritten = strategy.getAllUniqueFields( scope, entityId ); |
| |
| assertTrue( allFieldsWritten.hasNext() ); |
| |
| //test this interface. In most cases, we won't know the field name, so we want them all |
| UniqueValue writtenFieldEntry = allFieldsWritten.next(); |
| Assert.assertNotNull( writtenFieldEntry ); |
| |
| assertEquals( field, writtenFieldEntry.getField() ); |
| assertEquals( version, writtenFieldEntry.getEntityVersion() ); |
| |
| assertFalse(allFieldsWritten.hasNext()); |
| |
| |
| |
| } |
| |
| |
| @Test |
| public void testDelete() throws ConnectionException { |
| |
| |
| ApplicationScope scope = |
| new ApplicationScopeImpl( new SimpleId( "organization" ) ); |
| |
| IntegerField field = new IntegerField( "count", 5 ); |
| Id entityId = new SimpleId( UUIDGenerator.newTimeUUID(), "entity" ); |
| UUID version = UUIDGenerator.newTimeUUID(); |
| UniqueValue stored = new UniqueValueImpl( field, entityId, version ); |
| |
| //strategy.write( scope, stored ).execute(); |
| BatchStatement batch = strategy.writeCQL( scope, stored, -1); |
| session.execute(batch); |
| |
| |
| //strategy.delete( scope, stored ).execute(); |
| BatchStatement deleteBatch = strategy.deleteCQL(scope, stored); |
| session.execute(deleteBatch); |
| |
| UniqueValueSet fields = strategy.load( scope, entityId.getType(), Collections.<Field>singleton( field ) ); |
| |
| UniqueValue nullExpected = fields.getValue( field.getName() ); |
| |
| |
| Assert.assertNull( nullExpected ); |
| |
| |
| Iterator<UniqueValue> allFieldsWritten = strategy.getAllUniqueFields( scope, entityId ); |
| |
| assertFalse("No entries left", allFieldsWritten.hasNext() ); |
| } |
| |
| |
| @Test |
| public void testCapitalizationFixes() throws ConnectionException { |
| |
| ApplicationScope scope = |
| new ApplicationScopeImpl( new SimpleId( "organization" ) ); |
| |
| StringField field = new StringField( "count", "MiXeD CaSe" ); |
| Id entityId = new SimpleId( UUIDGenerator.newTimeUUID(), "entity" ); |
| UUID version = UUIDGenerator.newTimeUUID(); |
| UniqueValue stored = new UniqueValueImpl( field, entityId, version ); |
| //strategy.write( scope, stored ).execute(); |
| BatchStatement batch = strategy.writeCQL( scope, stored, -1); |
| session.execute(batch); |
| |
| UniqueValueSet fields = strategy.load( scope, entityId.getType(), Collections.<Field>singleton( field ) ); |
| |
| UniqueValue value = fields.getValue( field.getName() ); |
| |
| |
| assertEquals( field.getName(), value.getField().getName() ); |
| |
| assertEquals( entityId, value.getEntityId() ); |
| |
| //now test will all upper and all lower, we should get it all the same |
| fields = strategy.load( scope, entityId.getType(), |
| Collections.<Field>singleton( new StringField( field.getName(), "MIXED CASE" ) ) ); |
| |
| value = fields.getValue( field.getName() ); |
| |
| |
| assertEquals( field.getName(), value.getField().getName() ); |
| |
| assertEquals( entityId, value.getEntityId() ); |
| |
| fields = strategy.load( scope, entityId.getType(), |
| Collections.<Field>singleton( new StringField( field.getName(), "mixed case" ) ) ); |
| |
| value = fields.getValue( field.getName() ); |
| |
| |
| assertEquals( field.getName(), value.getField().getName() ); |
| |
| assertEquals( entityId, value.getEntityId() ); |
| |
| |
| Iterator<UniqueValue> allFieldsWritten = strategy.getAllUniqueFields( scope, entityId ); |
| |
| assertTrue( allFieldsWritten.hasNext() ); |
| |
| //test this interface. In most cases, we won't know the field name, so we want them all |
| UniqueValue writtenFieldEntry = allFieldsWritten.next(); |
| Assert.assertNotNull( writtenFieldEntry ); |
| |
| assertEquals( field.getName(), writtenFieldEntry.getField().getName() ); |
| assertEquals( field.getValue().toLowerCase(), writtenFieldEntry.getField().getValue() ); |
| assertEquals( version, writtenFieldEntry.getEntityVersion() ); |
| |
| assertFalse(allFieldsWritten.hasNext()); |
| } |
| |
| |
| |
| @Test |
| public void twoFieldsPerVersion() throws ConnectionException, InterruptedException { |
| |
| |
| ApplicationScope scope = |
| new ApplicationScopeImpl( new SimpleId( "organization" ) ); |
| |
| |
| Id entityId = new SimpleId( UUIDGenerator.newTimeUUID(), "entity" ); |
| final UUID version1 = UUIDGenerator.newTimeUUID(); |
| |
| |
| //write V1 of everything |
| IntegerField version1Field1 = new IntegerField( "count", 1 ); |
| StringField version1Field2 = new StringField("field", "v1value"); |
| |
| |
| UniqueValue version1Field1Value = new UniqueValueImpl( version1Field1, entityId, version1 ); |
| UniqueValue version1Field2Value = new UniqueValueImpl( version1Field2, entityId, version1 ); |
| |
| //final MutationBatch batch = strategy.write( scope, version1Field1Value ); |
| //batch.mergeShallow( strategy.write( scope, version1Field2Value ) ); |
| |
| final BatchStatement batch = new BatchStatement(); |
| |
| batch.add(strategy.writeCQL( scope, version1Field1Value, -1)); |
| batch.add(strategy.writeCQL( scope, version1Field2Value, -1)); |
| |
| //write V2 of everything |
| final UUID version2 = UUIDGenerator.newTimeUUID(); |
| |
| IntegerField version2Field1 = new IntegerField( "count", 2 ); |
| StringField version2Field2 = new StringField( "field", "v2value" ); |
| |
| |
| UniqueValue version2Field1Value = new UniqueValueImpl( version2Field1, entityId, version2 ); |
| UniqueValue version2Field2Value = new UniqueValueImpl( version2Field2, entityId, version2 ); |
| |
| //batch.mergeShallow( strategy.write( scope, version2Field1Value ) ); |
| //batch.mergeShallow( strategy.write( scope, version2Field2Value ) ); |
| |
| batch.add(strategy.writeCQL( scope, version2Field1Value, -1)); |
| batch.add(strategy.writeCQL( scope, version2Field2Value, -1)); |
| |
| session.execute(batch); |
| |
| //batch.execute(); |
| |
| |
| UniqueValueSet fields = strategy.load( scope, entityId.getType(), Arrays.<Field>asList( version1Field1, version1Field2 ) ); |
| |
| UniqueValue retrieved = fields.getValue( version1Field1.getName() ); |
| |
| assertEquals( version1Field1Value, retrieved ); |
| |
| |
| retrieved = fields.getValue( version1Field2.getName() ); |
| assertEquals( version1Field2Value, retrieved ); |
| |
| |
| Iterator<UniqueValue> allFieldsWritten = strategy.getAllUniqueFields( scope, entityId ); |
| |
| assertTrue(allFieldsWritten.hasNext()); |
| |
| //test this interface. In most cases, we won't know the field name, so we want them all |
| UniqueValue allFieldsValue = allFieldsWritten.next(); |
| |
| //version 2 fields should come first, ordered by field name |
| assertEquals( version2Field1, allFieldsValue.getField() ); |
| assertEquals( version2, allFieldsValue.getEntityVersion() ); |
| |
| allFieldsValue = allFieldsWritten.next(); |
| |
| assertEquals( version2Field2, allFieldsValue.getField() ); |
| assertEquals( version2, allFieldsValue.getEntityVersion() ); |
| |
| |
| //version 1 should come next ordered by field name |
| allFieldsValue = allFieldsWritten.next(); |
| |
| assertEquals( version1Field1, allFieldsValue.getField() ); |
| assertEquals( version1, allFieldsValue.getEntityVersion() ); |
| |
| allFieldsValue = allFieldsWritten.next(); |
| |
| assertEquals( version1Field2, allFieldsValue.getField() ); |
| assertEquals( version1, allFieldsValue.getEntityVersion() ); |
| |
| assertFalse(allFieldsWritten.hasNext()); |
| |
| } |
| |
| /** |
| * Test that inserting duplicates always show the oldest entity UUID being returned (versions of that OK to change). |
| * |
| * @throws ConnectionException |
| * @throws InterruptedException |
| */ |
| @Test |
| public void testWritingDuplicates() throws ConnectionException, InterruptedException { |
| |
| ApplicationScope scope = |
| new ApplicationScopeImpl( new SimpleId( "organization" ) ); |
| |
| IntegerField field = new IntegerField( "count", 5 ); |
| Id entityId1 = new SimpleId( UUIDGenerator.newTimeUUID(), "entity" ); |
| Id entityId2 = new SimpleId( UUIDGenerator.newTimeUUID(), "entity" ); |
| |
| |
| |
| UUID version1 = UUIDGenerator.newTimeUUID(); |
| UUID version2 = UUIDGenerator.newTimeUUID(); |
| |
| UniqueValue stored1 = new UniqueValueImpl( field, entityId1, version2 ); |
| UniqueValue stored2 = new UniqueValueImpl( field, entityId2, version1 ); |
| |
| |
| session.execute(strategy.writeCQL( scope, stored1, -1 )); |
| session.execute(strategy.writeCQL( scope, stored2, -1 )); |
| |
| // load descending to get the older version of entity for this unique value |
| UniqueValueSet fields = strategy.load( scope, ConsistencyLevel.LOCAL_QUORUM, |
| entityId1.getType(), Collections.<Field>singleton( field ), true); |
| |
| UniqueValue retrieved = fields.getValue( field.getName() ); |
| |
| // validate that the first entity UUID is returned after inserting a duplicate mapping |
| assertEquals( stored1, retrieved ); |
| |
| |
| |
| UUID version3 = UUIDGenerator.newTimeUUID(); |
| UniqueValue stored3 = new UniqueValueImpl( field, entityId2, version3); |
| session.execute(strategy.writeCQL( scope, stored3, -1 )); |
| |
| // load the values again, we should still only get back the original unique value |
| fields = strategy.load( scope, ConsistencyLevel.LOCAL_QUORUM, |
| entityId1.getType(), Collections.<Field>singleton( field ), true); |
| |
| retrieved = fields.getValue( field.getName() ); |
| |
| // validate that the first entity UUID is still returned after inserting duplicate with newer version |
| assertEquals( stored1, retrieved ); |
| |
| |
| UUID version4 = UUIDGenerator.newTimeUUID(); |
| UniqueValue stored4 = new UniqueValueImpl( field, entityId1, version4); |
| session.execute(strategy.writeCQL( scope, stored4, -1 )); |
| |
| // load the values again, now we should get the latest version of the original UUID written |
| fields = strategy.load( scope, ConsistencyLevel.LOCAL_QUORUM, |
| entityId1.getType(), Collections.<Field>singleton( field ), true); |
| |
| retrieved = fields.getValue( field.getName() ); |
| |
| // validate that the first entity UUID is still returned, but with the latest version |
| assertEquals( stored4, retrieved ); |
| |
| } |
| |
| /** |
| * Test that inserting multiple versions of the same entity UUID result in the latest version being returned. |
| * |
| * @throws ConnectionException |
| * @throws InterruptedException |
| */ |
| @Test |
| public void testMultipleVersionsSameEntity() throws ConnectionException, InterruptedException { |
| |
| ApplicationScope scope = |
| new ApplicationScopeImpl( new SimpleId( "organization" ) ); |
| |
| IntegerField field = new IntegerField( "count", 5 ); |
| Id entityId1 = new SimpleId( UUIDGenerator.newTimeUUID(), "entity" ); |
| |
| |
| |
| UUID version1 = UUIDGenerator.newTimeUUID(); |
| UUID version2 = UUIDGenerator.newTimeUUID(); |
| |
| UniqueValue stored1 = new UniqueValueImpl( field, entityId1, version1 ); |
| UniqueValue stored2 = new UniqueValueImpl( field, entityId1, version2 ); |
| |
| |
| session.execute(strategy.writeCQL( scope, stored1, -1 )); |
| session.execute(strategy.writeCQL( scope, stored2, -1 )); |
| |
| // load descending to get the older version of entity for this unique value |
| UniqueValueSet fields = strategy.load( scope, ConsistencyLevel.LOCAL_QUORUM, |
| entityId1.getType(), Collections.<Field>singleton( field ), true); |
| |
| UniqueValue retrieved = fields.getValue( field.getName() ); |
| Assert.assertNotNull( retrieved ); |
| assertEquals( stored2, retrieved ); |
| |
| |
| } |
| |
| @Test |
| public void testDuplicateEntitiesDescending() throws ConnectionException, InterruptedException { |
| |
| ApplicationScope scope = |
| new ApplicationScopeImpl( new SimpleId( "organization" ) ); |
| |
| IntegerField field = new IntegerField( "count", 5 ); |
| Id entityId1 = new SimpleId( UUIDGenerator.newTimeUUID(), "entity" ); |
| Id entityId2 = new SimpleId( UUIDGenerator.newTimeUUID(), "entity" ); |
| Id entityId3 = new SimpleId( UUIDGenerator.newTimeUUID(), "entity" ); |
| |
| |
| |
| UUID version1 = UUIDGenerator.newTimeUUID(); |
| UUID version2 = UUIDGenerator.newTimeUUID(); |
| UUID version3 = UUIDGenerator.newTimeUUID(); |
| |
| UniqueValue stored1 = new UniqueValueImpl( field, entityId3, version1 ); |
| UniqueValue stored2 = new UniqueValueImpl( field, entityId2, version2 ); |
| UniqueValue stored3 = new UniqueValueImpl( field, entityId1, version3 ); |
| |
| |
| session.execute(strategy.writeCQL( scope, stored1, -1 )); |
| session.execute(strategy.writeCQL( scope, stored2, -1 )); |
| session.execute(strategy.writeCQL( scope, stored3, -1 )); |
| |
| |
| // load descending to get the older version of entity for this unique value |
| UniqueValueSet fields = strategy.load( scope, ConsistencyLevel.LOCAL_QUORUM, |
| entityId1.getType(), Collections.<Field>singleton( field ), true); |
| |
| |
| UniqueValue retrieved = fields.getValue( field.getName() ); |
| assertEquals( stored3, retrieved ); |
| |
| |
| } |
| |
| @Test |
| public void testDuplicateEntitiesAscending() throws ConnectionException, InterruptedException { |
| |
| ApplicationScope scope = |
| new ApplicationScopeImpl( new SimpleId( "organization" ) ); |
| |
| IntegerField field = new IntegerField( "count", 5 ); |
| Id entityId1 = new SimpleId( UUIDGenerator.newTimeUUID(), "entity" ); |
| Id entityId2 = new SimpleId( UUIDGenerator.newTimeUUID(), "entity" ); |
| Id entityId3 = new SimpleId( UUIDGenerator.newTimeUUID(), "entity" ); |
| |
| |
| |
| UUID version1 = UUIDGenerator.newTimeUUID(); |
| UUID version2 = UUIDGenerator.newTimeUUID(); |
| UUID version3 = UUIDGenerator.newTimeUUID(); |
| |
| UniqueValue stored1 = new UniqueValueImpl( field, entityId1, version1 ); |
| UniqueValue stored2 = new UniqueValueImpl( field, entityId2, version2 ); |
| UniqueValue stored3 = new UniqueValueImpl( field, entityId3, version3 ); |
| |
| |
| session.execute(strategy.writeCQL( scope, stored1, -1 )); |
| session.execute(strategy.writeCQL( scope, stored2, -1 )); |
| session.execute(strategy.writeCQL( scope, stored3, -1 )); |
| |
| |
| // load descending to get the older version of entity for this unique value |
| UniqueValueSet fields = strategy.load( scope, |
| ConsistencyLevel.LOCAL_QUORUM, entityId1.getType(), Collections.<Field>singleton( field ), true); |
| |
| UniqueValue retrieved = fields.getValue( field.getName() ); |
| assertEquals( stored1, retrieved ); |
| |
| |
| } |
| |
| @Test |
| public void testMixedDuplicates() throws ConnectionException, InterruptedException { |
| |
| ApplicationScope scope = |
| new ApplicationScopeImpl( new SimpleId( "organization" ) ); |
| |
| IntegerField field = new IntegerField( "count", 5 ); |
| Id entityId1 = new SimpleId( UUIDGenerator.newTimeUUID(), "entity" ); |
| Id entityId2 = new SimpleId( UUIDGenerator.newTimeUUID(), "entity" ); |
| Id entityId3 = new SimpleId( UUIDGenerator.newTimeUUID(), "entity" ); |
| |
| |
| |
| UUID version1 = UUIDGenerator.newTimeUUID(); |
| UUID version2 = UUIDGenerator.newTimeUUID(); |
| UUID version3 = UUIDGenerator.newTimeUUID(); |
| UUID version4 = UUIDGenerator.newTimeUUID(); |
| UUID version5 = UUIDGenerator.newTimeUUID(); |
| |
| UniqueValue stored1 = new UniqueValueImpl( field, entityId1, version5 ); |
| UniqueValue stored2 = new UniqueValueImpl( field, entityId2, version4 ); |
| UniqueValue stored3 = new UniqueValueImpl( field, entityId1, version3 ); |
| UniqueValue stored4 = new UniqueValueImpl( field, entityId3, version2 ); |
| UniqueValue stored5 = new UniqueValueImpl( field, entityId3, version1 ); |
| |
| |
| |
| session.execute(strategy.writeCQL( scope, stored1, -1 )); |
| session.execute(strategy.writeCQL( scope, stored2, -1 )); |
| session.execute(strategy.writeCQL( scope, stored3, -1 )); |
| session.execute(strategy.writeCQL( scope, stored4, -1 )); |
| session.execute(strategy.writeCQL( scope, stored5, -1 )); |
| |
| |
| // load descending to get the older version of entity for this unique value |
| UniqueValueSet fields = strategy.load( scope, ConsistencyLevel.LOCAL_QUORUM, |
| entityId1.getType(), Collections.<Field>singleton( field ), true); |
| |
| UniqueValue retrieved = fields.getValue( field.getName() ); |
| assertEquals( stored1, retrieved ); |
| |
| |
| } |
| |
| } |