Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Kk/add subscriptions #4

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
*.iml
.idea
target
.DS_Store
build
.gradle
**/docker/*.jar
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,26 @@ A data point looks something like this

We may add documentation here if we find that the Postman collection isn't sufficient.

### Using Subscription API

The subscription API allows subscribing to data point events (creation and deletion of data points).
The API is RESTFul API that supports creation, deletion and retreival of subscriptions.
The authrization mechanism is the same as for the data point API (OAuth 2.0).

The subscription API is documented in a [RAML file](docs/raml/subscription.yml)

Once a subscription is created for a user, every data point creation or deletion will publish a notification using POST to the callback URL specified in the subscription. A sample notification look like this:

``` json
{
"dataPointId": "foo1",
"eventType":"CREATE",
"eventDateTime":"2015-04-14T01:30:58.474Z"
}
```

The callback URL should respond with status 200 (OK) if the notification is received correctly. A notification retry mechanism has not been implemented yet.


### Roadmap

Expand Down
64 changes: 64 additions & 0 deletions docs/raml/subscription.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#%RAML 0.8
title: Open mHealth Data Point API
version: v1.0.M1

baseUri: https://{endpointUrl}/{version}
baseUriParameters:
domain:
endpointUrl: the location of the API endpoint
type: string

mediaType: application/json


securitySchemes:
- oauth_2_0:
type: OAuth 2.0
describedBy:
headers:
Authorization:
description: the access token
type: string
responses:
401:
description: the access token is not valid or has not been granted the required scope

settings:
authorizationUri: http://domain/oauth/authorize
accessTokenUri: http://domain/oauth/token
authorizationGrants: [code, token, owner]
scopes: [read_data_points, write_data_points, delete_data_points,subscription]

/subscriptions:

post:
description: create a subscription
securedBy: [oauth_2_0]
body:
application/json
responses:
201:
description: the subscription has been created
409:
description: a subscription with the same user id and callback URL already exists
get:
description: get list of subscriptions
securedBy: [oauth_2_0]
responses:
200:
body:
application/json
/{id}:
uriParameters:
id:
description: the identifier of subscription
type: string

delete:
description: delete a subscription
securedBy: [oauth_2_0]
responses:
200:
description: the subscription has been deleted
404:
description: the subscription does not exist
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.openmhealth.dsu.configuration;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ApplicationEventMulticaster;
import org.springframework.context.event.SimpleApplicationEventMulticaster;
import org.springframework.core.task.SimpleAsyncTaskExecutor;

/**
* Overrides default event multicaster to support asynchronous processing.
*
* Created by kkujovic on 4/14/15.
*/
@Configuration
public class EventConfiguration {

@Bean(name = "applicationEventMulticaster")
public ApplicationEventMulticaster simpleApplicationEventMulticaster() {
SimpleApplicationEventMulticaster eventMulticaster = new SimpleApplicationEventMulticaster();
eventMulticaster.setTaskExecutor(new SimpleAsyncTaskExecutor());
return eventMulticaster;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
import org.openmhealth.dsu.domain.DataPoint;
import org.openmhealth.dsu.domain.DataPointSearchCriteria;
import org.openmhealth.dsu.domain.EndUserUserDetails;
import org.openmhealth.dsu.event.DataPointEvent;
import org.openmhealth.dsu.event.DataPointEventPublisher;
import org.openmhealth.dsu.service.DataPointService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -68,6 +70,9 @@ public class DataPointController {
@Autowired
private DataPointService dataPointService;

@Autowired
private DataPointEventPublisher dataPointEventPublisher;

/**
* Reads data points.
*
Expand Down Expand Up @@ -182,6 +187,9 @@ public ResponseEntity<?> writeDataPoint(@RequestBody @Valid DataPoint dataPoint,

dataPointService.save(dataPoint);

//create event for subscription API
dataPointEventPublisher.publishEvent(endUserId, dataPoint.getId(), DataPointEvent.DataPointEventType.CREATE);

return new ResponseEntity<>(CREATED);
}

Expand All @@ -201,6 +209,13 @@ public ResponseEntity<?> deleteDataPoint(@PathVariable String id, Authentication
// only delete the data point if it belongs to the user associated with the access token
Long dataPointsDeleted = dataPointService.deleteByIdAndUserId(id, endUserId);

return new ResponseEntity<>(dataPointsDeleted == 0 ? NOT_FOUND : OK);
if (dataPointsDeleted > 0) {
//publish notification
dataPointEventPublisher.publishEvent(endUserId, id, DataPointEvent.DataPointEventType.DELETE);

return new ResponseEntity<>(OK);
} else {
return new ResponseEntity<>(NOT_FOUND);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* Copyright 2014 Open mHealth
*
* Licensed 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.openmhealth.dsu.controller;

import org.openmhealth.dsu.domain.EndUserUserDetails;
import org.openmhealth.dsu.domain.Subscription;
import org.openmhealth.dsu.service.SubscriptionService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import static org.openmhealth.dsu.configuration.OAuth2Properties.CLIENT_ROLE;
import static org.openmhealth.dsu.configuration.OAuth2Properties.SUBSCRIPTION_SCOPE;
import static org.springframework.http.HttpStatus.*;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
import static org.springframework.web.bind.annotation.RequestMethod.GET;
import static org.springframework.web.bind.annotation.RequestMethod.POST;

/**
* REST controller that handles the saving, deleting and listing subscriptions.
*
* Created by kkujovic on 4/12/15.
*/
@ApiController
public class SubscriptionController {

private static final Logger log = LoggerFactory.getLogger(SubscriptionController.class);

@Autowired
private SubscriptionService subscriptionService;

/**
* Writes a subscription.
*
* @param subscription the subscription to create
*/
@PreAuthorize("#oauth2.clientHasRole('" + CLIENT_ROLE + "') and #oauth2.hasScope('" + SUBSCRIPTION_SCOPE + "')")
@RequestMapping(value = "/subscriptions", method = POST, consumes = APPLICATION_JSON_VALUE)
public ResponseEntity<Subscription> createSubscription(@RequestBody Subscription subscription, Authentication auth) {

String endUserId = getEndUserId(auth);

//does subscription already exist?
Iterable<Subscription> subscriptions = subscriptionService.findByUserIdAndCallbackUrl(endUserId, subscription.getCallbackUrl());
if (subscriptions.iterator().hasNext()) {
return new ResponseEntity<>(CONFLICT);
}

// set the owner of the subscription to be the user associated with the access token
subscription.setUserId(endUserId);

subscriptionService.save(subscription);

return new ResponseEntity<>(subscription, CREATED);
}

/**
* Returns list of subscriptions for user.
*
*/
@PreAuthorize("#oauth2.clientHasRole('" + CLIENT_ROLE + "') and #oauth2.hasScope('" + SUBSCRIPTION_SCOPE + "')")
@RequestMapping(value = "/subscriptions", method = GET, produces = APPLICATION_JSON_VALUE)
public ResponseEntity<Iterable<Subscription>> getSubscriptions(Authentication auth) {
String endUserId = getEndUserId(auth);

Iterable<Subscription> subscriptions = subscriptionService.findByUserId(endUserId);
return new ResponseEntity<>(subscriptions, OK);

}

/**
* Deletes a subscription.
*
* @param id of the subscription to delete
*/
@PreAuthorize("#oauth2.clientHasRole('" + CLIENT_ROLE + "') and #oauth2.hasScope('" + SUBSCRIPTION_SCOPE + "')")
@RequestMapping(value = "/subscriptions/{id}", method = RequestMethod.DELETE, consumes = APPLICATION_JSON_VALUE)
public ResponseEntity<?> deleteSubscription(@PathVariable String id, Authentication auth) {
String endUserId = getEndUserId(auth);

//delete by id AND user id (only delete subscription for the associated user)
Long deleteCount = subscriptionService.deleteByIdAndUserId(id, endUserId);
return new ResponseEntity<>(deleteCount == 0 ? NOT_FOUND : OK);
}

private String getEndUserId(Authentication authentication) {
return ((EndUserUserDetails) authentication.getPrincipal()).getUsername();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Copyright 2014 Open mHealth
*
* Licensed 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.openmhealth.dsu.domain;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.data.annotation.Id;

import java.util.UUID;

import static com.google.common.base.Preconditions.checkNotNull;


/**
* A subscription.
*
* @author Kenan Kujovic
*/
public class Subscription {

@Id
private String id;
private String userId;
private String callbackUrl;


/**
* @param callbackUrl the url where data changes should be posted
*/
@JsonCreator
public Subscription(@JsonProperty("callbackUrl") String callbackUrl) {

checkNotNull(callbackUrl);

this.id = UUID.randomUUID().toString();
this.callbackUrl = callbackUrl;
}

/**
* @deprecated should only be used by frameworks for persistence or serialisation
*/
@Deprecated
Subscription() {
}

/**
* @return the identifier of the subscription
*/
public String getId() {
return id;
}

@JsonIgnore
public String getUserId() {
return userId;
}

public void setUserId(String userId) {
this.userId = userId;
}

public String getCallbackUrl() {
return callbackUrl;
}
}
Loading