Skip to content

Commit d30e401

Browse files
chedimnebasuke
andauthored
DEVADV-1978: adds transaction example (#25)
* Devadv 1645 improve and fix current java springboot tutorial (#5) * Change tests from unit tests to integration tests. This prevents the mvn package from crashing when not running Couchbase Server locally. * Code cleanup: removal of unused code/imports etc. * Various code improvements. Adapt Swagger codes and exception catching to match actual behaviour. Documentation fixes. Change bucket name to not conflict with other Couchbase tutorials. Remove unneeded imports. * Add MIT license. * DEVADV-1978: adds transaction example Co-authored-by: Bas van Gijzel <[email protected]> Co-authored-by: chedim <>
1 parent b2a4849 commit d30e401

File tree

5 files changed

+165
-26
lines changed

5 files changed

+165
-26
lines changed

src/main/java/org/couchbase/quickstart/configs/CouchbaseConfig.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import com.couchbase.client.java.ClusterOptions;
77
import com.couchbase.client.java.manager.bucket.BucketSettings;
88
import com.couchbase.client.java.manager.bucket.BucketType;
9+
910
import org.springframework.beans.factory.annotation.Autowired;
1011
import org.springframework.context.annotation.Bean;
1112
import org.springframework.context.annotation.Configuration;
@@ -64,5 +65,4 @@ public Bucket getCouchbaseBucket(Cluster cluster) {
6465
}
6566
return cluster.bucket(dbProp.getBucketName());
6667
}
67-
6868
}

src/main/java/org/couchbase/quickstart/controllers/ProfileController.java

Lines changed: 91 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,42 @@
11
package org.couchbase.quickstart.controllers;
22

3+
import static org.couchbase.quickstart.configs.CollectionNames.PROFILE;
4+
5+
import java.util.Arrays;
6+
import java.util.List;
7+
import java.util.UUID;
8+
39
import com.couchbase.client.core.error.DocumentNotFoundException;
10+
import com.couchbase.client.core.msg.kv.DurabilityLevel;
411
import com.couchbase.client.java.Bucket;
512
import com.couchbase.client.java.Cluster;
613
import com.couchbase.client.java.Collection;
14+
import com.couchbase.client.java.json.JsonObject;
15+
import com.couchbase.client.java.query.QueryOptions;
716
import com.couchbase.client.java.query.QueryScanConsistency;
8-
import io.swagger.annotations.ApiOperation;
9-
import io.swagger.annotations.ApiResponse;
10-
import io.swagger.annotations.ApiResponses;
17+
import com.couchbase.client.java.transactions.TransactionQueryOptions;
18+
import com.couchbase.client.java.transactions.config.TransactionOptions;
19+
1120
import org.couchbase.quickstart.configs.DBProperties;
1221
import org.couchbase.quickstart.models.Profile;
1322
import org.couchbase.quickstart.models.ProfileRequest;
1423
import org.springframework.http.HttpStatus;
1524
import org.springframework.http.MediaType;
1625
import org.springframework.http.ResponseEntity;
17-
import org.springframework.web.bind.annotation.*;
26+
import org.springframework.web.bind.annotation.CrossOrigin;
27+
import org.springframework.web.bind.annotation.DeleteMapping;
28+
import org.springframework.web.bind.annotation.GetMapping;
29+
import org.springframework.web.bind.annotation.PathVariable;
30+
import org.springframework.web.bind.annotation.PostMapping;
31+
import org.springframework.web.bind.annotation.PutMapping;
32+
import org.springframework.web.bind.annotation.RequestBody;
33+
import org.springframework.web.bind.annotation.RequestMapping;
34+
import org.springframework.web.bind.annotation.RequestParam;
35+
import org.springframework.web.bind.annotation.RestController;
1836

19-
import java.util.List;
20-
import java.util.UUID;
21-
22-
import static com.couchbase.client.java.query.QueryOptions.queryOptions;
23-
import static org.couchbase.quickstart.configs.CollectionNames.PROFILE;
37+
import io.swagger.annotations.ApiOperation;
38+
import io.swagger.annotations.ApiResponse;
39+
import io.swagger.annotations.ApiResponses;
2440

