/*
* Copyright 2015 Google, Inc. All Rights Reserved.
*
* 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 com.examples.abelanav2.grpc;
import static com.google.api.services.datastore.client.DatastoreHelper.getByteString;
import static com.google.api.services.datastore.client.DatastoreHelper.getPropertyMap;
import static com.google.api.services.datastore.client.DatastoreHelper.getString;
import static com.google.api.services.datastore.client.DatastoreHelper.getTimestamp;
import com.google.api.services.datastore.DatastoreV1.Entity;
import com.google.api.services.datastore.DatastoreV1.Value;
import com.google.api.services.datastore.client.DatastoreException;
import com.google.identitytoolkit.GitkitClientException;
import com.google.identitytoolkit.GitkitUser;
import com.google.protobuf.ByteString;
import com.examples.abelanav2.BackendConstants;
import com.examples.abelanav2.datastore.DbClient;
import com.examples.abelanav2.datastore.DbUtils;
import com.examples.abelanav2.storage.CloudStorage;
import io.grpc.stub.StreamObserver;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.SignatureException;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
/**
* The controller for the Abelana GRPC server implementation.
*/
public class AbelanaGrpcImpl implements AbelanaGrpc.Abelana {
/**
* Logger.
*/
private static final Logger LOGGER = Logger.getLogger(AbelanaGrpcImpl.class.getName());
/**
* Datastore client.
*/
private DbClient dbClient = new DbClient();
/**
* Setter used by Mockito to set a mock DbClient.
* @param dbClient the new DbClient.
*/
void setDbClient(DbClient dbClient) {
this.dbClient = dbClient;
}
@Override
public final void signIn(final SignInRequest request,
final StreamObserver<SignInResponse> responseObserver) {
SignInResponse reply;
try {
GitkitUser gitkitUser = AuthUtils.verifyGitkitToken(request.getGitkitToken());
reply = SignInResponse.newBuilder().setUserToken(AuthUtils.getJwt(gitkitUser.getLocalId()))
.build();
} catch (GitkitClientException | IOException | SignatureException | InvalidKeyException e) {
LOGGER.warning("Authentication error with Gitkit Client " + e.getMessage());
reply = SignInResponse.newBuilder().setError(getErrorMessage("500", e.getMessage())).build();
}
responseObserver.onValue(reply);
responseObserver.onCompleted();
}
@Override
public final void photoStream(final PhotoListRequest request,
final StreamObserver<PhotoListResponse> responseObserver) {
listPhotos(request, responseObserver, DbClient.PhotoListType.PHOTO_LIST_STREAM);
}
@Override
public final void listMyPhotos(final PhotoListRequest request,
final StreamObserver<PhotoListResponse> responseObserver) {
listPhotos(request, responseObserver, DbClient.PhotoListType.PHOTO_LIST_MINE);
}
@Override
public final void listMyLikes(final PhotoListRequest request,
final StreamObserver<PhotoListResponse> responseObserver) {
listPhotos(request, responseObserver, DbClient.PhotoListType.PHOTO_LIST_LIKES);
}
/**
* Lists photos for listMyPhotos, listMyLikes.
* @param request the gRPC request
* @param responseObserver the gRPC response observer that will send the response to the client
* @param listKind the kind of list we want to return from dbClient.PhotoListType
*/
public final void listPhotos(final PhotoListRequest request,
final StreamObserver<PhotoListResponse> responseObserver,
final DbClient.PhotoListType listKind) {
PhotoListResponse reply;
if (AuthUtils.isSignedIn()) {
long page = request.getPageNumber();
DbClient.EntityListAndCursorResult photoListAndCursor;
try {
ByteString cursor = null;
boolean pageError = false;
if (page != 0) {
Entity cursorEntity = dbClient.getAndDeleteCursor(page);
if (cursorEntity != null) {
cursor = getByteString(getPropertyMap(cursorEntity).get("cursor"));
} else {
pageError = true;
}
}
photoListAndCursor = dbClient.getPhotoList(AuthUtils.getUserId(), listKind, cursor);
PhotoListResponse.Builder builder = PhotoListResponse.newBuilder();
if (pageError) {
// Invalid cursor, send an error but also return results
builder.setError(getErrorMessage("400-200",
"Invalid page, returning results from scratch"));
}
for (Entity result : photoListAndCursor.getEntityList()) {
Map<String, Value> props = getPropertyMap(result);
long rating = 1;
if (listKind == DbClient.PhotoListType.PHOTO_LIST_STREAM) {
rating = dbClient.getVoteValueForPhoto(DbUtils.getEntityId(result),
getString(props.get("userId")));
}
Photo photo = Photo.newBuilder()
.setPhotoId(DbUtils.getEntityId(result))
.setUserId(getString(props.get("userId")))
.setDate(getTimestamp(props.get("date")))
.setDescription(getString(props.get("description")))
.setRating(rating)
.setUrl(BackendConstants.IMAGES_BASE_URL + DbUtils.getEntityId(result) + "_"
+ getString(props.get("userId")) + ".webp")
.build();
builder.addPhoto(photo);
}
if (photoListAndCursor.getCursor() != null
&& photoListAndCursor.getEntityList().size() > 0) {
long cursorId = dbClient.insertCursor(photoListAndCursor.getCursor(),
AuthUtils.getUserId());
builder.setNextPage(cursorId);
}
// Everything went well
reply = builder.build();
} catch (DatastoreException e) {
// Internal error, impossible to list the photos
reply = PhotoListResponse.newBuilder().setError(getDbErrorMessage()).build();
}
} else {
reply = PhotoListResponse.newBuilder().setError(getAuthErrorMessage()).build();
}
responseObserver.onValue(reply);
responseObserver.onCompleted();
}
@Override
public final void flagPhoto(final FlagRequest request,
final StreamObserver<StatusResponse> responseObserver) {
StatusResponse reply;
if (AuthUtils.isSignedIn()) {
long photoId = request.getPhotoId();
try {
Entity photo = dbClient.getPhoto(photoId);
if (photo == null) {
throw new DatastoreException("flagPhoto", 404, "Photo not found in Database", null);
}
List<Entity> photoFlags = dbClient.getPhotoFlags(photo.getKey());
boolean hasAlreadyFlagged = false;
for (Entity flag : photoFlags) {
Map<String, Value> propsFlag = getPropertyMap(flag);
String userIdFlag = propsFlag.get("userId").getStringValue();
if (userIdFlag != null && userIdFlag.equals(AuthUtils.getUserId())) {
hasAlreadyFlagged = true;
}
}
if (!hasAlreadyFlagged) {
if (dbClient.insertFlag(photo.getKey(), AuthUtils.getUserId())) {
if (photoFlags.size() + 1 >= 2 && !dbClient.setPhotoFlagged(photo.getKey())) {
// Internal error, impossible to flag the photo
// Let's not say anything to the user as he did
// his job.
LOGGER.warning("Impossible to flag photo=" + photoId + " with count_flags="
+ photoFlags.size());
}
// Everything went well
reply = StatusResponse.newBuilder().build();
} else {
// Internal error, impossible to store the flag
reply = StatusResponse.newBuilder().setError(getDbErrorMessage()).build();
}
} else {
// User has already flagged the picture, do nothing
reply = StatusResponse.newBuilder().build();
}
} catch (DatastoreException e) {
// Internal error, impossible to store the flag
reply = StatusResponse.newBuilder().setError(getDbErrorMessage()).build();
}
} else {
reply = StatusResponse.newBuilder().setError(getAuthErrorMessage()).build();
}
responseObserver.onValue(reply);
responseObserver.onCompleted();
}
@Override
public final void uploadPhoto(final NewPhotoRequest request,
final StreamObserver<UploadPhotoResponse> responseObserver) {
UploadPhotoResponse reply;
if (AuthUtils.isSignedIn()) {
String description = request.getDescription();
try {
Entity photo = dbClient.insertPhoto(description, AuthUtils.getUserId());
try {
long photoId = DbUtils.getEntityId(photo);
// Everything went well, get upload URL
String uploadUrl = CloudStorage.getUploadUrl(photoId + "_" + AuthUtils.getUserId()
+ ".jpeg");
if (uploadUrl != null) {
reply = UploadPhotoResponse.newBuilder()
.setPhotoId(photoId)
.setUserId(AuthUtils.getUserId())
.setUploadUrl(uploadUrl).build();
} else {
reply = UploadPhotoResponse.newBuilder().setError(getErrorMessage("500",
"Impossible to start file upload")).build();
}
} catch (NullPointerException e) {
// Internal error, impossible to insert a new photo in the database
reply = UploadPhotoResponse.newBuilder().setError(getDbErrorMessage()).build();
}
} catch (DatastoreException e) {
// Internal error
reply = UploadPhotoResponse.newBuilder().setError(getDbErrorMessage()).build();
}
} else {
reply = UploadPhotoResponse.newBuilder().setError(getAuthErrorMessage()).build();
}
responseObserver.onValue(reply);
responseObserver.onCompleted();
}
@Override
public final void editPhoto(final EditPhotoRequest request,
final StreamObserver<StatusResponse> responseObserver) {
StatusResponse reply;
if (AuthUtils.isSignedIn()) {
long photoId = request.getPhotoId();
Entity photo;
try {
photo = dbClient.getPhoto(photoId);
if (photo == null) {
throw new DatastoreException("editPhoto", 404, "Photo not found in Database", null);
}
Map<String, Value> propsPhoto = getPropertyMap(photo);
String userIdPhoto = propsPhoto.get("userId").getStringValue();
if (userIdPhoto != null) {
if (userIdPhoto.equals(AuthUtils.getUserId())) {
// Let's update the photo
if (dbClient.updatePhotoDescription(photo.getKey(), request.getDescription())) {
// Everything went well
reply = StatusResponse.newBuilder().build();
} else {
// Internal error, impossible to delete
reply = StatusResponse.newBuilder().setError(getDbErrorMessage()).build();
}
} else {
// The user is trying to edit a photo that he does not
// own
reply = StatusResponse.newBuilder().setError(getErrorMessage("403",
"Not the owner of the photo")).build();
}
} else {
// Internal error - no property userId on the photo
reply = StatusResponse.newBuilder().setError(getDbErrorMessage()).build();
}
} catch (DatastoreException e) {
// Internal error
reply = StatusResponse.newBuilder().setError(getDbErrorMessage()).build();
}
} else {
reply = StatusResponse.newBuilder().setError(getAuthErrorMessage()).build();
}
responseObserver.onValue(reply);
responseObserver.onCompleted();
}
@Override
public final void deletePhoto(final DeletePhotoRequest request,
final StreamObserver<StatusResponse> responseObserver) {
StatusResponse reply;
if (AuthUtils.isSignedIn()) {
long photoId = request.getPhotoId();
try {
Entity photo = dbClient.getPhoto(photoId);
if (photo == null) {
throw new DatastoreException("deletePhoto", 404, "Photo not found in Database", null);
}
Map<String, Value> propsPhoto = getPropertyMap(photo);
String userIdPhoto = propsPhoto.get("userId").getStringValue();
if (userIdPhoto != null) {
if (userIdPhoto.equals(AuthUtils.getUserId())) {
// Let's delete all the data associated to this photo
if (dbClient.deletePhotoAndChildren(photo.getKey())) {
// Everything went well
reply = StatusResponse.newBuilder().build();
} else {
// Internal error, impossible to delete
reply = StatusResponse.newBuilder().setError(getDbErrorMessage()).build();
}
} else {
// The user is trying to suppress a photo that he
// does not own
reply = StatusResponse.newBuilder().setError(getErrorMessage("403",
"Not the owner of the photo")).build();
}
} else {
// Internal error - no property userId on the photo
reply = StatusResponse.newBuilder().setError(getDbErrorMessage()).build();
}
} catch (DatastoreException e) {
// Internal error
reply = StatusResponse.newBuilder().setError(getDbErrorMessage()).build();
}
} else {
reply = StatusResponse.newBuilder().setError(getAuthErrorMessage()).build();
}
responseObserver.onValue(reply);
responseObserver.onCompleted();
}
@Override
public final void ratePhoto(final VoteRequest request,
final StreamObserver<StatusResponse> responseObserver) {
StatusResponse reply;
if (AuthUtils.isSignedIn()) {
long photoId = request.getPhotoId();
try {
Entity photo = dbClient.getPhoto(photoId);
// Let's rate the photo
int vote = 0;
if (request.getVote() == VoteRequest.VoteType.THUMBS_DOWN) {
vote = -1;
} else if (request.getVote() == VoteRequest.VoteType.THUMBS_UP) {
vote = 1;
}
if (photo != null && dbClient.voteForPhoto(photoId, vote, AuthUtils.getUserId())) {
// Everything went well
reply = StatusResponse.newBuilder().build();
} else if (photo == null) {
// Photo does not exist
reply = StatusResponse.newBuilder().setError(getErrorMessage("404", "Photo not found"))
.build();
} else {
// Internal error, impossible to rate
reply = StatusResponse.newBuilder().setError(getDbErrorMessage()).build();
}
} catch (DatastoreException e) {
// Internal error
reply = StatusResponse.newBuilder().setError(getDbErrorMessage()).build();
}
} else {
reply = StatusResponse.newBuilder().setError(getAuthErrorMessage()).build();
}
responseObserver.onValue(reply);
responseObserver.onCompleted();
}
/**
* Returns an ErrorMessage to send via gRPC.
* @param code the error code.
* @param details details on the error.
* @return the error message to send.
*/
private Error getErrorMessage(final String code, final String details) {
return Error.newBuilder().setCode(code).setDetails(details).build();
}
/**
* Returns an ErrorMessage for an authentication error to send via gRPC.
* @return the error message to send.
*/
private Error getAuthErrorMessage() {
return getErrorMessage("403", "You are not authenticated");
}
/**
* Returns an ErrorMessage for an authentication error to send via gRPC.
* @return the error message to send.
*/
private Error getDbErrorMessage() {
return getErrorMessage("500", "Database error");
}
}