Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Parse/src/main/java/com/parse/FileObjectStore.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@

/** package */ class FileObjectStore<T extends ParseObject> implements ParseObjectStore<T> {

private static ParseObjectSubclassingController getSubclassingController() {
return ParseCorePlugins.getInstance().getSubclassingController();
}

/**
* Saves the {@code ParseObject} to the a file on disk as JSON in /2/ format.
*
Expand Down Expand Up @@ -75,7 +79,7 @@ private static <T extends ParseObject> T getFromDisk(
private final ParseObjectCurrentCoder coder;

public FileObjectStore(Class<T> clazz, File file, ParseObjectCurrentCoder coder) {
this(ParseObject.getClassName(clazz), file, coder);
this(getSubclassingController().getClassName(clazz), file, coder);
}

public FileObjectStore(String className, File file, ParseObjectCurrentCoder coder) {
Expand Down
6 changes: 5 additions & 1 deletion Parse/src/main/java/com/parse/OfflineObjectStore.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@

/** package */ class OfflineObjectStore<T extends ParseObject> implements ParseObjectStore<T> {

private static ParseObjectSubclassingController getSubclassingController() {
return ParseCorePlugins.getInstance().getSubclassingController();
}

private static <T extends ParseObject> Task<T> migrate(
final ParseObjectStore<T> from, final ParseObjectStore<T> to) {
return from.getAsync().onSuccessTask(new Continuation<T, Task<T>>() {
Expand Down Expand Up @@ -44,7 +48,7 @@ public T then(Task<Void> task) throws Exception {
private final ParseObjectStore<T> legacy;

public OfflineObjectStore(Class<T> clazz, String pinName, ParseObjectStore<T> legacy) {
this(ParseObject.getClassName(clazz), pinName, legacy);
this(getSubclassingController().getClassName(clazz), pinName, legacy);
}

public OfflineObjectStore(String className, String pinName, ParseObjectStore<T> legacy) {
Expand Down
16 changes: 16 additions & 0 deletions Parse/src/main/java/com/parse/ParseCorePlugins.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public static ParseCorePlugins getInstance() {
private AtomicReference<ParseDefaultACLController> defaultACLController = new AtomicReference<>();

private AtomicReference<LocalIdManager> localIdManager = new AtomicReference<>();
private AtomicReference<ParseObjectSubclassingController> subclassingController = new AtomicReference<>();

private ParseCorePlugins() {
// do nothing
Expand Down Expand Up @@ -336,5 +337,20 @@ public void registerLocalIdManager(LocalIdManager manager) {
"Another localId manager was already registered: " + localIdManager.get());
}
}

public ParseObjectSubclassingController getSubclassingController() {
if (subclassingController.get() == null) {
ParseObjectSubclassingController controller = new ParseObjectSubclassingController();
subclassingController.compareAndSet(null, controller);
}
return subclassingController.get();
}

public void registerSubclassingController(ParseObjectSubclassingController controller) {
if (!subclassingController.compareAndSet(null, controller)) {
throw new IllegalStateException(
"Another subclassing controller was already registered: " + subclassingController.get());
}
}
}

100 changes: 11 additions & 89 deletions Parse/src/main/java/com/parse/ParseObject.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@
import org.json.JSONException;
import org.json.JSONObject;

import java.lang.reflect.Member;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
Expand All @@ -29,7 +27,6 @@
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Lock;

Expand Down Expand Up @@ -72,11 +69,6 @@ public class ParseObject {
// and not check after a while
private static final String KEY_IS_DELETING_EVENTUALLY_OLD = "isDeletingEventually";

private static final Map<Class<? extends ParseObject>, String> classNames =
new ConcurrentHashMap<>();
private static final Map<String, Class<? extends ParseObject>> objectTypes =
new ConcurrentHashMap<>();

private static ParseObjectController getObjectController() {
return ParseCorePlugins.getInstance().getObjectController();
}
Expand All @@ -85,6 +77,10 @@ private static LocalIdManager getLocalIdManager() {
return ParseCorePlugins.getInstance().getLocalIdManager();
}

private static ParseObjectSubclassingController getSubclassingController() {
return ParseCorePlugins.getInstance().getSubclassingController();
}

/** package */ static class State {

public static Init<?> newBuilder(String className) {
Expand Down Expand Up @@ -360,23 +356,15 @@ public ParseObject(String theClassName) {
"You must specify a Parse class name when creating a new ParseObject.");
}
if (AUTO_CLASS_NAME.equals(theClassName)) {
theClassName = getClassName(this.getClass());
theClassName = getSubclassingController().getClassName(getClass());
}

// If this is supposed to be created by a factory but wasn't, throw an exception.
if (this.getClass().equals(ParseObject.class) && objectTypes.containsKey(theClassName)
&& !objectTypes.get(theClassName).isInstance(this)) {
if (!getSubclassingController().isSubclassValid(theClassName, getClass())) {
throw new IllegalArgumentException(
"You must create this type of ParseObject using ParseObject.create() or the proper subclass.");
}

// If this is an unregistered subclass, throw an exception.
if (!this.getClass().equals(ParseObject.class)
&& !this.getClass().equals(objectTypes.get(theClassName))) {
throw new IllegalArgumentException(
"You must register this ParseObject subclass before instantiating it.");
}

operationSetQueue = new LinkedList<>();
operationSetQueue.add(new ParseOperationSet());
estimatedData = new HashMap<>();
Expand Down Expand Up @@ -410,17 +398,7 @@ public ParseObject(String theClassName) {
* @return A new {@code ParseObject} for the given class name.
*/
public static ParseObject create(String className) {
if (objectTypes.containsKey(className)) {
try {
return objectTypes.get(className).newInstance();
} catch (Exception e) {
if (e instanceof RuntimeException) {
throw (RuntimeException) e;
}
throw new RuntimeException("Failed to create instance of subclass.", e);
}
}
return new ParseObject(className);
return getSubclassingController().newInstance(className);
}

/**
Expand All @@ -434,7 +412,7 @@ public static ParseObject create(String className) {
*/
@SuppressWarnings("unchecked")
public static <T extends ParseObject> T create(Class<T> subclass) {
return (T) create(getClassName(subclass));
return (T) create(getSubclassingController().getClassName(subclass));
}

/**
Expand Down Expand Up @@ -497,13 +475,7 @@ public static ParseObject createWithoutData(String className, String objectId) {
*/
@SuppressWarnings({"unused", "unchecked"})
public static <T extends ParseObject> T createWithoutData(Class<T> subclass, String objectId) {
return (T) createWithoutData(getClassName(subclass), objectId);
}

private static boolean isAccessible(Member m) {
return Modifier.isPublic(m.getModifiers())
|| (m.getDeclaringClass().getPackage().getName().equals("com.parse")
&& !Modifier.isPrivate(m.getModifiers()) && !Modifier.isProtected(m.getModifiers()));
return (T) createWithoutData(getSubclassingController().getClassName(subclass), objectId);
}

/**
Expand All @@ -515,41 +487,11 @@ private static boolean isAccessible(Member m) {
* The subclass type to register.
*/
public static void registerSubclass(Class<? extends ParseObject> subclass) {
String className = getClassName(subclass);
if (className == null) {
throw new IllegalArgumentException("No ParseClassName annotation provided on " + subclass);
}
if (subclass.getDeclaredConstructors().length > 0) {
try {
if (!isAccessible(subclass.getDeclaredConstructor())) {
throw new IllegalArgumentException("Default constructor for " + subclass
+ " is not accessible.");
}
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException("No default constructor provided for " + subclass);
}
}
Class<? extends ParseObject> oldValue = objectTypes.get(className);
if (oldValue != null && subclass.isAssignableFrom(oldValue)) {
// The old class was already more descendant than the new subclass type. No-op.
return;
}
objectTypes.put(className, subclass);
if (oldValue != null && !subclass.equals(oldValue)) {
if (className.equals(getClassName(ParseUser.class))) {
ParseUser.getCurrentUserController().clearFromMemory();
} else if (className.equals(getClassName(ParseInstallation.class))) {
ParseInstallation.getCurrentInstallationController().clearFromMemory();
}
}
getSubclassingController().registerSubclass(subclass);
}

/* package for tests */ static void unregisterSubclass(Class<? extends ParseObject> subclass) {
unregisterSubclass(getClassName(subclass));
}

/* package for tests */ static void unregisterSubclass(String className) {
objectTypes.remove(className);
getSubclassingController().unregisterSubclass(subclass);
}

/**
Expand Down Expand Up @@ -3516,26 +3458,6 @@ public boolean hasSameId(ParseObject other) {
}
}

/**
* Gets the class name based on the {@link ParseClassName} annotation associated with a class.
*
* @param clazz
* The class to inspect.
* @return The name of the Parse class, if one is provided. Otherwise, {@code null}.
*/
static String getClassName(Class<? extends ParseObject> clazz) {
String name = classNames.get(clazz);
if (name == null) {
ParseClassName info = clazz.getAnnotation(ParseClassName.class);
if (info == null) {
return null;
}
name = info.value();
classNames.put(clazz, name);
}
return name;
}

/**
* Called when a non-pointer is being created to allow additional initialization to occur.
*/
Expand Down
130 changes: 130 additions & 0 deletions Parse/src/main/java/com/parse/ParseObjectSubclassingController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;

import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;

/* package */ class ParseObjectSubclassingController {
private final Object mutex = new Object();
private final Map<String, Constructor<? extends ParseObject>> registeredSubclasses = new HashMap<>();

/* package */ String getClassName(Class<? extends ParseObject> clazz) {
ParseClassName info = clazz.getAnnotation(ParseClassName.class);
if (info == null) {
throw new IllegalArgumentException("No ParseClassName annotation provided on " + clazz);
}
return info.value();
}

/* package */ boolean isSubclassValid(String className, Class<? extends ParseObject> clazz) {
Constructor<? extends ParseObject> constructor = null;

synchronized (mutex) {
constructor = registeredSubclasses.get(className);
}

return constructor == null
? clazz == ParseObject.class
: constructor.getDeclaringClass() == clazz;
}

/* package */ void registerSubclass(Class<? extends ParseObject> clazz) {
if (!ParseObject.class.isAssignableFrom(clazz)) {
throw new IllegalArgumentException("Cannot register a type that is not a subclass of ParseObject");
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we ever hit this condition? We have strong typing already in the method signature that clazz has to extend ParseObject

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Theoretically yes, especially if we go the route of runtime searching for automatic registration. It's mostly just a sanity check, though.


String className = getClassName(clazz);
Constructor<? extends ParseObject> previousConstructor = null;

synchronized (mutex) {
previousConstructor = registeredSubclasses.get(className);
if (previousConstructor != null) {
Class<? extends ParseObject> previousClass = previousConstructor.getDeclaringClass();
if (clazz.isAssignableFrom(previousClass)) {
// Previous subclass is more specific or equal to the current type, do nothing.
return;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any reason why we have return here but not in the next condition?

} else if (previousClass.isAssignableFrom(clazz)) {
// Previous subclass is parent of new child subclass, fallthrough and actually
// register this class.
/* Do nothing */
} else {
throw new IllegalArgumentException(
"Tried to register both " + previousClass.getName() + " and " + clazz.getName() +
" as the ParseObject subclass of " + className + ". " + "Cannot determine the right " +
"class to use because neither inherits from the other."
);
}
}

try {
registeredSubclasses.put(className, getConstructor(clazz));
} catch (NoSuchMethodException ex) {
throw new IllegalArgumentException(
"Cannot register a type that does not implement the default constructor!"
);
} catch (IllegalAccessException ex) {
throw new IllegalArgumentException(
"Default constructor for " + clazz + " is not accessible."
);
}
}

if (previousConstructor != null) {
// TODO: This is super tightly coupled. Let's remove it when automatic registration is in.
// NOTE: Perform this outside of the mutex, to prevent any potential deadlocks.
if (className.equals(getClassName(ParseUser.class))) {
ParseUser.getCurrentUserController().clearFromMemory();
} else if (className.equals(getClassName(ParseInstallation.class))) {
ParseInstallation.getCurrentInstallationController().clearFromMemory();
}
}
}

/* package */ void unregisterSubclass(Class<? extends ParseObject> clazz) {
String className = getClassName(clazz);

synchronized (mutex) {
registeredSubclasses.remove(className);
}
}

/* package */ ParseObject newInstance(String className) {
Constructor<? extends ParseObject> constructor = null;

synchronized (mutex) {
constructor = registeredSubclasses.get(className);
}

try {
return constructor != null
? constructor.newInstance()
: new ParseObject(className);
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException("Failed to create instance of subclass.", e);
}
}

private static Constructor<? extends ParseObject> getConstructor(Class<? extends ParseObject> clazz) throws NoSuchMethodException, IllegalAccessException {
Constructor<? extends ParseObject> constructor = clazz.getDeclaredConstructor();
if (constructor == null) {
throw new NoSuchMethodException();
}
int modifiers = constructor.getModifiers();
if (Modifier.isPublic(modifiers) || (clazz.getPackage().getName().equals("com.parse") &&
!(Modifier.isProtected(modifiers) || Modifier.isPrivate(modifiers)))) {
return constructor;
}
throw new IllegalAccessException();
}
}
Loading