2541
@RestController
2642
@RequestMapping("/api/v1/profile")
@@ -29,10 +45,12 @@ public class ProfileController {
2945
private Cluster cluster;
3046
private Collection profileCol;
3147
private DBProperties dbProperties;
48+
private Bucket bucket;
3249

3350
public ProfileController(Cluster cluster, Bucket bucket, DBProperties dbProperties) {
3451
System.out.println("Initializing profile controller, cluster: " + cluster + "; bucket: " + bucket);
3552
this.cluster = cluster;
53+
this.bucket = bucket;
3654
this.profileCol = bucket.collection(PROFILE);
3755
this.dbProperties = dbProperties;
3856
}
@@ -82,6 +100,8 @@ public ResponseEntity<Profile> update(@PathVariable("id") UUID id, @RequestBody
82100
try {
83101
profileCol.upsert(id.toString(), profile);
84102
return ResponseEntity.status(HttpStatus.CREATED).body(profile);
103+
} catch (DocumentNotFoundException dnfe) {
104+
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
85105
} catch (Exception e){
86106
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null);
87107
}
@@ -127,8 +147,69 @@ public ResponseEntity<List<Profile>> getProfiles(
127147
//TBD with params: final List<Profile> profiles = cluster.query("SELECT p.* FROM `$bucketName`.`_default`.`$collectionName` p WHERE lower(p.firstName) LIKE '$search' OR lower(p.lastName) LIKE '$search' LIMIT $limit OFFSET $skip",
128148
final List<Profile> profiles =
129149
cluster.query(qryString,
130-
queryOptions().scanConsistency(QueryScanConsistency.REQUEST_PLUS))
150+
QueryOptions.queryOptions().scanConsistency(QueryScanConsistency.REQUEST_PLUS))
131151
.rowsAs(Profile.class);
132152
return ResponseEntity.status(HttpStatus.OK).body(profiles);
133153
}
154+
155+
@CrossOrigin(value="*")
156+
@PostMapping(path = "/transfer", produces = MediaType.APPLICATION_JSON_VALUE)
157+
@ApiOperation(value = "Transfer credits between 2 profiles", produces = MediaType.APPLICATION_JSON_VALUE)
158+
@ApiResponses(
159+
value = {
160+
@ApiResponse(code = 200, message = "Returns the list of changed user profiles"),
161+
@ApiResponse(code = 500, message = "Error occurred while transfer operation", response = Error.class)
162+
})
163+
public ResponseEntity transferCredits(
164+
@RequestParam(name="source", required=true) String sourceProfileId,
165+
@RequestParam(name="target", required=true) String targetProfileId,
166+
@RequestParam(name="amount", required=true) Integer amount
167+
) {
168+
169+
Profile sourceProfile = profileCol.get(sourceProfileId).contentAs(Profile.class),
170+
targetProfile = profileCol.get(targetProfileId).contentAs(Profile.class);
171+
172+
if (sourceProfile == null) {
173+
return ResponseEntity.status(500).body("Source profile not found");
174+
}
175+
if (targetProfile == null) {
176+
return ResponseEntity.status(500).body("Target profile not found");
177+
}
178+
179+
if (sourceProfile.getBalance() < amount) {
180+
return ResponseEntity.status(500).body("Insufficient balance");
181+
}
182+
183+
TransactionOptions to = TransactionOptions.transactionOptions();
184+
TransactionQueryOptions args = TransactionQueryOptions.queryOptions().parameters(
185+
JsonObject.create()
186+
.put("source", sourceProfileId)
187+
.put("amount", amount)
188+
.put("target", targetProfileId)
189+
);
190+
191+
while(true) {
192+
try {
193+
cluster.transactions().run(ctx -> {
194+
195+
ctx.query("UPDATE `"+dbProperties.getBucketName()+"`.`_default`.`"+PROFILE+"` SET balance = balance - $amount WHERE pid = $source", args);
196+
ctx.query("UPDATE `"+dbProperties.getBucketName()+"`.`_default`.`"+PROFILE+"` SET balance = balance + $amount WHERE pid = $target", args);
197+
}, to);
198+
199+
break;
200+
} catch (Exception e) {
201+
if (e.getMessage().contains("DurabilityImpossible")) {
202+
// Sets DurabilityLevel.NONE to make it work with local clusters
203+
to.durabilityLevel(DurabilityLevel.NONE);
204+
} else {
205+
throw e;
206+
}
207+
}
208+
}
209+
210+
211+
212+
return ResponseEntity.ok(Arrays.asList(sourceProfile, targetProfile));
213+
}
214+
134215
}

