/*
* Copyright 2017 Google Inc.
*
* 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.google.firebase.database.integration;
import com.google.firebase.database.ChildEventListener;
import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseException;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.EventRecord;
import com.google.firebase.database.ValueEventListener;
import com.google.firebase.database.core.view.Event;
import com.google.firebase.testing.TestUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
class EventHelper {
private List<Expectation> lookingFor;
private Set<DatabaseReference> locations;
private Set<DatabaseReference> toListen;
private List<EventRecord> results;
private Set<DatabaseReference> uninitializedRefs;
private int initializationEvents = 0;
private boolean success;
private Semaphore semaphore;
private Semaphore initializationSemaphore;
private boolean waitingForInitialization = false;
private Map<DatabaseReference, ValueEventListener> valueListeners = new HashMap<>();
private Map<DatabaseReference, ChildEventListener> childListeners = new HashMap<>();
EventHelper() {
lookingFor = new ArrayList<>();
locations = new HashSet<>();
toListen = new HashSet<>();
results = new ArrayList<>();
semaphore = new Semaphore(1);
uninitializedRefs = new HashSet<>();
initializationSemaphore = new Semaphore(0);
}
EventHelper addValueExpectation(DatabaseReference ref) {
if (!locations.contains(ref)) {
toListen.add(ref);
}
lookingFor.add(new Expectation(Event.EventType.VALUE, ref.toString()));
return this;
}
EventHelper addChildExpectation(
DatabaseReference ref, Event.EventType eventType, String childName) throws DatabaseException {
if (!locations.contains(ref)) {
toListen.add(ref);
}
lookingFor.add(new Expectation(eventType, ref.child(childName).toString()));
return this;
}
EventHelper startListening() throws InterruptedException {
return startListening(false);
}
EventHelper startListening(boolean waitForInitialization) throws InterruptedException {
waitingForInitialization = waitForInitialization;
semaphore.acquire(1);
locations.addAll(toListen);
List<DatabaseReference> locationList =
Arrays.asList(toListen.toArray(new DatabaseReference[] {}));
Collections.sort(
locationList,
new Comparator<DatabaseReference>() {
@Override
public int compare(DatabaseReference o1, DatabaseReference o2) {
int o1Length = o1.toString().length();
int o2Length = o2.toString().length();
if (o1Length < o2Length) {
return -1;
} else if (o1Length == o2Length) {
return 0;
} else {
return 1;
}
}
});
for (DatabaseReference location : locationList) {
if (waitForInitialization) {
uninitializedRefs.add(location);
}
listen(location);
}
toListen.clear();
if (waitForInitialization) {
initializationSemaphore.tryAcquire(
locationList.size(), TestUtils.TEST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
// Cut out the initialization events
synchronized (this) {
waitingForInitialization = false;
results = results.subList(initializationEvents, results.size());
}
}
return this;
}
private void listen(final DatabaseReference ref) {
valueListeners.put(
ref,
ref.addValueEventListener(
new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot snapshot) {
recordEvent(new EventRecord(snapshot, Event.EventType.VALUE, null));
if (uninitializedRefs.remove(ref)) {
initializationSemaphore.release(1);
initializationEvents++;
}
}
@Override
public void onCancelled(DatabaseError error) {
// No-op
}
}));
childListeners.put(
ref,
ref.addChildEventListener(
new ChildEventListener() {
@Override
public void onChildAdded(DataSnapshot snapshot, String previousChildName) {
recordEvent(
new EventRecord(snapshot, Event.EventType.CHILD_ADDED, previousChildName));
if (uninitializedRefs.contains(ref)) {
initializationEvents++;
}
}
@Override
public void onChildChanged(DataSnapshot snapshot, String previousChildName) {
if (uninitializedRefs.contains(ref)) {
initializationEvents++;
}
recordEvent(
new EventRecord(snapshot, Event.EventType.CHILD_CHANGED, previousChildName));
}
@Override
public void onChildRemoved(DataSnapshot snapshot) {
if (uninitializedRefs.contains(ref)) {
initializationEvents++;
}
recordEvent(new EventRecord(snapshot, Event.EventType.CHILD_REMOVED, null));
}
@Override
public void onChildMoved(DataSnapshot snapshot, String previousChildName) {
if (uninitializedRefs.contains(ref)) {
initializationEvents++;
}
recordEvent(
new EventRecord(snapshot, Event.EventType.CHILD_MOVED, previousChildName));
}
@Override
public void onCancelled(DatabaseError error) {
// No-op
}
}));
}
private void recordEvent(EventRecord record) {
synchronized (this) {
results.add(record);
checkSuccess();
}
}
void cleanup() {
for (Map.Entry<DatabaseReference, ValueEventListener> entry : valueListeners.entrySet()) {
entry.getKey().removeEventListener(entry.getValue());
}
for (Map.Entry<DatabaseReference, ChildEventListener> entry : childListeners.entrySet()) {
entry.getKey().removeEventListener(entry.getValue());
}
}
private void checkSuccess() {
if (!waitingForInitialization) {
int index = results.size() - 1;
if (index >= lookingFor.size()) {
// we've seen too many events
System.out.println("we've seen too many events");
cleanup();
success = false;
semaphore.release(1);
} else if (lookingFor.get(index).matches(results.get(index))) {
if (index == lookingFor.size() - 1) {
// we've seen all the events we're looking for
success = true;
semaphore.release(1);
}
} else {
success = false;
cleanup();
semaphore.release(1);
}
}
}
boolean waitForEvents() throws InterruptedException {
// Try waiting on the semaphore
if (!semaphore.tryAcquire(1, TestUtils.TEST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) {
return false;
} else {
semaphore.release(1);
return success;
}
}
private static class Expectation {
private Event.EventType eventType;
private String location;
private Expectation(Event.EventType eventType, String location) {
this.eventType = eventType;
this.location = location;
}
boolean matches(EventRecord record) {
return record.getEventType().equals(eventType)
&& record.getSnapshot().getRef().toString().equals(location);
}
@Override
public String toString() {
return this.eventType + " => " + this.location;
}
}
}