diff --git a/Parse/src/main/java/com/parse/FileObjectStore.java b/Parse/src/main/java/com/parse/FileObjectStore.java index 0baf67b00..8bb8a8f58 100644 --- a/Parse/src/main/java/com/parse/FileObjectStore.java +++ b/Parse/src/main/java/com/parse/FileObjectStore.java @@ -19,6 +19,10 @@ /** package */ class FileObjectStore implements ParseObjectStore { + private static ParseObjectSubclassingController getSubclassingController() { + return ParseCorePlugins.getInstance().getSubclassingController(); + } + /** * Saves the {@code ParseObject} to the a file on disk as JSON in /2/ format. * @@ -75,7 +79,7 @@ private static T getFromDisk( private final ParseObjectCurrentCoder coder; public FileObjectStore(Class 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) { diff --git a/Parse/src/main/java/com/parse/OfflineObjectStore.java b/Parse/src/main/java/com/parse/OfflineObjectStore.java index 1a7d26ae5..526d6fb8a 100644 --- a/Parse/src/main/java/com/parse/OfflineObjectStore.java +++ b/Parse/src/main/java/com/parse/OfflineObjectStore.java @@ -16,6 +16,10 @@ /** package */ class OfflineObjectStore implements ParseObjectStore { + private static ParseObjectSubclassingController getSubclassingController() { + return ParseCorePlugins.getInstance().getSubclassingController(); + } + private static Task migrate( final ParseObjectStore from, final ParseObjectStore to) { return from.getAsync().onSuccessTask(new Continuation>() { @@ -44,7 +48,7 @@ public T then(Task task) throws Exception { private final ParseObjectStore legacy; public OfflineObjectStore(Class clazz, String pinName, ParseObjectStore legacy) { - this(ParseObject.getClassName(clazz), pinName, legacy); + this(getSubclassingController().getClassName(clazz), pinName, legacy); } public OfflineObjectStore(String className, String pinName, ParseObjectStore legacy) { diff --git a/Parse/src/main/java/com/parse/ParseCorePlugins.java b/Parse/src/main/java/com/parse/ParseCorePlugins.java index 366e8cb2e..bad4e7c23 100644 --- a/Parse/src/main/java/com/parse/ParseCorePlugins.java +++ b/Parse/src/main/java/com/parse/ParseCorePlugins.java @@ -49,6 +49,7 @@ public static ParseCorePlugins getInstance() { private AtomicReference defaultACLController = new AtomicReference<>(); private AtomicReference localIdManager = new AtomicReference<>(); + private AtomicReference subclassingController = new AtomicReference<>(); private ParseCorePlugins() { // do nothing @@ -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()); + } + } } diff --git a/Parse/src/main/java/com/parse/ParseObject.java b/Parse/src/main/java/com/parse/ParseObject.java index f1495c573..56a79a49d 100644 --- a/Parse/src/main/java/com/parse/ParseObject.java +++ b/Parse/src/main/java/com/parse/ParseObject.java @@ -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; @@ -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; @@ -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, String> classNames = - new ConcurrentHashMap<>(); - private static final Map> objectTypes = - new ConcurrentHashMap<>(); - private static ParseObjectController getObjectController() { return ParseCorePlugins.getInstance().getObjectController(); } @@ -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) { @@ -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<>(); @@ -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); } /** @@ -434,7 +412,7 @@ public static ParseObject create(String className) { */ @SuppressWarnings("unchecked") public static T create(Class subclass) { - return (T) create(getClassName(subclass)); + return (T) create(getSubclassingController().getClassName(subclass)); } /** @@ -497,13 +475,7 @@ public static ParseObject createWithoutData(String className, String objectId) { */ @SuppressWarnings({"unused", "unchecked"}) public static T createWithoutData(Class 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); } /** @@ -515,41 +487,11 @@ private static boolean isAccessible(Member m) { * The subclass type to register. */ public static void registerSubclass(Class 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 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 subclass) { - unregisterSubclass(getClassName(subclass)); - } - - /* package for tests */ static void unregisterSubclass(String className) { - objectTypes.remove(className); + getSubclassingController().unregisterSubclass(subclass); } /** @@ -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 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. */ diff --git a/Parse/src/main/java/com/parse/ParseObjectSubclassingController.java b/Parse/src/main/java/com/parse/ParseObjectSubclassingController.java new file mode 100644 index 000000000..a5bd92f3d --- /dev/null +++ b/Parse/src/main/java/com/parse/ParseObjectSubclassingController.java @@ -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> registeredSubclasses = new HashMap<>(); + + /* package */ String getClassName(Class 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 clazz) { + Constructor constructor = null; + + synchronized (mutex) { + constructor = registeredSubclasses.get(className); + } + + return constructor == null + ? clazz == ParseObject.class + : constructor.getDeclaringClass() == clazz; + } + + /* package */ void registerSubclass(Class clazz) { + if (!ParseObject.class.isAssignableFrom(clazz)) { + throw new IllegalArgumentException("Cannot register a type that is not a subclass of ParseObject"); + } + + String className = getClassName(clazz); + Constructor previousConstructor = null; + + synchronized (mutex) { + previousConstructor = registeredSubclasses.get(className); + if (previousConstructor != null) { + Class previousClass = previousConstructor.getDeclaringClass(); + if (clazz.isAssignableFrom(previousClass)) { + // Previous subclass is more specific or equal to the current type, do nothing. + return; + } 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 clazz) { + String className = getClassName(clazz); + + synchronized (mutex) { + registeredSubclasses.remove(className); + } + } + + /* package */ ParseObject newInstance(String className) { + Constructor 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 getConstructor(Class clazz) throws NoSuchMethodException, IllegalAccessException { + Constructor 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(); + } +} diff --git a/Parse/src/main/java/com/parse/ParsePush.java b/Parse/src/main/java/com/parse/ParsePush.java index 73cfd0f45..1bf4421f4 100644 --- a/Parse/src/main/java/com/parse/ParsePush.java +++ b/Parse/src/main/java/com/parse/ParsePush.java @@ -37,6 +37,10 @@ public class ParsePush { return ParseCorePlugins.getInstance().getPushChannelsController(); } + private static ParseObjectSubclassingController getSubclassingController() { + return ParseCorePlugins.getInstance().getSubclassingController(); + } + private static void checkArgument(boolean expression, Object errorMessage) { if (!expression) { throw new IllegalArgumentException(String.valueOf(errorMessage)); @@ -126,7 +130,8 @@ public Builder query(ParseQuery query) { checkArgument(pushToIOS == null && pushToAndroid == null, "Cannot set push targets " + "(i.e. setPushToAndroid or setPushToIOS) when pushing to a query"); checkArgument( - query.getClassName().equals(ParseObject.getClassName(ParseInstallation.class)), + query.getClassName().equals( + getSubclassingController().getClassName(ParseInstallation.class)), "Can only push to a query for Installations"); channelSet = null; this.query = query; diff --git a/Parse/src/main/java/com/parse/ParseQuery.java b/Parse/src/main/java/com/parse/ParseQuery.java index e1537bb1f..5231e9c22 100644 --- a/Parse/src/main/java/com/parse/ParseQuery.java +++ b/Parse/src/main/java/com/parse/ParseQuery.java @@ -92,6 +92,10 @@ private static ParseQueryController getQueryController() { return ParseCorePlugins.getInstance().getQueryController(); } + private static ParseObjectSubclassingController getSubclassingController() { + return ParseCorePlugins.getInstance().getSubclassingController(); + } + /** * Constraints for a {@code ParseQuery}'s where clause. A map of field names to constraints. The * values can either be actual values to compare with for equality, or instances of @@ -361,7 +365,7 @@ public Builder(String className) { } public Builder(Class subclass) { - this(ParseObject.getClassName(subclass)); + this(getSubclassingController().getClassName(subclass)); } public Builder(State state) { @@ -896,7 +900,7 @@ public String toString() { * The {@link ParseObject} subclass type to retrieve. */ public ParseQuery(Class subclass) { - this(ParseObject.getClassName(subclass)); + this(getSubclassingController().getClassName(subclass)); } /** diff --git a/Parse/src/test/java/com/parse/ParseRoleTest.java b/Parse/src/test/java/com/parse/ParseRoleTest.java index 2c82bcf24..53d70e6d0 100644 --- a/Parse/src/test/java/com/parse/ParseRoleTest.java +++ b/Parse/src/test/java/com/parse/ParseRoleTest.java @@ -8,6 +8,8 @@ */ package com.parse; +import com.parse.ParseCorePlugins; + import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -153,9 +155,9 @@ public void testPutFailureWithInvalidNameValueSet() { @Test public void testGetQuery() { - ParseQuery query = ParseRole.getQuery(); + ParseQuery query = ParseRole.getQuery(); - assertEquals(ParseObject.getClassName(ParseRole.class), query.getBuilder().getClassName()); + assertEquals(ParseCorePlugins.getInstance().getSubclassingController().getClassName(ParseRole.class), query.getBuilder().getClassName()); } //endregion diff --git a/Parse/src/test/java/com/parse/SubclassTest.java b/Parse/src/test/java/com/parse/SubclassTest.java index 6f2cedf08..236404413 100644 --- a/Parse/src/test/java/com/parse/SubclassTest.java +++ b/Parse/src/test/java/com/parse/SubclassTest.java @@ -12,6 +12,8 @@ import org.junit.Before; import org.junit.Test; +import java.lang.IllegalArgumentException; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -85,8 +87,8 @@ public void setUp() throws Exception { @After public void tearDown() throws Exception { ParseObject.unregisterParseSubclasses(); - ParseObject.unregisterSubclass("Person"); - ParseObject.unregisterSubclass("ClassWithDirtyingConstructor"); + ParseObject.unregisterSubclass(Person.class); + ParseObject.unregisterSubclass(ClassWithDirtyingConstructor.class); } @SuppressWarnings("unused") @@ -102,7 +104,7 @@ public void testUnregisteredConstruction() throws Exception { try { new UnregisteredClass(); } finally { - ParseObject.unregisterSubclass("UnregisteredClass"); + ParseObject.unregisterSubclass(UnregisteredClass.class); } } @@ -137,10 +139,18 @@ public void testRegisteringSubclassesUsesMostDescendantSubclass() throws Excepti assertEquals(MyUser.class, ParseObject.create("_User").getClass()); ParseObject.registerSubclass(ParseUser.class); assertEquals(MyUser.class, ParseObject.create("_User").getClass()); - ParseObject.registerSubclass(MyUser2.class); - assertEquals(MyUser2.class, ParseObject.create("_User").getClass()); + + // This is expected to fail as MyUser2 and MyUser are not directly related. + try { + ParseObject.registerSubclass(MyUser2.class); + fail(); + } catch (IllegalArgumentException ex) { + /* expected */ + } + + assertEquals(MyUser.class, ParseObject.create("_User").getClass()); } finally { - ParseObject.unregisterSubclass("_User"); + ParseObject.unregisterSubclass(ParseUser.class); ParseCorePlugins.getInstance().reset(); } }