src/main/java/org/couchbase/quickstart/models/Profile.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
public class Profile {
66
private String pid;
77
private String firstName, lastName, email, password;
8+
private Integer balance;
89

910
public String getPid() { return pid; }
1011
public void setPid(String pid) { this.pid = pid; }
@@ -23,14 +24,20 @@ public void setPassword(String password) {
2324
this.password = BCrypt.hashpw(password, BCrypt.gensalt());
2425
}
2526

27+
public Integer getBalance() { return balance; }
28+
public void setBalance(Integer balance) {
29+
this.balance = balance;
30+
}
31+
2632
public Profile() { }
2733

28-
public Profile(String pid, String firstName, String lastName, String email, String password){
34+
public Profile(String pid, String firstName, String lastName, String email, String password, Integer balance){
2935
this.pid = pid;
3036
this.firstName = firstName;
3137
this.lastName = lastName;
3238
this.email = email;
3339
this.password = password;
40+
this.balance = balance;
3441
}
3542

3643
public Profile(Profile profile) {
@@ -39,11 +46,10 @@ public Profile(Profile profile) {
3946
this.lastName = profile.getLastName();
4047
this.email = profile.getEmail();
4148
this.password = profile.getPassword();
49+
this.balance = profile.getBalance();
4250
}
4351

4452
public String toString() {
45-
return "Profile: { pid="+this.pid+",firstName="+this.firstName+",lastName="+this.lastName+",email="+this.email+",password="+this.password;
53+
return "Profile: { pid="+this.pid+",firstName="+this.firstName+",lastName="+this.lastName+",email="+this.email+",password="+this.password+",balance="+this.balance+" }";
4654
}
47-
48-
4955
}

src/main/java/org/couchbase/quickstart/models/ProfileRequest.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
public class ProfileRequest {
66

77
private String firstName, lastName, email, password;
8+
private Integer balance;
89

910
public String getFirstName() { return firstName; }
1011
public void setFirstName(String firstName) { this.firstName = firstName; }
@@ -20,17 +21,22 @@ public void setPassword(String password) {
2021
this.password = password;
2122
}
2223

23-
public ProfileRequest() { }
24+
public Integer getBalance() { return balance; }
25+
public void setBalance(Integer balance) {
26+
this.balance = balance;
27+
}
2428

25-
public ProfileRequest(String firstName, String lastName, String email, String password){
29+
public ProfileRequest() { }
2630

31+
public ProfileRequest(String firstName, String lastName, String email, String password, Integer balance){
2732
this.firstName = firstName;
2833
this.lastName = lastName;
2934
this.email = email;
3035
this.password = password;
36+
this.balance = balance;
3137
}
3238

3339
public Profile getProfile() {
34-
return new Profile(UUID.randomUUID().toString(), firstName, lastName, email, password);
40+
return new Profile(UUID.randomUUID().toString(), firstName, lastName, email, password, balance);
3541
}
3642
}

src/test/java/org/couchbase/quickstart/userProfile/UserProfileIntegrationTest.java

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
package org.couchbase.quickstart.userProfile;
22

3+
import static org.junit.Assert.assertEquals;
4+
import static org.junit.Assert.assertNotEquals;
5+
import static org.junit.Assert.assertNotNull;
6+
7+
import java.util.List;
8+
import java.util.UUID;
9+
310
import com.couchbase.client.core.error.DocumentNotFoundException;
411
import com.couchbase.client.java.Bucket;
512
import com.couchbase.client.java.Cluster;
613
import com.couchbase.client.java.json.JsonObject;
14+
715
import org.couchbase.quickstart.configs.CollectionNames;
816
import org.couchbase.quickstart.configs.DBProperties;
917
import org.couchbase.quickstart.models.Profile;
@@ -13,7 +21,6 @@
1321
import org.junit.After;
1422
import org.junit.Rule;
1523
import org.junit.Test;
16-
import org.junit.jupiter.api.AfterEach;
1724
import org.junit.rules.ExpectedException;
1825
import org.junit.runner.RunWith;
1926
import org.springframework.beans.factory.annotation.Autowired;
@@ -23,11 +30,8 @@
2330
import org.springframework.test.context.junit4.SpringRunner;
2431
import org.springframework.test.web.reactive.server.EntityExchangeResult;
2532
import org.springframework.test.web.reactive.server.WebTestClient;
26-
27-
import java.util.List;
28-
import java.util.UUID;
29-
30-
import static org.junit.Assert.*;
33+
import org.springframework.test.web.reactive.server.WebTestClient.ResponseSpec;
34+
import org.springframework.web.reactive.function.BodyInserters;
3135

3236

3337
@RunWith(SpringRunner.class)
@@ -161,13 +165,53 @@ public void testDeleteUserProfile() {
161165
bucket.collection(CollectionNames.PROFILE).get(testProfile.getPid());
162166
}
163167

168+
@Test
169+
public void testTransferCredits() {
170+
Profile sourceProfile = getTestProfile(),
171+
targetProfile = getTestProfile();
172+
173+
bucket.collection(CollectionNames.PROFILE).insert(sourceProfile.getPid(), sourceProfile);
174+
bucket.collection(CollectionNames.PROFILE).insert(targetProfile.getPid(), targetProfile);
175+
176+
// transfer credits
177+
callTransferCredits(sourceProfile, targetProfile, 100)
178+
.expectStatus().isOk();
179+
180+
// attempt to transfer credits again -- should fail
181+
callTransferCredits(sourceProfile, targetProfile, 100)
182+
.expectStatus().is5xxServerError();
183+
184+
sourceProfile = bucket.collection(CollectionNames.PROFILE).get(sourceProfile.getPid()).contentAs(Profile.class);
185+
targetProfile = bucket.collection(CollectionNames.PROFILE).get(targetProfile.getPid()).contentAs(Profile.class);
186+
187+
assertNotNull(sourceProfile);
188+
assertNotNull(targetProfile);
189+
190+
assertEquals((Integer)0, sourceProfile.getBalance());
191+
assertEquals((Integer)200, targetProfile.getBalance());
192+
}
193+
194+
private ResponseSpec callTransferCredits(Profile sourceProfile, Profile targetProfile, Integer amount) {
195+
return webTestClient.post()
196+
.uri("/api/v1/profile/transfer")
197+
// .contentType(MediaType.APPLICATION_FORM_URLENCODED)
198+
.body(BodyInserters
199+
.fromFormData("source", sourceProfile.getPid())
200+
.with("target", targetProfile.getPid())
201+
.with("amount", amount.toString())
202+
)
203+
//.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
204+
.exchange();
205+
}
206+
164207
private String getCreatedUserJson(ProfileRequest profile) {
165208
//create json to post to integration test
166209
return JsonObject.create()
167210
.put("firstName", profile.getFirstName())
168211
.put("lastName", profile.getLastName())
169212
.put("email", profile.getEmail())
170213
.put("password", profile.getPassword())
214+
.put("balance", profile.getBalance())
171215
.toString();
172216
}
173217

@@ -176,7 +220,8 @@ private ProfileRequest getCreateTestProfile() {
176220
"James",
177221
"Gosling",
178222
179-
"password");
223+
"password",
224+
100);
180225
}
181226

182227
private Profile getTestProfile() {
@@ -185,6 +230,7 @@ private Profile getTestProfile() {
185230
"James",
186231
"Gosling",
187232
188-
"password");
233+
"password",
234+
100);
189235
}
190236
}

0 commit comments

Comments
 (0)