SLING-4381 switch to Couchbase 4.0 with N1QL to get rid of couchbase views

git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1703076 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/README.md b/README.md
index 4fd582b..6b88a77 100644
--- a/README.md
+++ b/README.md
@@ -5,6 +5,8 @@
 

 Based on the "Apache Sling NoSQL Generic Resource Provider" and "Apache Sling NoSQL Couchbase Client".

 

+Couchbase Server 4.0 with N1QL support is required for this implementation.

+

 

 Configuration on deployment

 ---------------------------

@@ -13,21 +15,6 @@
 * Additionally a factory configuration for "Apache Sling NoSQL Couchbase Resource Provider Factory" defines the root of the resource tree that should be stored in Couchbase

 

 

-Couchbase Views for path-based access

--------------------------------------

-

-For list and delete operations two couchbase views have to be defined and published in the bucket that is used by the resource provider.

-

-Steps to create those views:

-* Log into Couchbase Console

-* Go to "Views" and select the correct bucket

-* Add a new design document via "Create Development View" and name it "\_design/dev\_resourceIndex" (the prefix "\_design/dev\_" is added automatically)

-* Use the name "ancestorPath" for the first view that is created together with the design document

-* Paste the view code from [ancestorPath.js](src/main/couchbase-views/ancestorPath.js) into the editor and save it

-* Create another view named "parentPath", paste the view code from [parentPath.js](src/main/couchbase-views/parentPath.js) and save it

-* Publish the design document so the views are production views

-

-

 Run integration tests

 ---------------------

 

@@ -36,4 +23,3 @@
 ```

 mvn -Pcouchbase-integration-test -DcouchbaseHosts=localhost:8091 -DbucketName=test integration-test

 ```

-

diff --git a/pom.xml b/pom.xml
index 07c509d..49fc370 100644
--- a/pom.xml
+++ b/pom.xml
@@ -67,7 +67,14 @@
             <version>2.2.2</version>
             <scope>provided</scope>
         </dependency>
-          
+
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+            <version>3.3.2</version>
+            <scope>provided</scope>
+        </dependency>
+
         <dependency>
             <groupId>org.apache.sling</groupId>
             <artifactId>org.apache.sling.testing.sling-mock</artifactId>
