| /* |
| * 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.jackrabbit.oak.query.index; |
| |
| import static com.google.common.base.Preconditions.checkNotNull; |
| import static com.google.common.collect.ImmutableList.of; |
| import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE; |
| import static org.apache.jackrabbit.JcrConstants.NT_UNSTRUCTURED; |
| import static org.apache.jackrabbit.oak.api.Type.LONG; |
| import static org.apache.jackrabbit.oak.api.Type.NAME; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| |
| import org.apache.jackrabbit.oak.Oak; |
| import org.apache.jackrabbit.oak.api.CommitFailedException; |
| import org.apache.jackrabbit.oak.api.ContentRepository; |
| import org.apache.jackrabbit.oak.api.Tree; |
| import org.apache.jackrabbit.oak.api.Type; |
| import org.apache.jackrabbit.oak.InitialContent; |
| import org.apache.jackrabbit.oak.query.AbstractQueryTest; |
| import org.apache.jackrabbit.oak.spi.security.OpenSecurityProvider; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| import org.junit.Ignore; |
| import org.junit.Test; |
| |
| import com.google.common.collect.ImmutableList; |
| |
| /** |
| * Tests the query engine using the default index implementation: the |
| * {@link TraversingIndex} |
| */ |
| public class TraversingIndexQueryTest extends AbstractQueryTest { |
| |
| @Override |
| protected ContentRepository createRepository() { |
| return new Oak() |
| .with(new OpenSecurityProvider()) |
| .with(new InitialContent()) |
| .createContentRepository(); |
| } |
| |
| @Test |
| public void sql1() throws Exception { |
| test("sql1.txt"); |
| } |
| |
| @Test |
| public void sql2() throws Exception { |
| test("sql2.txt"); |
| } |
| |
| @Test |
| @Ignore("OAK-2050") |
| public void testFullTextTerm() throws Exception { |
| //OAK-1024 allow '/' in a full-text query |
| Tree node = root.getTree("/").addChild("content"); |
| node.setProperty("jcr:mimeType", "text/plain"); |
| root.commit(); |
| assertQuery("//*[jcr:contains(., 'text/plain')]", "xpath", |
| ImmutableList.of("/content")); |
| } |
| |
| @Test |
| @Ignore("OAK-2050") |
| public void testFullTextTermName() throws Exception { |
| Tree c = root.getTree("/").addChild("content"); |
| c.addChild("testFullTextTermNameSimple"); |
| c.addChild("testFullTextTermNameFile.txt"); |
| root.commit(); |
| assertQuery("//*[jcr:contains(., 'testFullTextTermNameSimple')]", |
| "xpath", |
| ImmutableList.of("/content/testFullTextTermNameSimple")); |
| assertQuery("//*[jcr:contains(., 'testFullTextTermNameFile.txt')]", |
| "xpath", |
| ImmutableList.of("/content/testFullTextTermNameFile.txt")); |
| } |
| |
| @Test |
| public void testMultiNotEqual() throws Exception { |
| Tree c = root.getTree("/").addChild("content"); |
| |
| c.addChild("one").setProperty("prop", "value"); |
| c.addChild("two").setProperty("prop", |
| ImmutableList.of("aaa", "value", "bbb"), Type.STRINGS); |
| c.addChild("three").setProperty("prop", |
| ImmutableList.of("aaa", "bbb", "ccc"), Type.STRINGS); |
| root.commit(); |
| |
| assertQuery("//*[@prop != 'value']", "xpath", |
| ImmutableList.of("/content/two", "/content/three")); |
| } |
| |
| @Test |
| public void testMultiAndEquals() throws Exception { |
| Tree c = root.getTree("/").addChild("content"); |
| |
| c.addChild("one").setProperty("prop", "aaa"); |
| c.addChild("two").setProperty("prop", |
| ImmutableList.of("aaa", "bbb", "ccc"), Type.STRINGS); |
| c.addChild("three").setProperty("prop", ImmutableList.of("aaa", "bbb"), |
| Type.STRINGS); |
| root.commit(); |
| |
| assertQuery("//*[(@prop = 'aaa' and @prop = 'bbb' and @prop = 'ccc')]", |
| "xpath", ImmutableList.of("/content/two")); |
| } |
| |
| @Test |
| public void testMultiAndLike() throws Exception { |
| Tree c = root.getTree("/").addChild("content"); |
| |
| c.addChild("one").setProperty("prop", "aaaBoom"); |
| c.addChild("two").setProperty("prop", |
| ImmutableList.of("aaaBoom", "bbbBoom", "cccBoom"), Type.STRINGS); |
| c.addChild("three").setProperty("prop", ImmutableList.of("aaaBoom", "bbbBoom"), |
| Type.STRINGS); |
| root.commit(); |
| |
| assertQuery("//*[(jcr:like(@prop, 'aaa%') and jcr:like(@prop, 'bbb%') and jcr:like(@prop, 'ccc%'))]", |
| "xpath", ImmutableList.of("/content/two")); |
| } |
| |
| @Test |
| public void testSubPropertyMultiAndEquals() throws Exception { |
| Tree c = root.getTree("/").addChild("content"); |
| |
| c.addChild("one").addChild("child").setProperty("prop", "aaa"); |
| c.addChild("two") |
| .addChild("child") |
| .setProperty("prop", ImmutableList.of("aaa", "bbb", "ccc"), |
| Type.STRINGS); |
| c.addChild("three") |
| .addChild("child") |
| .setProperty("prop", ImmutableList.of("aaa", "bbb"), |
| Type.STRINGS); |
| root.commit(); |
| |
| assertQuery( |
| "//*[(child/@prop = 'aaa' and child/@prop = 'bbb' and child/@prop = 'ccc')]", |
| "xpath", ImmutableList.of("/content/two")); |
| } |
| |
| @Test |
| public void testSubPropertyMultiAndLike() throws Exception { |
| Tree c = root.getTree("/").addChild("content"); |
| |
| c.addChild("one").addChild("child").setProperty("prop", "aaaBoom"); |
| c.addChild("two") |
| .addChild("child") |
| .setProperty("prop", ImmutableList.of("aaaBoom", "bbbBoom", "cccBoom"), |
| Type.STRINGS); |
| c.addChild("three") |
| .addChild("child") |
| .setProperty("prop", ImmutableList.of("aaaBoom", "bbbBoom"), |
| Type.STRINGS); |
| root.commit(); |
| |
| assertQuery( |
| "//*[(jcr:like(child/@prop, 'aaa%') and jcr:like(child/@prop, 'bbb%') and jcr:like(child/@prop, 'ccc%'))]", |
| "xpath", ImmutableList.of("/content/two")); |
| } |
| |
| @Test |
| public void testOak1301() throws Exception { |
| Tree t1 = root.getTree("/").addChild("home").addChild("users") |
| .addChild("testing").addChild("socialgraph_test_user_4"); |
| t1.setProperty("jcr:primaryType", "rep:User"); |
| t1.setProperty("rep:authorizableId", "socialgraph_test_user_4"); |
| |
| Tree s = t1.addChild("social"); |
| s.setProperty("jcr:primaryType", "sling:Folder"); |
| |
| Tree r = s.addChild("relationships"); |
| r.setProperty("jcr:primaryType", "sling:Folder"); |
| |
| Tree f = r.addChild("friend"); |
| f.setProperty("jcr:primaryType", "sling:Folder"); |
| |
| Tree sg = f.addChild("socialgraph_test_group"); |
| sg.setProperty("jcr:primaryType", "nt:unstructured"); |
| sg.setProperty("id", "socialgraph_test_group"); |
| |
| Tree t2 = root.getTree("/").addChild("home").addChild("groups") |
| .addChild("testing").addChild("socialgraph_test_group"); |
| root.commit(); |
| |
| // select [jcr:path], [jcr:score], * from [nt:base] as a where [id/*] = |
| // 'socialgraph_test_group' and isdescendantnode(a, '/home') /* xpath: |
| // /jcr:root/home//*[id='socialgraph_test_group'] */ |
| assertQuery( |
| "/jcr:root/home//*[@id='socialgraph_test_group']", |
| "xpath", |
| ImmutableList |
| .of("/home/users/testing/socialgraph_test_user_4/social/relationships/friend/socialgraph_test_group")); |
| |
| // sql2 select c.[jcr:path] as [jcr:path], c.[jcr:score] as [jcr:score], |
| // c.* from [nt:base] as a inner join [nt:base] as b on ischildnode(b, |
| // a) inner join [nt:base] as c on isdescendantnode(c, b) where name(a) |
| // = 'social' and isdescendantnode(a, '/home') and name(b) = |
| // 'relationships' and c.[id/*] = 'socialgraph_test_group' /* xpath: |
| // /jcr:root/home//social/relationships//*[id='socialgraph_test_group'] |
| // */ |
| assertQuery( |
| "/jcr:root/home//social/relationships//*[@id='socialgraph_test_group']", |
| "xpath", |
| ImmutableList |
| .of("/home/users/testing/socialgraph_test_user_4/social/relationships/friend/socialgraph_test_group")); |
| |
| } |
| |
| @Test |
| public void testRelativeProperties() throws Exception { |
| root.getTree("/").addChild("content").addChild("node1") |
| .setProperty("prop", 128); |
| root.commit(); |
| |
| assertQuery("//*[(@prop > 1)]", "xpath", |
| ImmutableList.of("/content/node1")); |
| assertQuery("//*[(@prop > 2)]", "xpath", |
| ImmutableList.of("/content/node1")); |
| assertQuery("//*[(@prop > 20)]", "xpath", |
| ImmutableList.of("/content/node1")); |
| assertQuery("//*[(@prop > 100)]", "xpath", |
| ImmutableList.of("/content/node1")); |
| assertQuery("//*[(@prop > 200)]", "xpath", new ArrayList<String>()); |
| assertQuery("//*[(@prop > 1000)]", "xpath", new ArrayList<String>()); |
| |
| assertQuery("//*[(*/@prop > 1)]", "xpath", ImmutableList.of("/content")); |
| assertQuery("//*[(*/@prop > 2)]", "xpath", ImmutableList.of("/content")); |
| assertQuery("//*[(*/@prop > 20)]", "xpath", |
| ImmutableList.of("/content")); |
| assertQuery("//*[(*/@prop > 100)]", "xpath", |
| ImmutableList.of("/content")); |
| assertQuery("//*[(*/@prop > 200)]", "xpath", new ArrayList<String>()); |
| assertQuery("//*[(*/@prop > 1000)]", "xpath", new ArrayList<String>()); |
| } |
| |
| @Test // OAK-2062 |
| public void testRelativeProperties2() throws Exception { |
| Tree t = root.getTree("/").addChild("content").addChild("nodes"); |
| Tree a = t.addChild("a"); |
| a.setProperty(JCR_PRIMARYTYPE, "nt:unstructured", Type.NAME); |
| Tree b = a.addChild("b"); |
| b.setProperty(JCR_PRIMARYTYPE, "nt:unstructured", Type.NAME); |
| Tree c = b.addChild("c"); |
| c.setProperty(JCR_PRIMARYTYPE, "nt:unstructured", Type.NAME); |
| |
| Tree d1 = c.addChild("d1"); |
| d1.setProperty("prop", 10); |
| d1.setProperty(JCR_PRIMARYTYPE, "nt:unstructured", Type.NAME); |
| Tree d2 = c.addChild("d2"); |
| d2.setProperty("prop", 20); |
| d2.setProperty(JCR_PRIMARYTYPE, "nt:unstructured", Type.NAME); |
| Tree d3 = c.addChild("d3"); |
| d3.setProperty("prop", 30); |
| d3.setProperty(JCR_PRIMARYTYPE, "nt:unstructured", Type.NAME); |
| root.commit(); |
| |
| assertQuery("/jcr:root/content/nodes//*[(*/*/*/@prop >= 9)]", "xpath", |
| ImmutableList.of("/content/nodes/a")); |
| |
| assertQuery( |
| "/jcr:root/content/nodes//element(*, nt:unstructured)[(*/*/*/@prop >= 9)]", |
| "xpath", ImmutableList.of("/content/nodes/a")); |
| |
| assertQuery("/jcr:root/content/nodes//*[(*/*/*/@prop >= 10)]", "xpath", |
| ImmutableList.of("/content/nodes/a")); |
| assertQuery( |
| "/jcr:root/content/nodes//element(*, nt:unstructured)[(*/*/*/@prop >= 10)]", |
| "xpath", ImmutableList.of("/content/nodes/a")); |
| |
| assertQuery("/jcr:root/content/nodes//*[(*/*/*/@prop >= 15)]", "xpath", |
| ImmutableList.of("/content/nodes/a")); |
| assertQuery( |
| "/jcr:root/content/nodes//element(*, nt:unstructured)[(*/*/*/@prop >= 15)]", |
| "xpath", ImmutableList.of("/content/nodes/a")); |
| |
| assertQuery("/jcr:root/content/nodes//*[(*/*/*/@prop >= 20)]", "xpath", |
| ImmutableList.of("/content/nodes/a")); |
| assertQuery( |
| "/jcr:root/content/nodes//element(*, nt:unstructured)[(*/*/*/@prop >= 20)]", |
| "xpath", ImmutableList.of("/content/nodes/a")); |
| |
| assertQuery("/jcr:root/content/nodes//*[(*/*/*/@prop >= 30)]", "xpath", |
| ImmutableList.of("/content/nodes/a")); |
| assertQuery( |
| "/jcr:root/content/nodes//element(*, nt:unstructured)[(*/*/*/@prop >= 30)]", |
| "xpath", ImmutableList.of("/content/nodes/a")); |
| } |
| |
| /** |
| * tests range queries, long comparisons and relative properties |
| * @throws CommitFailedException |
| */ |
| @Test // OAK-2062 |
| public void testRangeRelativeProperties() throws CommitFailedException { |
| final List<String> emptyList = new ArrayList<String>(); |
| final String property = "prop"; |
| Tree contentNodes, t; |
| |
| contentNodes = root.getTree("/").addChild("content").addChild("nodes"); |
| |
| /* creating content structure |
| * content : { |
| * nodes : { |
| * a9 { |
| * b : { |
| * c : { |
| * d9 : { |
| * prop : 9 |
| * } |
| * } |
| * } |
| * }, |
| * a10 { |
| * b : { |
| * c : { |
| * d10 : { |
| * prop : 10 |
| * } |
| * } |
| * } |
| * }, |
| * a20 { |
| * b : { |
| * c : { |
| * d20 : { |
| * prop : 20 |
| * } |
| * } |
| * } |
| * }, |
| * a30 { |
| * b : { |
| * c : { |
| * d30 : { |
| * prop : 30 |
| * } |
| * } |
| * } |
| * }, |
| * } |
| * } |
| * |
| */ |
| t = addNtUnstructuredChild(contentNodes, "a9", null, null); |
| t = addNtUnstructuredChild(t, "b", null, null); |
| t = addNtUnstructuredChild(t, "c", null, null); |
| t = addNtUnstructuredChild(t, "d9", property, 9L); |
| t = addNtUnstructuredChild(contentNodes, "a10", null, null); |
| t = addNtUnstructuredChild(t, "b", null, null); |
| t = addNtUnstructuredChild(t, "c", null, null); |
| t = addNtUnstructuredChild(t, "d10", property, 10L); |
| t = addNtUnstructuredChild(contentNodes, "a20", null, null); |
| t = addNtUnstructuredChild(t, "b", null, null); |
| t = addNtUnstructuredChild(t, "c", null, null); |
| t = addNtUnstructuredChild(t, "d20", property, 20L); |
| t = addNtUnstructuredChild(contentNodes, "a30", null, null); |
| t = addNtUnstructuredChild(t, "b", null, null); |
| t = addNtUnstructuredChild(t, "c", null, null); |
| t = addNtUnstructuredChild(t, "d30", property, 30L); |
| |
| root.commit(); |
| |
| assertQuery("/jcr:root/content/nodes//*[(*/*/*/@prop >= 9)]", "xpath", |
| of("/content/nodes/a9", "/content/nodes/a10", "/content/nodes/a20", |
| "/content/nodes/a30")); |
| assertQuery("/jcr:root/content/nodes//*[(*/*/*/@prop >= 10)]", "xpath", |
| of("/content/nodes/a10", "/content/nodes/a20", "/content/nodes/a30")); |
| assertQuery("/jcr:root/content/nodes//*[(*/*/*/@prop >= 20)]", "xpath", |
| of("/content/nodes/a20", "/content/nodes/a30")); |
| assertQuery("/jcr:root/content/nodes//*[(*/*/*/@prop >= 30)]", "xpath", |
| of("/content/nodes/a30")); |
| assertQuery("/jcr:root/content/nodes//*[(*/*/*/@prop >= 40)]", "xpath", emptyList); |
| |
| assertQuery("/jcr:root/content/nodes//*[(*/*/*/@prop <= 8)]", "xpath", emptyList); |
| assertQuery("/jcr:root/content/nodes//*[(*/*/*/@prop <= 9)]", "xpath", |
| of("/content/nodes/a9")); |
| assertQuery("/jcr:root/content/nodes//*[(*/*/*/@prop <= 10)]", "xpath", |
| of("/content/nodes/a9", "/content/nodes/a10")); |
| assertQuery("/jcr:root/content/nodes//*[(*/*/*/@prop <= 20)]", "xpath", |
| of("/content/nodes/a9", "/content/nodes/a10", "/content/nodes/a20")); |
| assertQuery("/jcr:root/content/nodes//*[(*/*/*/@prop <= 30)]", "xpath", |
| of("/content/nodes/a9", "/content/nodes/a10", "/content/nodes/a20", |
| "/content/nodes/a30")); |
| |
| assertQuery( |
| "/jcr:root/content/nodes//element(*, nt:unstructured)[(*/*/*/@prop >= 9)]", |
| "xpath", |
| of("/content/nodes/a9", "/content/nodes/a10", "/content/nodes/a20", |
| "/content/nodes/a30")); |
| assertQuery("/jcr:root/content/nodes//element(*, nt:unstructured)[(*/*/*/@prop >= 10)]", |
| "xpath", of("/content/nodes/a10", "/content/nodes/a20", "/content/nodes/a30")); |
| assertQuery("/jcr:root/content/nodes//element(*, nt:unstructured)[(*/*/*/@prop >= 20)]", |
| "xpath", of("/content/nodes/a20", "/content/nodes/a30")); |
| assertQuery("/jcr:root/content/nodes//element(*, nt:unstructured)[(*/*/*/@prop >= 30)]", |
| "xpath", of("/content/nodes/a30")); |
| assertQuery("/jcr:root/content/nodes//element(*, nt:unstructured)[(*/*/*/@prop >= 40)]", |
| "xpath", emptyList); |
| |
| assertQuery("/jcr:root/content/nodes//element(*, nt:unstructured)[(*/*/*/@prop <= 8)]", |
| "xpath", emptyList); |
| assertQuery("/jcr:root/content/nodes//element(*, nt:unstructured)[(*/*/*/@prop <= 9)]", |
| "xpath", of("/content/nodes/a9")); |
| assertQuery("/jcr:root/content/nodes//element(*, nt:unstructured)[(*/*/*/@prop <= 10)]", |
| "xpath", of("/content/nodes/a9", "/content/nodes/a10")); |
| assertQuery("/jcr:root/content/nodes//element(*, nt:unstructured)[(*/*/*/@prop <= 20)]", |
| "xpath", of("/content/nodes/a9", "/content/nodes/a10", "/content/nodes/a20")); |
| assertQuery( |
| "/jcr:root/content/nodes//element(*, nt:unstructured)[(*/*/*/@prop <= 30)]", |
| "xpath", |
| of("/content/nodes/a9", "/content/nodes/a10", "/content/nodes/a20", |
| "/content/nodes/a30")); |
| } |
| |
| /** |
| * adds a child of type {@link JcrConstants#NT_UNSTRUCTURED} under the provided {@code parent} |
| * with the provided {@code name} and an optional {@code propertyName} and {@code value}. If |
| * either {@code propertyName} or {@code value} are null the property won't be set. |
| * |
| * @param parent |
| * @param name |
| * @param propertyName |
| * @param value |
| * @return |
| */ |
| private static Tree addNtUnstructuredChild(@NotNull final Tree parent, |
| @NotNull final String name, |
| @Nullable final String propertyName, |
| @Nullable final Long value) { |
| checkNotNull(parent); |
| checkNotNull(name); |
| |
| Tree ret = parent.addChild(name); |
| ret.setProperty(JCR_PRIMARYTYPE, NT_UNSTRUCTURED, NAME); |
| |
| if (propertyName != null && value != null) { |
| ret.setProperty(propertyName, value, LONG); |
| } |
| |
| return ret; |
| } |
| |
| @Test |
| public void testMultipleRelativeProperties() throws Exception { |
| Tree content = root.getTree("/").addChild("content"); |
| |
| content.addChild("node1").setProperty("a", 128); |
| content.addChild("node2").setProperty("a", "abc"); |
| content.addChild("node3").setProperty("a", "1280"); |
| |
| content.addChild("node1").setProperty("b", 128); |
| content.addChild("node2").setProperty("b", 1024); |
| content.addChild("node3").setProperty("b", 2048); |
| |
| content.addChild("node1").setProperty("c", 10.3); |
| content.addChild("node2").setProperty("c", -10.3); |
| content.addChild("node3").setProperty("c", 9.8); |
| |
| content.addChild("node1").setProperty("d", Arrays.asList("x", "y"), Type.STRINGS); |
| content.addChild("node2").setProperty("d", 10); |
| content.addChild("node3").setProperty("d", Arrays.asList(1L, 2L), Type.LONGS); |
| |
| root.commit(); |
| |
| assertQuery("//*[*/@a > 2]", "xpath", Arrays.asList("/content")); |
| assertQuery("//*[*/@a > '1']", "xpath", Arrays.asList("/content")); |
| assertQuery("//*[*/@a > 'abd']", "xpath", Arrays.<String>asList()); |
| assertQuery("//*[*/@a = 'abc']", "xpath", Arrays.asList("/content")); |
| // this may be unexpected: it is evaluated as |
| // ['128', 'abc', '1280'] >= 'abc' |
| assertQuery("//*[*/@a >= 'abc']", "xpath", Arrays.asList("/content")); |
| |
| assertQuery("//*[*/@b > 2]", "xpath", Arrays.asList("/content")); |
| assertQuery("//*[*/@b > 2048]", "xpath", Arrays.<String>asList()); |
| assertQuery("//*[*/@b > '1']", "xpath", Arrays.asList("/content")); |
| assertQuery("//*[*/@b = 128]", "xpath", Arrays.asList("/content")); |
| |
| assertQuery("//*[*/@c > 10]", "xpath", Arrays.asList("/content")); |
| assertQuery("//*[*/@c > 11]", "xpath", Arrays.<String>asList()); |
| assertQuery("//*[*/@c > '1']", "xpath", Arrays.asList("/content")); |
| assertQuery("//*[*/@c = 9.8]", "xpath", Arrays.asList("/content")); |
| |
| assertQuery("//*[*/@d > 10]", "xpath", Arrays.asList("/content")); |
| assertQuery("//*[*/@d > 11]", "xpath", Arrays.asList("/content")); |
| assertQuery("//*[*/@d > '1']", "xpath", Arrays.asList("/content")); |
| assertQuery("//*[*/@d = 10]", "xpath", Arrays.asList("/content")); |
| // this may be unexpected: it is evaluated as |
| // ['x', 'y', '10', '1', '2'] < '3' |
| assertQuery("//*[*/@d < 3]", "xpath", Arrays.asList("/content")); |
| } |
| |
| @Test |
| public void testLowercaseOnArrays() throws Exception { |
| // OAK-1829 |
| Tree content = root.getTree("/").addChild("content"); |
| content.setProperty("array", Arrays.asList("X", "Y"), Type.STRINGS); |
| root.commit(); |
| assertQuery("//*[jcr:like(fn:lower-case(@array), '%x%')]", "xpath", |
| Arrays.asList("/content")); |
| } |
| |
| } |