Antonio Mallia & Nicola Corti #droidconIT 2016 Turin
Software Engineer
@ .it Registry
Android Software Engineer
@ Monetas AG
Open-source
but...Many Parse features missing
Javascript
AnyPresence
Appcelerator
Appery
Backendless
CloudMine
FeedHenry
Firebase
Kii
Kinvey
Kumulos
moBack
Syncano
and many others...
REST
Java
JSON
Gradle
GET /users
Retrieves a list of users
GET /users/1
Retrieves user #1
POST /users
Creates a new user
PUT /users/1
Updates user #1
DELETE /users/1
Deletes user #1
{
"user_id":1,
"name":"Antonio Mallia",
"username":"antonio",
"email":"me@antoniomallia.it",
"last_active":1360031425,
"created":1315711352,
"is_deleted":0
}
GET /users?deleted=1
Filter the deleted users
GET /users?sort=name+DESC
Sort by name descending
GET /users?q=antonio
Search word "antonio"
A RESTful API should be stateless.
Add the dropwizard-core library as a dependency
dependencies {
compile ('com.yammer.dropwizard:dropwizard-core:'+dropwizardVersion)
}
One-JAR lets you package a Java application together with its dependency Jars into a single executable Jar file
buildscript {
dependencies {
classpath 'com.github.rholder:gradle-one-jar:1.0.4'
}
}
mainClassName = 'com.droidcon.it.backend.BackendApp'
task oneJar(type: OneJar) {
mainClass = mainClassName
}
Just need to create a simple YAML file
server:
type: simple
connector:
type: https
port: 8080
logging:
level: INFO
Can be extended with custom configuration
public class BackendApp extends Application<BackendConf> {
@Override
public void run(BackendConf configuration, Environment environment) {
// nothing to do yet
}
public static void main(String[] args) {
new BackendApp().run(args);
}
}
@Path("/hello-world")
public class HelloWorldResource {
@GET
@Path("/")
public Response sayHello() {
return Response.ok("Hello world!").build();
}
@GET
@Path("/{name}")
public Response sayHello(@PathParam("name") @NotNull String name) {
return Response.ok(String.format("Hello %s!", name)).build();
}
}
java -jar target/backend-0.0.1.jar server backend-conf.yml
As simple as that!
Add the dependencies
compile 'com.meltmedia.dropwizard:dropwizard-mongo:0.2.0'
Extend configuration
public class BackendConf extends Configuration{
@JsonProperty
private MongoConfiguration mongo;
}
Initialize
public class BackendApp extends Application<BackendConf> {
MongoBundle<BackendConf> mongoBundle;
@Override
public void initialize(Bootstrap<BackendConf> bootstrap) {
bootstrap.addBundle(mongoBundle =
MongoBundle.<BackendConf>builder()
.withConfiguration(BackendConf::getMongo)
.build());
}
...
}
Get the default DB
DB db = mongoBundle.getDB();
Since MongoDB uses BSON, a binary form of JSON, to store its documents, a JSON mapper is a perfect mechanism for mapping Java objects to MongoDB documents.
GET /users/1
@GET
@Path("/{id}")
public Response getUser(@PathParam("id") @NotNull String id) {
DBCollection dbColl = db.getCollection("users");
JacksonDBCollection<User, String> coll =
JacksonDBCollection.wrap(dbCollection, User.class, String.class);
User user = coll.findOneById(id);
return Response.ok(user).build();
}
POST /users
@Context
UriInfo uri;
@POST
@Path("/")
public Response createUser(@Valid User user) {
DBCollection dbColl = db.getCollection("users");
JacksonDBCollection<User, String> coll =
JacksonDBCollection.wrap(dbCollection, User.class, String.class);
WriteResult<User, String> result = coll.insert(user);
String id = result.getSavedId();
UriBuilder builder = uri.getAbsolutePathBuilder();
return Response.created(builder.path(id).build()).build();
}
POST /login
@POST
@Path("/")
public Response login(@Valid Credentials credentials) {
DBCollection dbColl = db.getCollection("users");
JacksonDBCollection<User, String> coll =
JacksonDBCollection.wrap(dbCollection, User.class, String.class);
User user = coll.findOneById(DBQuery.and(
DBQuery.is("username", credentials.getUsername()),
DBQuery.is("password", credentials.getHashPassword())
));
if(user != null){
// Generate token
return Response.ok(token).build();
}
return Response.status(Response.Status.NOT_FOUND).build();
}
"JSON-based open standard (RFC 7519) for passing claims between parties in web application environment."
Used to send information that can be verified
and trusted by means of a digital signature
JWT consist of three parts separated by dots (.)
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJuYW1lIjoiRHJvaWRjb24iLCJhZG1pbiI6dHJ1ZX0.
x6Uvbp2N8M64yK4tV4rj971Di8xErW-vMEFFL7IN084
Consist of two parts:
{"alg": "HS256", "typ": "JWT"}
Base64 encoded
Contains the claims
{"name": "Droidcon", "admin": true}
Encoded header and payload with a secret key
The signature is used to verify the sender of the JWT and to ensure that the message wasn't changed
HMACSHA256( base64UrlEncode(header) + "."
+ base64UrlEncode(payload), secret)
Add the dependencies
dependencies {
compile (
'io.dropwizard:dropwizard-auth:'+dropwizardVersion,
'com.github.toastshaman:dropwizard-auth-jwt:'+dropwizardVersion+'-0'
)
}
UserPrincipal is the one from java.security
public class Auth implements Authenticator<JsonWebToken, UserPrincipal>
{
@Override
public Optional<UserPrincipal> authenticate(JsonWebToken token) {
final JsonWebTokenValidator expiryValidator =
new ExpiryValidator();
expiryValidator.validate(token);
// Check on DB
if(token.claim()...){
return Optional.of(new UserPrincipal(...));
}
return Optional.absent();
}
}
Add to the header:
Authorization: Bearer <token>
@Path("/secret")
public class SuperSecretResource {
@GET
@Path("/")
public Response secret(@Auth UserPrincipal user){
return Response.ok("Secret code").build();
}
}
final JwtClaims claims = new JwtClaims();
claims.setExpirationTimeMinutesInTheFuture(30);
claims.setClaim("username","...");
final JsonWebSignature jws = new JsonWebSignature();
jws.setPayload(claims.toJson());
jws.setAlgorithmHeaderValue(HMAC_SHA256);
jws.setKey(new HmacKey(tokenSecret));
Prior to Froyo, HttpURLConnection had some frustrating bugs. In particular, calling close() on a readable InputStream could poison the connection pool...
...the large size of this API makes it difficult for us to improve it without breaking compatibility. The Android team is not actively working on Apache HTTP Client.
public static JSONObject requestRestResponse() {
HttpURLConnection urlConnection = null;
try {
// create connection
URL urlToRequest = new URL("http://mybackend.com/v1/req");
urlConnection = (HttpURLConnection)
urlToRequest.openConnection();
urlConnection.setConnectTimeout(CONNECTION_TIMEOUT);
urlConnection.setReadTimeout(DATARETRIEVAL_TIMEOUT);
// handle issues
int statusCode = urlConnection.getResponseCode();
if (statusCode == HttpURLConnection.HTTP_UNAUTHORIZED) {
// handle unauthorized (if service requires user login)
} else if (statusCode != HttpURLConnection.HTTP_OK) {
// handle any other errors, like 404, 500,..
}
// create JSON object from content
InputStream in =
new BufferedInputStream(urlConnection.getInputStream());
return new JSONObject(getResponseText(in));
} catch (MalformedURLException e) {
// URL is invalid
} catch (SocketTimeoutException e) {
// data retrieval or connection timed out
} catch (IOException e) {
// could not read response body
} catch (JSONException e) {
// response body is no valid JSON string
}
return null;
}} finally {
if (urlConnection != null) {
// Don't forget to release resources
urlConnection.disconnect();
}
}
return null;
}
Don't forget about NetworkOnMainThreadException. The most common solution for this kind of problems are AsyncTasks!
Have you ever wrote?
@Override
protected void onPostExecute(String result) {
if (getActivity() == null){
return; // Here Activity is gone...
}
...
}
If you have more than
1K lines
of code for creating and handling your HTTP requests...
Ask yourself if you're doing it right!
compile 'com.squareup.retrofit2:retrofit:2.0.0'
public interface GitHubService {
@GET("users/{user}/repos")
Call<List<Repo>> listRepos(@Path("user") String user);
}
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.build();
GitHubService service = retrofit.create(GitHubService.class);
public interface GitHubService {
@GET("users/{user}/repos")
Call<List<Repo>> listRepos(
@Path("user") String user);
@GET("users/{user}/repos")
Call<List<Repo>> listRepos(
@Path("user") String user,
@Query("type") String type);
@GET("users/{user}/repos")
Call<List<Repo>> listRepos(
@Path("user") String user,
@QueryMap Map<String, String> options);
}
public interface GitHubService {
@Headers("User-Agent: my-Awesome-Retrofit-powered-app")
@GET("users/{user}/repos")
Call<List<Repo>> listRepos(@Path("user") String user);
@GET("gists/public")
Call<List<Gist>> listGists(@Header("User-Agent") String uAgent)
}
public interface GitHubService {
@GET
Call<List<User>> getCustomUsers(@Url String reqUrl);
@POST("gists")
Call<Gist> createGist(@Body GistRequest grequest);
}
com.squareup.retrofit:converter-gson
com.squareup.retrofit:converter-jackson
com.squareup.retrofit:converter-moshi
com.squareup.retrofit:converter-protobuf
com.squareup.retrofit:converter-wire
com.squareup.retrofit:converter-simplexml
You can also implement yours with Converter.Factory
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addConverterFactory(JacksonConverterFactory.create())
.build();
service = retrofit.create(GitHubService.class);
Gson gson = new GsonBuilder()
.setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
.create();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
service = retrofit.create(GitHubService.class);
You can plug a Call Adapter to work with:
// Sync call
Call<Repo> call = service.loadRepo();
Response<Repo> response = call.execute();
// Async call
Call<Repo> call = service.loadRepo();
call.enqueue(new Callback<Repo>() {
@Override
public void onResponse(Response<Repo> response) {
// Get result Repo from response.body()
}
@Override
public void onFailure(Throwable t) {
// Handle failure
}
});
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build();
public interface APIService {
@GET("gists/public")
Call<Gist> getGists();
@GET("gists/public")
Observable<Gist> getGistsRx();
}
compile 'com.android.volley:volley:1.0.0'
RequestQueue queue =
Volley.newRequestQueue(getApplicationContext());
StringRequest request = new StringRequest(
Request.Method.GET,
"https://api.github.com/gists/public",
this::handleResponse,
this::handleError);
request.setShouldCache(true);
request.setTag(requestTag); // A class member 'requestTag'
queue.add(request);
@Override
protected void onStop() {
queue.cancelAll(requestTag);
super.onStop();
}
Don't forget to do it!
ImageLoader mImageLoader;
NetworkImageView mNetworkImageView;
private static final String IMAGE_URL =
"http://i.imgur.com/RLKixQW.png";
// Retrieve the ImageLoader (singleton/application/...)
// Retrieve the NetworkImageView (findViewById)
mNetworkImageView.setImageUrl(IMAGE_URL, mImageLoader);
<com.android.volley.toolbox.NetworkImageView
android:id="@+id/networkImageView"
android:layout_width="300dp"
android:layout_height="300dp"
android:layout_centerHorizontal="true" />
mImageLoader = new ImageLoader(mRequestQueue,
new ImageLoader.ImageCache() {
private final LruCache<String, Bitmap> cache = new LruCache<>(20);
@Override
public Bitmap getBitmap(String url) {
return cache.get(url);
}
@Override
public void putBitmap(String url, Bitmap bitmap) {
cache.put(url, bitmap);
}
});
~$ adb shell setprop log.tag.Volley VERBOSE
D/Volley (670 ms) [ ] https://api.github.com/users/cortinico
D/Volley (+0 ) [ 1] add-to-queue
D/Volley (+0 ) [238] cache-queue-take
D/Volley (+0 ) [238] cache-miss
D/Volley (+10 ) [242] network-queue-take
D/Volley (+630 ) [242] network-http-complete
D/Volley (+0 ) [242] network-parse-complete
D/Volley (+0 ) [242] network-cache-written
D/Volley (+0 ) [242] post-response
D/Volley (+30 ) [ 1] done
D/Volley (10 ms) [ ] https://api.github.com/users/cortinico
D/Volley (+0 ) [ 1] add-to-queue
D/Volley (+0 ) [238] cache-queue-take
D/Volley (+0 ) [238] cache-hit
D/Volley (+0 ) [238] cache-hit-parsed
D/Volley (+0 ) [238] post-response
D/Volley (+10 ) [ 1] done
public interface UserInterface {
@GET("users")
Call<List<User>> getUsers();
@GET("users")
Call<List<User>> getUsers(@Query("deleted") int isDeleted);
@GET("users")
Call<List<User>> getUsers(@Query("sort") String sort);
@GET("users")
Call<List<User>> getUsers(@QueryMap Map<String, String> params);
@GET("users")
Call<List<User>> searchUsers(@Query("q") String query);
}
public interface UserInterface {
@GET("users/{id}")
Call<User> getUser(@Path("id") int userid);
@POST("users")
Call<User> createUser(@Body User u);
@PUT("users/{id}")
Call<User> updateUser(@Path("id") int userid, @Body User user);
@DELETE("users/{id}")
Call<User> deleteUser(@Path("id") int userid);
@GET("secret")
Call<Secret> getSecret(@Header("Authorization") String secret);
}
github.com/backend4android/slides