diff --git a/src/main/couchbase-views/ancestorPath.js b/src/main/couchbase-views/ancestorPath.js
deleted file mode 100644
index f8cd94b..0000000
--- a/src/main/couchbase-views/ancestorPath.js
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * 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.
- */
-/*
- * Emits for each document the all parent paths - allowing to fetch children and their decendants by path.
- * Includes the path of the item itself.
- */
-function(doc, meta) {
-  
-  // handle only sling resource documents with a valid path
-  if (!(meta.id.indexOf("sling-resource:")==0 && doc.path && doc.data)) {
-    return;
-  }
-  var pathParts = doc.path.split("/");
-  if (pathParts.length < 3) {
-    return;
-  }
-  
-  while (pathParts.length >= 2) {
-    // remove last element to get parent path
-    var parentPath = pathParts.join("/");
-    emit(parentPath, null);
-    pathParts.pop();
-  }
-}
diff --git a/src/main/couchbase-views/ancestorPathTester.html b/src/main/couchbase-views/ancestorPathTester.html
deleted file mode 100644
index 9933e6f..0000000
--- a/src/main/couchbase-views/ancestorPathTester.html
+++ /dev/null
@@ -1,84 +0,0 @@
-<!DOCTYPE html>
-<!--
- * 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.
--->
-<html>
-  <head>
-    <title>Couchbase View Tester</title>
-    <style>body { font-family: Courier }</style>
-  </head>
-  <body>
-  
-<script>
-
-var emit = function(key, value) {
-  document.write("[" + key + "]" + "<br/>");
-}
-
-var testFunction = function(doc, meta) {
-  
-  // handle only sling resource documents with a valid path
-  if (!(meta.id.indexOf("sling-resource:")==0 && doc.path && doc.data)) {
-    return;
-  }
-  var pathParts = doc.path.split("/");
-  if (pathParts.length < 3) {
-    return;
-  }
-  
-  while (pathParts.length >= 2) {
-    // remove last element to get parent path
-    var parentPath = pathParts.join("/");
-    emit(parentPath, null);
-    pathParts.pop();
-  }
-};
-
-var testInput = [
-	  null,
-    "",
-    "abc",
-    "/",
-    "/content",
-    "/content/node1",
-    "/content/node1/node2",
-    "/content/node1/node2/node3",
-    "/content/node1/node2/node3/node4"
-];
-  
-</script>
-    
-    <table border="1">
-      <tr>
-	      <th>Input</th>
-	      <th>Output</th>
-	    </tr>
-<script>
-for (var i=0; i < testInput.length; i++) {
-  document.write("<tr>")
-  document.write("<td>" + testInput[i] + "</td>")
-  document.write("<td>")
-  testFunction({path: testInput[i], data: {}}, {id: "sling-resource:doc" + i});
-  document.write("</td>")
-  document.write("</tr>")
-}
-</script>
-    </table>
-  
-  </body>
-</html>
diff --git a/src/main/couchbase-views/parentPath.js b/src/main/couchbase-views/parentPath.js
deleted file mode 100644
index 54c4c1f..0000000
--- a/src/main/couchbase-views/parentPath.js
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * 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.
- */
-/*
- * Emits for each document the direct parent path - allowing to fetch direct children by path.
- */
-function(doc, meta) {
-  
-  // handle only sling resource documents with a valid path
-  if (!(meta.id.indexOf("sling-resource:")==0 && doc.path && doc.data)) {
-    return;
-  }
-  var pathParts = doc.path.split("/");
-  if (pathParts.length < 3) {
-    return;
-  }
-  
-  // remove last element to get parent path
-  pathParts.pop();
-  var parentPath = pathParts.join("/");
-  emit(parentPath, null);
-}
diff --git a/src/main/couchbase-views/parentPathTester.html b/src/main/couchbase-views/parentPathTester.html
deleted file mode 100644
index efcc2d1..0000000
--- a/src/main/couchbase-views/parentPathTester.html
+++ /dev/null
@@ -1,82 +0,0 @@
-<!DOCTYPE html>
-<!--
- * 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.
--->
-<html>
-  <head>
-    <title>Couchbase View Tester</title>
-    <style>body { font-family: Courier }</style>
-  </head>
-  <body>
-  
-<script>
-
-var emit = function(key, value) {
-  document.write("[" + key + "]" + "<br/>");
-}
-
-var testFunction = function(doc, meta) {
-  
-  // handle only sling resource documents with a valid path
-  if (!(meta.id.indexOf("sling-resource:")==0 && doc.path && doc.data)) {
-    return;
-  }
-  var pathParts = doc.path.split("/");
-  if (pathParts.length < 3) {
-    return;
-  }
-  
-  // remove last element to get parent path
-  pathParts.pop();
-  var parentPath = pathParts.join("/");
-  emit(parentPath, null);
-};
-
-var testInput = [
-	  null,
-    "",
-    "abc",
-    "/",
-    "/content",
-    "/content/node1",
-    "/content/node1/node2",
-    "/content/node1/node2/node3",
-    "/content/node1/node2/node3/node4"
-];
-  
-</script>
-    
-    <table border="1">
-      <tr>
-	      <th>Input</th>
-	      <th>Output</th>
-	    </tr>
-<script>
-for (var i=0; i < testInput.length; i++) {
-  document.write("<tr>")
-  document.write("<td>" + testInput[i] + "</td>")
-  document.write("<td>")
-  testFunction({path: testInput[i], data: {}}, {id: "sling-resource:doc" + i});
-  document.write("</td>")
-  document.write("</tr>")
-}
-</script>
-    </table>
-  
-  </body>
-</html>
diff --git a/src/main/java/org/apache/sling/nosql/couchbase/resourceprovider/impl/CouchbaseNoSqlAdapter.java b/src/main/java/org/apache/sling/nosql/couchbase/resourceprovider/impl/CouchbaseNoSqlAdapter.java
index 5ebcc6f..fd4a798 100644
--- a/src/main/java/org/apache/sling/nosql/couchbase/resourceprovider/impl/CouchbaseNoSqlAdapter.java
+++ b/src/main/java/org/apache/sling/nosql/couchbase/resourceprovider/impl/CouchbaseNoSqlAdapter.java
@@ -18,8 +18,12 @@
  */
 package org.apache.sling.nosql.couchbase.resourceprovider.impl;
 
-import java.util.Iterator;
+import static com.couchbase.client.java.query.Select.select;
 
+import java.util.Iterator;
+import java.util.regex.Pattern;
+
+import org.apache.commons.lang3.StringUtils;
 import org.apache.sling.nosql.couchbase.client.CouchbaseClient;
 import org.apache.sling.nosql.couchbase.client.CouchbaseKey;
 import org.apache.sling.nosql.generic.adapter.AbstractNoSqlAdapter;
@@ -30,9 +34,11 @@
 import com.couchbase.client.java.document.JsonDocument;
 import com.couchbase.client.java.document.json.JsonObject;
 import com.couchbase.client.java.error.DocumentAlreadyExistsException;
-import com.couchbase.client.java.view.Stale;
-import com.couchbase.client.java.view.ViewQuery;
-import com.couchbase.client.java.view.ViewRow;
+import com.couchbase.client.java.query.N1qlParams;
+import com.couchbase.client.java.query.N1qlQuery;
+import com.couchbase.client.java.query.N1qlQueryResult;
+import com.couchbase.client.java.query.N1qlQueryRow;
+import com.couchbase.client.java.query.consistency.ScanConsistency;
 
 /**
  * {@link org.apache.sling.nosql.generic.adapter.NoSqlAdapter} implementation for Couchbase.
@@ -49,16 +55,18 @@
      */
     public static final String PN_DATA = "data";
 
