app/src/main/java/ch/epfl/sweng/zuluzulu/firebase/FirebaseProxy.java
package ch.epfl.sweng.zuluzulu.firebase;
import android.content.Context;
import android.util.Log;
import com.google.android.gms.tasks.OnFailureListener;
import com.google.firebase.FirebaseApp;
import com.google.firebase.firestore.DocumentSnapshot;
import com.google.firebase.firestore.FieldValue;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import ch.epfl.sweng.zuluzulu.firebase.Database.Database;
import ch.epfl.sweng.zuluzulu.firebase.Database.DatabaseCollection;
import ch.epfl.sweng.zuluzulu.firebase.Database.DatabaseQuery;
import ch.epfl.sweng.zuluzulu.firebase.Database.FirebaseFactory;
import ch.epfl.sweng.zuluzulu.idlingResource.IdlingResourceFactory;
import ch.epfl.sweng.zuluzulu.structure.Association;
import ch.epfl.sweng.zuluzulu.structure.Channel;
import ch.epfl.sweng.zuluzulu.structure.ChatMessage;
import ch.epfl.sweng.zuluzulu.structure.Event;
import ch.epfl.sweng.zuluzulu.structure.Post;
import ch.epfl.sweng.zuluzulu.structure.user.AuthenticatedUser;
import ch.epfl.sweng.zuluzulu.structure.user.User;
import ch.epfl.sweng.zuluzulu.structure.user.UserRole;
public class FirebaseProxy implements Proxy {
private static FirebaseProxy proxy;
private DatabaseCollection userCollection;
private DatabaseCollection assoCollection;
private DatabaseCollection eventCollection;
private DatabaseCollection channelCollection;
private FirebaseProxy(Context appContext) {
FirebaseApp.initializeApp(appContext);
}
public static void init(Context appContext) {
if (proxy == null)
proxy = new FirebaseProxy(appContext);
}
public static FirebaseProxy getInstance() {
if (proxy == null)
throw new IllegalStateException("The FirebaseProxy hasn't been initialized");
else {
proxy.create();
return proxy;
}
}
/**
* Create the instance
* I can't put it in the constructor
* because it's called in the mainactivity
* and I can't inject from tests because tests are called after launching the main
*/
private void create() {
Database firebaseInstance = FirebaseFactory.getDependency();
userCollection = firebaseInstance.collection("new_user");
assoCollection = firebaseInstance.collection("new_asso");
eventCollection = firebaseInstance.collection("new_even");
channelCollection = firebaseInstance.collection("new_chan");
}
/**
* Get all objects T from database
*
* @param query from the collection
* @param onResult Called on result
* @param creator Create the object
* @param <T> The object
*/
private <T> void getAll(DatabaseQuery query, OnResult<List<T>> onResult, mapToObject<T> creator) {
IdlingResourceFactory.incrementCountingIdlingResource();
query.get().addOnSuccessListener(queryDocumentSnapshots -> {
List<T> resultList = new ArrayList<>();
for (DocumentSnapshot snap : queryDocumentSnapshots.getDocuments()) {
FirebaseMapDecorator fmap = new FirebaseMapDecorator(snap);
try {
T object = creator.apply(fmap);
if (object != null)
resultList.add(object);
} catch (Exception ignored) {
}
}
onResult.apply(resultList);
IdlingResourceFactory.decrementCountingIdlingResource();
}).addOnFailureListener(onFailureWithErrorMessage("Cannot fetch all"));
}
/**
* Get object T by ID
*
* @param collection Collection
* @param id Id
* @param onResult On result
* @param creator Create the object
* @param <T> The object
*/
private <T> void getObjectById(DatabaseCollection collection, String id, OnResult<T> onResult, mapToObject<T> creator) {
IdlingResourceFactory.incrementCountingIdlingResource();
collection.document(id).getAndAddOnSuccessListener(fmap -> {
T object = null;
try {
object = creator.apply(fmap);
} catch (Exception ignored) {
}
if (object != null)
onResult.apply(object);
IdlingResourceFactory.decrementCountingIdlingResource();
}).addOnFailureListener(onFailureWithErrorMessage("Cannot fetch the with id " + id));
}
/**
* Get Objects T by Ids
*
* @param collection colection
* @param ids ID
* @param onResult OnResult
* @param creator Create the object
* @param <T> The object
*/
private <T> void getFromIds(DatabaseCollection collection, List<String> ids, OnResult<List<T>> onResult, mapToObject<T> creator) {
IdlingResourceFactory.incrementCountingIdlingResource();
List<T> result = new ArrayList<>();
Counter counter = new Counter(ids.size());
for (String id : ids) {
collection.document(id).getAndAddOnSuccessListener(fmap -> {
T object = null;
try {
object = creator.apply(fmap);
} catch (Exception ignored) {
}
if (object != null)
result.add(object);
if (counter.increment()) {
onResult.apply(result);
IdlingResourceFactory.decrementCountingIdlingResource();
}
}).addOnFailureListener(e -> {
Log.e("PROXY", "cannot fetch from id " + id);
if (counter.increment()) {
onResult.apply(result);
IdlingResourceFactory.decrementCountingIdlingResource();
}
});
}
}
//----- Association related methods -----\\
/**
* Get all associations and apply an OnResult on them
*
* @param onResult interface defining apply()
*/
@Override
public void getAllAssociations(OnResult<List<Association>> onResult) {
getAll(assoCollection.orderBy("name"), onResult, fmap -> {
if (fmap.hasFields(Association.requiredFields()))
return new Association(fmap);
return null;
});
}
/**
* Get one association from its id and apply an OnResult on them
*
* @param onResult interface defining apply()
*/
@Override
public void getAssociationFromId(String id, OnResult<Association> onResult) {
getObjectById(assoCollection, id, onResult, fmap -> {
if (fmap.hasFields(Association.requiredFields()))
return new Association(fmap);
return null;
});
}
/**
* Get all associations from an ID list and apply an OnResult on them
*
* @param onResult interface defining apply()
*/
@Override
public void getAssociationsFromIds(List<String> ids, OnResult<List<Association>> onResult) {
getFromIds(assoCollection, ids, onResult, fmap -> {
if (fmap.hasFields(Association.requiredFields()))
return new Association(fmap);
return null;
}
);
}
@Override
public void addAssociation(Association association) {
IdlingResourceFactory.incrementCountingIdlingResource();
createChannel(association);
assoCollection.document(association.getId()).set(association.getData());
IdlingResourceFactory.decrementCountingIdlingResource();
}
//----- Event related methods -----\\
/**
* add an event to the database
* @param event the event we want to add
*/
@Override
public void addEvent(Event event) {
IdlingResourceFactory.incrementCountingIdlingResource();
createChannel(event);
eventCollection.document(event.getId()).set(event.getData());
IdlingResourceFactory.decrementCountingIdlingResource();
}
/**
* Get all events and apply an OnResult on them
*
* @param onResult interface defining apply()
*/
@Override
public void getAllEvents(OnResult<List<Event>> onResult) {
getEventsFromToday(onResult, 200);
}
/**
* Get Events from today ordered by date.
*
* @param onResult interface defining apply()
*/
@Override
public void getEventsFromToday(OnResult<List<Event>> onResult, int limit) {
getAll(eventCollection.whereGreaterThan("end_date", new Date()).orderBy("end_date").limit(limit), onResult, fmap -> {
if (fmap.hasFields(Event.requiredFields()))
return new Event(fmap);
return null;
});
}
/**
* Get one event from its id and apply an OnResult on them
*
* @param onResult interface defining apply()
*/
@Override
public void getEventFromId(String id, OnResult<Event> onResult) {
getObjectById(eventCollection, id, onResult, fmap -> {
if (fmap.hasFields(Event.requiredFields()))
return new Event(fmap);
return null;
});
}
/**
* Get all events from an ID list and apply an OnResult on them
*
* @param onResult interface defining apply()
*/
@Override
public void getEventsFromIds(List<String> ids, OnResult<List<Event>> onResult) {
getFromIds(eventCollection, ids, onResult, fmap -> {
if (fmap.hasFields(Event.requiredFields()))
return new Event(fmap);
return null;
}
);
}
//----- Channel related methods -----\\
/**
* create a channel for an association
* @param association the association we want to create a channel for
*/
private void createChannel(Association association) {
IdlingResourceFactory.incrementCountingIdlingResource();
Map<String, Object> map = new HashMap<>();
map.put("id", Objects.requireNonNull(association.getChannelId()));
map.put("name", association.getName());
map.put("short_description", association.getShortDescription());
map.put("restrictions", new HashMap<>());
map.put("icon_uri", association.getIconUri().toString());
channelCollection.document(association.getChannelId()).set(map);
IdlingResourceFactory.decrementCountingIdlingResource();
}
/**
* create a channel for an event
* @param event the event we want to create a channel for
*/
private void createChannel(Event event) {
IdlingResourceFactory.incrementCountingIdlingResource();
Map<String, Object> map = new HashMap<>();
map.put("id", event.getChannelId());
map.put("name", event.getName());
map.put("short_description", event.getShortDescription());
map.put("restrictions", new HashMap<>());
map.put("icon_uri", event.getIconUri().toString());
channelCollection.document(event.getChannelId()).set(map);
IdlingResourceFactory.decrementCountingIdlingResource();
}
/**
* Get all events and apply an OnResult on them
*
* @param onResult interface defining apply()
*/
@Override
public void getAllChannels(OnResult<List<Channel>> onResult) {
getAll(channelCollection.orderBy("id"), onResult, fmap -> {
if (fmap.hasFields(Channel.requiredFields()))
return new Channel(fmap);
return null;
});
}
/**
* Get one event from its id and apply an OnResult on them
*
* @param onResult interface defining apply()
*/
@Override
public void getChannelFromId(String id, OnResult<Channel> onResult) {
getObjectById(channelCollection, id, onResult, fmap -> {
if (fmap.hasFields(Channel.requiredFields()))
return new Channel(fmap);
return null;
});
}
/**
* Get all events from an ID list and apply an OnResult on them
*
* @param onResult interface defining apply()
*/
public void getChannelsFromIds(List<String> ids, OnResult<List<Channel>> onResult) {
getFromIds(channelCollection, ids, onResult, fmap -> {
if (fmap.hasFields(Channel.requiredFields()))
return new Channel(fmap);
return null;
}
);
}
/**
* get the messages of a channel
* @param id the id of the channel
* @param onResult the onResult for the list of messages
*/
@Override
public void getMessagesFromChannel(String id, OnResult<List<ChatMessage>> onResult) {
IdlingResourceFactory.incrementCountingIdlingResource();
channelCollection.document(id).collection("messages").getAndAddOnSuccessListener(fmapList -> {
List<ChatMessage> result = new ArrayList<>();
for (FirebaseMapDecorator fmap : fmapList) {
if (fmap.hasFields(ChatMessage.requiredFields()))
result.add(new ChatMessage(fmap));
}
onResult.apply(result);
IdlingResourceFactory.decrementCountingIdlingResource();
}).addOnFailureListener(onFailureWithErrorMessage("Cannot getAndAddOnSuccessListener MessagesFromChannel " + id));
}
/**
* get the posts of a channel
* @param id the id of the channel
* @param onResult the onResult for the list of post
*/
@Override
public void getPostsFromChannel(String id, OnResult<List<Post>> onResult) {
IdlingResourceFactory.incrementCountingIdlingResource();
channelCollection.document(id).collection("posts").getAndAddOnSuccessListener(fmapList -> {
List<Post> result = new ArrayList<>();
for (FirebaseMapDecorator data : fmapList) {
if (data.hasFields(Post.requiredFields())) {
/*
* On devrait faire ça partout
* Capturer les erreurs lors des créations
* et envoyer null ??
*
* Car new Post() peut renvoyer une exception !
*
* Si on se met d'accord sur la facon d'implémenter cela, je suis chaud
* à modifier le reste
* //TODO
*/
try {
result.add(new Post(data));
} catch (Exception e) {
// not added
// do somethine ?
}
}
}
onResult.apply(result);
IdlingResourceFactory.decrementCountingIdlingResource();
}).addOnFailureListener(onFailureWithErrorMessage("Cannot getAndAddOnSuccessListener PostsFromChannel " + id));
}
/**
* add the channel to the list of followed channels of a user
* @param channel the channel we want to add
* @param user the user
*/
@Override
public void addChannelToUserFollowedChannels(Channel channel, AuthenticatedUser user) {
userCollection.document(user.getSciper()).update("followed_channels", FieldValue.arrayUnion(channel.getId()));
}
/**
* add the event to the list of followed events of a user
* @param event the event we want to add
* @param user the user
*/
@Override
public void addEventToUserFollowedEvents(Event event, AuthenticatedUser user) {
userCollection.document(user.getSciper()).update("followed_events", FieldValue.arrayUnion(event.getId()));
userCollection.document(user.getSciper()).update("followed_channels", FieldValue.arrayUnion(event.getChannelId()));
eventCollection.document(event.getId()).update("followers", FieldValue.arrayUnion(user.getSciper()));
}
/**
* add the association to the list of followed associations of a user
* @param association the association we want to add
* @param user the user
*/
@Override
public void addAssociationToUserFollowedAssociations(Association association, AuthenticatedUser user) {
userCollection.document(user.getSciper()).update("followed_associations", FieldValue.arrayUnion(association.getId()));
userCollection.document(user.getSciper()).update("followed_channels", FieldValue.arrayUnion(association.getChannelId()));
}
/**
* remove the channel of the list of followed channels of a user
* @param channel the channel we want to remove
* @param user the user
*/
@Override
public void removeChannelFromUserFollowedChannels(Channel channel, AuthenticatedUser user) {
userCollection.document(user.getSciper()).update("followed_channels", FieldValue.arrayRemove(channel.getId()));
}
/**
* remove the event of the list of followed events of a user
* @param event the event we want to remove
* @param user the user
*/
@Override
public void removeEventFromUserFollowedEvents(Event event, AuthenticatedUser user) {
userCollection.document(user.getSciper()).update("followed_events", FieldValue.arrayRemove(event.getId()));
userCollection.document(user.getSciper()).update("followed_channels", FieldValue.arrayRemove(event.getChannelId()));
eventCollection.document(event.getId()).update("followers", FieldValue.arrayRemove(user.getSciper()));
}
/**
* remove the association of the list of followed associations of a user
* @param association the association we want to remove
* @param user the user
*/
@Override
public void removeAssociationFromUserFollowedAssociations(Association association, AuthenticatedUser user) {
userCollection.document(user.getSciper()).update("followed_associations", FieldValue.arrayRemove(association.getId()));
userCollection.document(user.getSciper()).update("followed_channels", FieldValue.arrayRemove(association.getChannelId()));
}
/**
* get the replies from a post
* @param channelId the id of the channel containing the post
* @param postId the id of the post
* @param onResult the onResult of the list of posts
*/
@Override
public void getRepliesFromPost(String channelId, String postId, OnResult<List<Post>> onResult) {
IdlingResourceFactory.incrementCountingIdlingResource();
channelCollection.document(channelId).collection("posts")
.document(postId)
.collection("replies").getAndAddOnSuccessListener(fmapList -> {
List<Post> result = new ArrayList<>();
for (FirebaseMapDecorator data : fmapList) {
if (data.hasFields(Post.requiredFields()))
result.add(new Post(data));
}
onResult.apply(result);
IdlingResourceFactory.decrementCountingIdlingResource();
}).addOnFailureListener(onFailureWithErrorMessage("Cannot getAndAddOnSuccessListener Replies from Post " + postId));
}
/**
* update the channel when receiving a new message
* @param channelId the channel to update
* @param onResult the OnResult of the list of messages
*/
@Override
public void updateOnNewMessagesFromChannel(String channelId, OnResult<List<ChatMessage>> onResult) {
channelCollection.document(channelId).collection("messages").addSnapshotListener(fmapList -> {
List<ChatMessage> result = new ArrayList<>();
for (FirebaseMapDecorator data : fmapList) {
if (data.hasFields(ChatMessage.requiredFields()))
result.add(new ChatMessage(data));
}
onResult.apply(result);
}
);
}
/**
* add a post to a channel
* @param post the post we want to add
*/
@Override
public void addPost(Post post) {
IdlingResourceFactory.incrementCountingIdlingResource();
channelCollection.document(post.getChannelId())
.collection("posts")
.document(post.getId())
.set(post.getData());
IdlingResourceFactory.decrementCountingIdlingResource();
}
/**
* add a reply to a post
* @param post the reply
*/
@Override
public void addReply(Post post) {
IdlingResourceFactory.incrementCountingIdlingResource();
channelCollection.document(post.getChannelId())
.collection("posts")
.document(post.getOriginalPostId())
.collection("replies")
.document(post.getId())
.set(post.getData());
channelCollection.document(post.getChannelId())
.collection("posts")
.document(post.getOriginalPostId())
.update("replies", FieldValue.arrayUnion(post.getId()));
IdlingResourceFactory.decrementCountingIdlingResource();
}
/**
* update a post
* @param post the post we want to modify
*/
@Override
public void updatePost(Post post) {
if (post.getOriginalPostId() == null)
updateOriginalPost(post);
else
updateReplyPost(post);
}
/**
* update a source post
* @param post the post to modify
*/
private void updateOriginalPost(Post post) {
channelCollection.document(post.getChannelId())
.collection("posts")
.document(post.getId())
.update(post.getData());
}
/**
* update a reply post
* @param post the reply to modify
*/
private void updateReplyPost(Post post) {
channelCollection.document(post.getChannelId())
.collection("posts")
.document(post.getOriginalPostId())
.collection("replies")
.document(post.getId())
.update(post.getData());
}
/**
* add a message to a channel
* @param message the message to add
*/
public void addMessage(ChatMessage message) {
IdlingResourceFactory.incrementCountingIdlingResource();
channelCollection.document(message.getChannelId())
.collection("messages")
.document(message.getId())
.set(message.getData());
IdlingResourceFactory.decrementCountingIdlingResource();
}
//----- User related methods -----\\
/**
* update the user on the database
* @param user the user to update
*/
@Override
public void updateUser(AuthenticatedUser user) {
IdlingResourceFactory.incrementCountingIdlingResource();
userCollection.document(user.getSciper()).set(user.getData());
IdlingResourceFactory.decrementCountingIdlingResource();
}
/**
* get a user from an id and if it doesn't exist, create it on the database
* @param id the id of the user
* @param onResult the OnResult for authenticated user
*/
public void getUserWithIdOrCreateIt(String id, OnResult<AuthenticatedUser> onResult) {
IdlingResourceFactory.incrementCountingIdlingResource();
userCollection.document(id).getAndAddOnSuccessListener(fmap -> {
try {
AuthenticatedUser user = new User.UserBuilder()
.setSciper(fmap.getString("sciper"))
.setFirst_names(fmap.getString("first_name"))
.setLast_names(fmap.getString("last_name"))
.setSection(fmap.getString("section"))
.setSemester(fmap.getString("semester"))
.setGaspar(fmap.getString("gaspar"))
.setEmail(fmap.getString("email"))
.setFollowedAssociations(fmap.getStringList("followed_associations"))
.setFollowedEvents(fmap.getStringList("followed_events"))
.setFollowedChannels(fmap.getStringList("followed_channels"))
.buildAuthenticatedUser();
for (String role : fmap.getStringList("roles"))
Objects.requireNonNull(user).addRole(UserRole.valueOf(role));
onResult.apply(user);
} catch (Exception e) {
e.printStackTrace();
onResult.apply(null);
}
IdlingResourceFactory.decrementCountingIdlingResource();
}).addOnFailureListener(onFailureWithErrorMessage("Cannot set user " + id));
}
/**
* get all the users of the database
* @param onResult the OnResult of all the data
*/
@Override
public void getAllUsers(OnResult<List<Map<String, Object>>> onResult) {
IdlingResourceFactory.incrementCountingIdlingResource();
userCollection.getAndAddOnSuccessListener(fmapList -> {
List<Map<String, Object>> resultList = new ArrayList<>();
for (FirebaseMapDecorator snap : fmapList) {
Map<String, Object> map = new HashMap<>(snap.getMap());
resultList.add(map);
}
onResult.apply(resultList);
IdlingResourceFactory.decrementCountingIdlingResource();
}).addOnFailureListener(onFailureWithErrorMessage("Cannot fetch all users"));
}
/**
* update the role of a user
* @param sciper the sciper of the user
* @param roles the new roles
*/
@Override
public void updateUserRole(String sciper, List<String> roles) {
userCollection.document(sciper).update("roles", roles);
}
//----- Generating new id to store -----\\
/**
* get a new Id for a channel
* @return the new id
*/
@Override
public String getNewChannelId() {
return channelCollection.document().getId();
}
/**
* get a new Id for an event
* @return the new id
*/
@Override
public String getNewEventId() {
return eventCollection.document().getId();
}
/**
* get a new id for an association
* @return the new id
*/
@Override
public String getNewAssociationId() {
return assoCollection.document().getId();
}
/**
* get a new id for a post
* @param channelId the id of the channel containing the post
* @return the new id
*/
@Override
public String getNewPostId(String channelId) {
return channelCollection.document(channelId).collection("posts").document().getId();
}
/**
* get a new id for a message
* @param channelId the id of the channel containing the message
* @return the new id
*/
@Override
public String getNewMessageId(String channelId) {
return channelCollection.document(channelId).collection("messages").document().getId();
}
/**
* get a new id for a reply
* @param channelId the id of the channel containing the reply
* @param originalPostId the post the reply is answering to
* @return the new id
*/
@Override
public String getNewReplyId(String channelId, String originalPostId) {
return channelCollection.document(channelId).collection("posts").document(originalPostId).collection("replies").document().getId();
}
/**
* Return a new OnFailureListener logging the error in the error section of the console
*
* @param message Body of the error message
* @return OnFailureListener with customized text
*/
private OnFailureListener onFailureWithErrorMessage(String message) {
return e -> {
Log.e("PROXY", message);
IdlingResourceFactory.decrementCountingIdlingResource();
};
}
private class Counter {
private int counter = 0;
private final int end;
public Counter(int end) {
this.end = end;
}
public boolean increment() {
counter++;
return counter >= end;
}
}
}