com.google.firebase.database.DatabaseReference Maven / Gradle / Ivy
/*
* 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;
import com.google.api.core.ApiFuture;
import com.google.firebase.database.core.CompoundWrite;
import com.google.firebase.database.core.DatabaseConfig;
import com.google.firebase.database.core.Path;
import com.google.firebase.database.core.Repo;
import com.google.firebase.database.core.RepoManager;
import com.google.firebase.database.core.ValidationPath;
import com.google.firebase.database.snapshot.ChildKey;
import com.google.firebase.database.snapshot.Node;
import com.google.firebase.database.snapshot.NodeUtilities;
import com.google.firebase.database.snapshot.PriorityUtilities;
import com.google.firebase.database.utilities.Pair;
import com.google.firebase.database.utilities.ParsedUrl;
import com.google.firebase.database.utilities.PushIdGenerator;
import com.google.firebase.database.utilities.Utilities;
import com.google.firebase.database.utilities.Validation;
import com.google.firebase.database.utilities.encoding.CustomClassMapper;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Map;
/**
* A Firebase reference represents a particular location in your Database and can be used for
* reading or writing data to that Database location.
*
* This class is the starting point for all Database operations. After you've initialized it with
* a URL, you can use it to read data, write data, and to create new DatabaseReferences.
*/
public class DatabaseReference extends Query {
private static DatabaseConfig defaultConfig;
/**
* @param repo The repo for this ref
* @param path The path to reference
*/
DatabaseReference(Repo repo, Path path) {
super(repo, path);
}
/** Legacy method left here (as package private) for tests. */
DatabaseReference(String url, DatabaseConfig config) {
this(Utilities.parseUrl(url), config);
}
private DatabaseReference(ParsedUrl parsedUrl, DatabaseConfig config) {
this(RepoManager.getRepo(config, parsedUrl.repoInfo), parsedUrl.path);
}
/**
* Manually disconnect the Firebase Database client from the server and disable automatic
* reconnection.
*
*
Note: Invoking this method will impact all Firebase Database connections.
*/
public static void goOffline() {
goOffline(getDefaultConfig());
}
static void goOffline(DatabaseConfig config) {
RepoManager.interrupt(config);
}
/**
* Manually reestablish a connection to the Firebase Database server and enable automatic
* reconnection.
*
*
Note: Invoking this method will impact all Firebase Database connections.
*/
public static void goOnline() {
goOnline(getDefaultConfig());
}
static void goOnline(DatabaseConfig config) {
RepoManager.resume(config);
}
/**
* Legacy method for legacy creation of DatabaseReference for tests.
*
* @return A reference to the default config object. This can be modified up until your first
* Database call
*/
private static synchronized DatabaseConfig getDefaultConfig() {
if (defaultConfig == null) {
defaultConfig = new DatabaseConfig();
}
return defaultConfig;
}
/**
* Get a reference to location relative to this one
*
* @param pathString The relative path from this reference to the new one that should be created
* @return A new DatabaseReference to the given path
*/
public DatabaseReference child(String pathString) {
if (pathString == null) {
throw new NullPointerException("Can't pass null for argument 'pathString' in child()");
}
if (getPath().isEmpty()) {
// If this is the root of the tree, allow '.info' nodes.
Validation.validateRootPathString(pathString);
} else {
Validation.validatePathString(pathString);
}
Path childPath = getPath().child(new Path(pathString));
return new DatabaseReference(repo, childPath);
}
/**
* Create a reference to an auto-generated child location. The child key is generated client-side
* and incorporates an estimate of the server's time for sorting purposes. Locations generated on
* a single client will be sorted in the order that they are created, and will be sorted
* approximately in order across all clients.
*
* @return A DatabaseReference pointing to the new location
*/
public DatabaseReference push() {
String childNameStr = PushIdGenerator.generatePushChildName(repo.getServerTime());
ChildKey childKey = ChildKey.fromString(childNameStr);
return new DatabaseReference(repo, getPath().child(childKey));
}
/**
* Set the data at this location to the given value. Passing null to setValue() will delete the
* data at the specified location. The native types accepted by this method for the value
* correspond to the JSON types:
*
*
* - Boolean
*
- Long
*
- Double
*
- Map<String, Object>
*
- List<Object>
*
*
*
*
* In addition, you can set instances of your own class into this location, provided they satisfy
* the following constraints:
*
*
* - The class must have a default constructor that takes no arguments
*
- The class must define public getters for the properties to be assigned. Properties
* without a public getter will be set to their default value when an instance is
* deserialized
*
*
*
*
* Generic collections of objects that satisfy the above constraints are also permitted, i.e.
* Map<String, MyPOJO>
, as well as null values.
*
* @param value The value to set at this location
* @return The ApiFuture for this operation.
*/
public ApiFuture setValueAsync(Object value) {
return setValueInternal(value, PriorityUtilities.parsePriority(this.path, null), null);
}
/**
* Set the data and priority to the given values. Passing null to setValue() will delete the data
* at the specified location. The native types accepted by this method for the value correspond to
* the JSON types:
*
*
* - Boolean
*
- Long
*
- Double
*
- Map<String, Object>
*
- List<Object>
*
*
*
*
* In addition, you can set instances of your own class into this location, provided they satisfy
* the following constraints:
*
*
* - The class must have a default constructor that takes no arguments
*
- The class must define public getters for the properties to be assigned. Properties
* without a public getter will be set to their default value when an instance is
* deserialized
*
*
*
*
* Generic collections of objects that satisfy the above constraints are also permitted, i.e.
* Map<String, MyPOJO>
, as well as null values.
*
* @param value The value to set at this location
* @param priority The priority to set at this location
* @return The ApiFuture for this operation.
*/
public ApiFuture setValueAsync(Object value, Object priority) {
return setValueInternal(value, PriorityUtilities.parsePriority(this.path, priority), null);
}
/**
* Set the data at this location to the given value. Passing null to setValue() will delete the
* data at the specified location. The native types accepted by this method for the value
* correspond to the JSON types:
*
*
* - Boolean
*
- Long
*
- Double
*
- Map<String, Object>
*
- List<Object>
*
*
*
*
* In addition, you can set instances of your own class into this location, provided they satisfy
* the following constraints:
*
*
* - The class must have a default constructor that takes no arguments
*
- The class must define public getters for the properties to be assigned. Properties
* without a public getter will be set to their default value when an instance is
* deserialized
*
*
*
*
* Generic collections of objects that satisfy the above constraints are also permitted, i.e.
* Map<String, MyPOJO>
, as well as null values.
*
* @param value The value to set at this location
* @param listener A listener that will be triggered with the results of the operation
*/
public void setValue(Object value, CompletionListener listener) {
setValueInternal(value, PriorityUtilities.parsePriority(this.path, null), listener);
}
/**
* Set the data and priority to the given values. The native types accepted by this method for the
* value correspond to the JSON types:
*
*
* - Boolean
*
- Long
*
- Double
*
- Map<String, Object>
*
- List<Object>
*
*
*
*
* In addition, you can set instances of your own class into this location, provided they satisfy
* the following constraints:
*
*
* - The class must have a default constructor that takes no arguments
*
- The class must define public getters for the properties to be assigned. Properties
* without a public getter will be set to their default value when an instance is
* deserialized
*
*
*
*
* Generic collections of objects that satisfy the above constraints are also permitted, i.e.
* Map<String, MyPOJO>
, as well as null values.
*
* @param value The value to set at this location
* @param priority The priority to set at this location
* @param listener A listener that will be triggered with the results of the operation
*/
public void setValue(Object value, Object priority, CompletionListener listener) {
setValueInternal(value, PriorityUtilities.parsePriority(this.path, priority), listener);
}
private ApiFuture setValueInternal(Object value, Node priority, CompletionListener
optListener) {
Validation.validateWritablePath(getPath());
ValidationPath.validateWithObject(getPath(), value);
Object bouncedValue = CustomClassMapper.convertToPlainJavaTypes(value);
Validation.validateWritableObject(bouncedValue);
final Node node = NodeUtilities.NodeFromJSON(bouncedValue, priority);
final Pair, CompletionListener> wrapped = Utilities.wrapOnComplete(optListener);
repo.scheduleNow(
new Runnable() {
@Override
public void run() {
repo.setValue(getPath(), node, wrapped.getSecond());
}
});
return wrapped.getFirst();
}
// Set priority
/**
* Set a priority for the data at this Database location. Priorities can be used to provide a
* custom ordering for the children at a location (if no priorities are specified, the children
* are ordered by key).
*
* You cannot set a priority on an empty location. For this reason setValue(data, priority) should
* be used when setting initial data with a specific priority and setPriority should be used when
* updating the priority of existing data.
*
* Children are sorted based on this priority using the following rules:
*
*
* - Children with no priority come first.
*
- Children with a number as their priority come next. They are sorted numerically by
* priority (small to large).
*
- Children with a string as their priority come last. They are sorted lexicographically by
* priority.
*
- Whenever two children have the same priority (including no priority), they are sorted by
* key. Numeric keys come first (sorted numerically), followed by the remaining keys (sorted
* lexicographically).
*
*
* Note that numerical priorities are parsed and ordered as IEEE 754 double-precision
* floating-point numbers. Keys are always stored as strings and are treated as numeric only when
* they can be parsed as a 32-bit integer.
*
* @param priority The priority to set at the specified location.
* @return The ApiFuture for this operation.
*/
public ApiFuture setPriorityAsync(Object priority) {
return setPriorityInternal(PriorityUtilities.parsePriority(this.path, priority), null);
}
/**
* Set a priority for the data at this Database location. Priorities can be used to provide a
* custom ordering for the children at a location (if no priorities are specified, the children
* are ordered by key).
*
* You cannot set a priority on an empty location. For this reason setValue(data, priority) should
* be used when setting initial data with a specific priority and setPriority should be used when
* updating the priority of existing data.
*
* Children are sorted based on this priority using the following rules:
*
*
* - Children with no priority come first.
*
- Children with a number as their priority come next. They are sorted numerically by
* priority (small to large).
*
- Children with a string as their priority come last. They are sorted lexicographically by
* priority.
*
- Whenever two children have the same priority (including no priority), they are sorted by
* key. Numeric keys come first (sorted numerically), followed by the remaining keys (sorted
* lexicographically).
*
*
* Note that numerical priorities are parsed and ordered as IEEE 754 double-precision
* floating-point numbers. Keys are always stored as strings and are treated as numeric only when
* they can be parsed as a 32-bit integer.
*
* @param priority The priority to set at the specified location.
* @param listener A listener that will be triggered with results of the operation
*/
public void setPriority(Object priority, CompletionListener listener) {
setPriorityInternal(PriorityUtilities.parsePriority(this.path, priority), listener);
}
// Remove
private ApiFuture setPriorityInternal(final Node priority, CompletionListener optListener) {
Validation.validateWritablePath(getPath());
final Pair, CompletionListener> wrapped = Utilities.wrapOnComplete(optListener);
repo.scheduleNow(
new Runnable() {
@Override
public void run() {
repo.setValue(
getPath().child(ChildKey.getPriorityKey()), priority, wrapped.getSecond());
}
});
return wrapped.getFirst();
}
// Update
/**
* Update the specific child keys to the specified values. Passing null in a map to
* updateChildren() will remove the value at the specified location.
*
* @param update The paths to update and their new values
* @return The ApiFuture for this operation.
*/
public ApiFuture updateChildrenAsync(Map update) {
return updateChildrenInternal(update, null);
}
// Access to disconnect operations
/**
* Update the specific child keys to the specified values. Passing null in a map to
* updateChildren() will remove the value at the specified location.
*
* @param update The paths to update and their new values
* @param listener A listener that will be triggered with results of the operation
*/
public void updateChildren(final Map update, final CompletionListener listener) {
updateChildrenInternal(update, listener);
}
// Transactions
private ApiFuture updateChildrenInternal(
final Map update, final CompletionListener optListener) {
if (update == null) {
throw new NullPointerException("Can't pass null for argument 'update' in updateChildren()");
}
final Map bouncedUpdate = CustomClassMapper.convertToPlainJavaTypes(update);
final Map parsedUpdate =
Validation.parseAndValidateUpdate(getPath(), bouncedUpdate);
final CompoundWrite merge = CompoundWrite.fromPathMerge(parsedUpdate);
final Pair, CompletionListener> wrapped = Utilities.wrapOnComplete(optListener);
repo.scheduleNow(
new Runnable() {
@Override
public void run() {
repo.updateChildren(getPath(), merge, wrapped.getSecond(), bouncedUpdate);
}
});
return wrapped.getFirst();
}
/**
* Set the value at this location to 'null'
*
* @return The ApiFuture for this operation.
*/
public ApiFuture removeValueAsync() {
return setValueAsync(null);
}
// Manual Connection Management
/*
* The Firebase Database client automatically maintains a persistent connection to the Database
* server, which will remain active indefinitely and reconnect when disconnected. However, the
* goOffline( ) and goOnline( ) methods may be used to manually control the client connection in
* cases where a persistent connection is undesirable.
*
* While offline, the Firebase Database client will no longer receive data updates from the
* server. However, all Database operations performed locally will continue to immediately fire
* events, allowing your application to continue behaving normally. Additionally, each operation
* performed locally will automatically be queued and retried upon reconnection to the Database
* server.
*
*
To reconnect to the Database server and begin receiving remote events, see goOnline( ). Once
* the connection is reestablished, the Database client will transmit the appropriate data and
* fire the appropriate events so that your client "catches up" automatically.
*/
/**
* Set the value at this location to 'null'
*
* @param listener A listener that will be triggered when the operation is complete
*/
public void removeValue(CompletionListener listener) {
setValue(null, listener);
}
/**
* Provides access to disconnect operations at this location
*
* @return An object for managing disconnect operations at this location
*/
public OnDisconnect onDisconnect() {
Validation.validateWritablePath(getPath());
return new OnDisconnect(repo, getPath());
}
/**
* Run a transaction on the data at this location. For more information on running transactions,
* see {@link com.google.firebase.database.Transaction.Handler Transaction.Handler}.
*
* @param handler An object to handle running the transaction
*/
public void runTransaction(Transaction.Handler handler) {
runTransaction(handler, true);
}
/**
* Run a transaction on the data at this location. For more information on running transactions,
* see {@link com.google.firebase.database.Transaction.Handler Transaction.Handler}.
*
* @param handler An object to handle running the transaction
* @param fireLocalEvents Defaults to true. If set to false, events will only be fired for the
* final result state of the transaction, and not for any intermediate states
*/
public void runTransaction(final Transaction.Handler handler, final boolean fireLocalEvents) {
if (handler == null) {
throw new NullPointerException("Can't pass null for argument 'handler' in runTransaction()");
}
Validation.validateWritablePath(getPath());
repo.scheduleNow(
new Runnable() {
@Override
public void run() {
repo.startTransaction(getPath(), handler, fireLocalEvents);
}
});
}
// Getters and other auxiliary methods
/**
* Gets the Database instance associated with this reference.
*
* @return The Database object for this reference.
*/
public FirebaseDatabase getDatabase() {
return this.repo.getDatabase();
}
/**
* @return The full location url for this reference
*/
@Override
public String toString() {
DatabaseReference parent = getParent();
if (parent == null) {
return repo.toString();
} else {
try {
return parent.toString() + "/" + URLEncoder.encode(getKey(), "UTF-8").replace("+", "%20");
} catch (UnsupportedEncodingException e) {
throw new DatabaseException("Failed to URLEncode key: " + getKey(), e);
}
}
}
/**
* @return A DatabaseReference to the parent location, or null if this instance references the
* root location
*/
public DatabaseReference getParent() {
Path parentPath = getPath().getParent();
if (parentPath != null) {
return new DatabaseReference(repo, parentPath);
} else {
return null;
}
}
/**
* @return A reference to the root location of this Firebase Database
*/
public DatabaseReference getRoot() {
return new DatabaseReference(repo, new Path(""));
}
/**
* @return The last token in the location pointed to by this reference
*/
public String getKey() {
if (getPath().isEmpty()) {
return null;
}
return getPath().getBack().asString();
}
@Override
public boolean equals(Object other) {
return other instanceof DatabaseReference && toString().equals(other.toString());
}
@Override
public int hashCode() {
return toString().hashCode();
}
void setHijackHash(final boolean hijackHash) {
repo.scheduleNow(
new Runnable() {
@Override
public void run() {
repo.setHijackHash(hijackHash);
}
});
}
/**
* This interface is used as a method of being notified when an operation has been acknowledged by
* the Database servers and can be considered complete
*
* @since 1.1
*/
public interface CompletionListener {
/**
* This method will be triggered when the operation has either succeeded or failed. If it has
* failed, an error will be given. If it has succeeded, the error will be null
*
* @param error A description of any errors that occurred or null on success
* @param ref A reference to the specified Firebase Database location
*/
void onComplete(final DatabaseError error, final DatabaseReference ref);
}
}