-    private static final String VIEW_DESIGN_DOCUMENT = "resourceIndex";
-    private static final String VIEW_PARENT_PATH = "parentPath";
-    private static final String VIEW_ANCESTOR_PATH = "ancestorPath";
-
     private final CouchbaseClient couchbaseClient;
     private final String cacheKeyPrefix;
+    
+    private static final N1qlParams N1QL_PARAMS = N1qlParams.build().consistency(ScanConsistency.REQUEST_PLUS);
 
     public CouchbaseNoSqlAdapter(CouchbaseClient couchbaseClient, String cacheKeyPrefix) {
         this.couchbaseClient = couchbaseClient;
         this.cacheKeyPrefix = cacheKeyPrefix;
+        
+        // make sure primary index is present - ignore error if it is already present
+        Bucket bucket = couchbaseClient.getBucket();
+        bucket.query(N1qlQuery.simple("CREATE PRIMARY INDEX ON " + couchbaseClient.getBucketName()));
     }
 
     @Override
@@ -89,8 +97,14 @@
     public Iterator<NoSqlData> getChildren(String parentPath) {
         Bucket bucket = couchbaseClient.getBucket();
         // fetch all direct children of this path
-        final Iterator<ViewRow> results = bucket.query(
-                ViewQuery.from(VIEW_DESIGN_DOCUMENT, VIEW_PARENT_PATH).key(parentPath).stale(Stale.FALSE)).rows();
+        Pattern directChildren = Pattern.compile("^" + parentPath + "/[^/]+$");
+        N1qlQuery query = N1qlQuery.simple(select("*")
+                .from(couchbaseClient.getBucketName())
+                .where("REGEXP_LIKE(`" + PN_PATH + "`, '" + directChildren.pattern() + "')"),
+                N1QL_PARAMS);
+        N1qlQueryResult queryResult = bucket.query(query);
+        handleQueryError(queryResult);
+        final Iterator<N1qlQueryRow> results = queryResult.iterator();
         return new Iterator<NoSqlData>() {
             @Override
             public boolean hasNext() {
@@ -99,8 +113,8 @@
 
             @Override
             public NoSqlData next() {
-                JsonDocument doc = results.next().document();
-                JsonObject envelope = doc.content();
+                JsonObject item = results.next().value();
+                JsonObject envelope = item.getObject(couchbaseClient.getBucketName());
                 String path = envelope.getString(PN_PATH);
                 JsonObject data = envelope.getObject(PN_DATA);
                 return new NoSqlData(path, data.toMap(), MultiValueMode.LISTS);
@@ -136,16 +150,34 @@
     @Override
     public boolean deleteRecursive(String path) {
         Bucket bucket = couchbaseClient.getBucket();
-        // fetch referenced item and all descendants
-        Iterator<ViewRow> results = bucket.query(
-                ViewQuery.from(VIEW_DESIGN_DOCUMENT, VIEW_ANCESTOR_PATH).key(path).stale(Stale.FALSE)).rows();
+        // fetch all descendants and self for deletion
+        Pattern descendantsAndSelf = Pattern.compile("^" + path + "(/.+)?$");
+        N1qlQuery query = N1qlQuery.simple(select("*")
+                .from(couchbaseClient.getBucketName())
+                .where("REGEXP_LIKE(`" + PN_PATH + "`, '" + descendantsAndSelf.pattern() + "')"),
+                N1QL_PARAMS);
+        N1qlQueryResult queryResult = bucket.query(query);
+        handleQueryError(queryResult);
+        final Iterator<N1qlQueryRow> results = queryResult.iterator();
         boolean deletedAny = false;
         while (results.hasNext()) {
-            ViewRow result = results.next();
-            bucket.remove(result.document());
+            JsonObject item = results.next().value();
+            JsonObject envelope = item.getObject(couchbaseClient.getBucketName());
+            String itemPath = envelope.getString(PN_PATH);
+            String itemCacheKey = CouchbaseKey.build(itemPath, cacheKeyPrefix);
+            bucket.remove(itemCacheKey);
             deletedAny = true;
         }
         return deletedAny;
     }
+    
+    private void handleQueryError(N1qlQueryResult queryResult) {
+        if (!queryResult.parseSuccess()) {
+            throw new RuntimeException("Couchbase query parsing error: " + StringUtils.join(queryResult.errors(), "\n"));
+        }
+        if (!queryResult.finalSuccess()) {
+            throw new RuntimeException("Couchbase query error: " + StringUtils.join(queryResult.errors(), "\n"));
+        }
+    }
 
 }