Skip to content

Commit e6227a6

Browse files
Merge pull request #245 from ParsePlatform/richardross.subclassing.decouple
Decouple object subclassing.
2 parents 2dd0095 + d4d9305 commit e6227a6

File tree

9 files changed

+199
-102
lines changed

9 files changed

+199
-102
lines changed

Parse/src/main/java/com/parse/FileObjectStore.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919

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

22+
private static ParseObjectSubclassingController getSubclassingController() {
23+
return ParseCorePlugins.getInstance().getSubclassingController();
24+
}
25+
2226
/**
2327
* Saves the {@code ParseObject} to the a file on disk as JSON in /2/ format.
2428
*
@@ -75,7 +79,7 @@ private static <T extends ParseObject> T getFromDisk(
7579
private final ParseObjectCurrentCoder coder;
7680

7781
public FileObjectStore(Class<T> clazz, File file, ParseObjectCurrentCoder coder) {
78-
this(ParseObject.getClassName(clazz), file, coder);
82+
this(getSubclassingController().getClassName(clazz), file, coder);
7983
}
8084

8185
public FileObjectStore(String className, File file, ParseObjectCurrentCoder coder) {

Parse/src/main/java/com/parse/OfflineObjectStore.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616

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

19+
private static ParseObjectSubclassingController getSubclassingController() {
20+
return ParseCorePlugins.getInstance().getSubclassingController();
21+
}
22+
1923
private static <T extends ParseObject> Task<T> migrate(
2024
final ParseObjectStore<T> from, final ParseObjectStore<T> to) {
2125
return from.getAsync().onSuccessTask(new Continuation<T, Task<T>>() {
@@ -44,7 +48,7 @@ public T then(Task<Void> task) throws Exception {
4448
private final ParseObjectStore<T> legacy;
4549

4650
public OfflineObjectStore(Class<T> clazz, String pinName, ParseObjectStore<T> legacy) {
47-
this(ParseObject.getClassName(clazz), pinName, legacy);
51+
this(getSubclassingController().getClassName(clazz), pinName, legacy);
4852
}
4953

5054
public OfflineObjectStore(String className, String pinName, ParseObjectStore<T> legacy) {

Parse/src/main/java/com/parse/ParseCorePlugins.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ public static ParseCorePlugins getInstance() {
4949
private AtomicReference<ParseDefaultACLController> defaultACLController = new AtomicReference<>();
5050

5151
private AtomicReference<LocalIdManager> localIdManager = new AtomicReference<>();
52+
private AtomicReference<ParseObjectSubclassingController> subclassingController = new AtomicReference<>();
5253

5354
private ParseCorePlugins() {
5455
// do nothing
@@ -336,5 +337,20 @@ public void registerLocalIdManager(LocalIdManager manager) {
336337
"Another localId manager was already registered: " + localIdManager.get());
337338
}
338339
}
340+
341+
public ParseObjectSubclassingController getSubclassingController() {
342+
if (subclassingController.get() == null) {
343+
ParseObjectSubclassingController controller = new ParseObjectSubclassingController();
344+
subclassingController.compareAndSet(null, controller);
345+
}
346+
return subclassingController.get();
347+
}
348+
349+
public void registerSubclassingController(ParseObjectSubclassingController controller) {
350+
if (!subclassingController.compareAndSet(null, controller)) {
351+
throw new IllegalStateException(
352+
"Another subclassing controller was already registered: " + subclassingController.get());
353+
}
354+
}
339355
}
340356

Parse/src/main/java/com/parse/ParseObject.java

Lines changed: 11 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@
1212
import org.json.JSONException;
1313
import org.json.JSONObject;
1414

15-
import java.lang.reflect.Member;
16-
import java.lang.reflect.Modifier;
1715
import java.util.ArrayList;
1816
import java.util.Arrays;
1917
import java.util.Collection;
@@ -29,7 +27,6 @@
2927
import java.util.Map;
3028
import java.util.Set;
3129
import java.util.concurrent.Callable;
32-
import java.util.concurrent.ConcurrentHashMap;
3330
import java.util.concurrent.atomic.AtomicBoolean;
3431
import java.util.concurrent.locks.Lock;
3532

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

75-
private static final Map<Class<? extends ParseObject>, String> classNames =
76-
new ConcurrentHashMap<>();
77-
private static final Map<String, Class<? extends ParseObject>> objectTypes =
78-
new ConcurrentHashMap<>();
79-
8072
private static ParseObjectController getObjectController() {
8173
return ParseCorePlugins.getInstance().getObjectController();
8274
}
@@ -85,6 +77,10 @@ private static LocalIdManager getLocalIdManager() {
8577
return ParseCorePlugins.getInstance().getLocalIdManager();
8678
}
8779

80+
private static ParseObjectSubclassingController getSubclassingController() {
81+
return ParseCorePlugins.getInstance().getSubclassingController();
82+
}
83+
8884
/** package */ static class State {
8985

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

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

373-
// If this is an unregistered subclass, throw an exception.
374-
if (!this.getClass().equals(ParseObject.class)
375-
&& !this.getClass().equals(objectTypes.get(theClassName))) {
376-
throw new IllegalArgumentException(
377-
"You must register this ParseObject subclass before instantiating it.");
378-
}
379-
380368
operationSetQueue = new LinkedList<>();
381369
operationSetQueue.add(new ParseOperationSet());
382370
estimatedData = new HashMap<>();
@@ -410,17 +398,7 @@ public ParseObject(String theClassName) {
410398
* @return A new {@code ParseObject} for the given class name.
411399
*/
412400
public static ParseObject create(String className) {
413-
if (objectTypes.containsKey(className)) {
414-
try {
415-
return objectTypes.get(className).newInstance();
416-
} catch (Exception e) {
417-
if (e instanceof RuntimeException) {
418-
throw (RuntimeException) e;
419-
}
420-
throw new RuntimeException("Failed to create instance of subclass.", e);
421-
}
422-
}
423-
return new ParseObject(className);
401+
return getSubclassingController().newInstance(className);
424402
}
425403

426404
/**
@@ -434,7 +412,7 @@ public static ParseObject create(String className) {
434412
*/
435413
@SuppressWarnings("unchecked")
436414
public static <T extends ParseObject> T create(Class<T> subclass) {
437-
return (T) create(getClassName(subclass));
415+
return (T) create(getSubclassingController().getClassName(subclass));
438416
}
439417

440418
/**
@@ -497,13 +475,7 @@ public static ParseObject createWithoutData(String className, String objectId) {
497475
*/
498476
@SuppressWarnings({"unused", "unchecked"})
499477
public static <T extends ParseObject> T createWithoutData(Class<T> subclass, String objectId) {
500-
return (T) createWithoutData(getClassName(subclass), objectId);
501-
}
502-
503-
private static boolean isAccessible(Member m) {
504-
return Modifier.isPublic(m.getModifiers())
505-
|| (m.getDeclaringClass().getPackage().getName().equals("com.parse")
506-
&& !Modifier.isPrivate(m.getModifiers()) && !Modifier.isProtected(m.getModifiers()));
478+
return (T) createWithoutData(getSubclassingController().getClassName(subclass), objectId);
507479
}
508480

509481
/**
@@ -515,41 +487,11 @@ private static boolean isAccessible(Member m) {
515487
* The subclass type to register.
516488
*/
517489
public static void registerSubclass(Class<? extends ParseObject> subclass) {
518-
String className = getClassName(subclass);
519-
if (className == null) {
520-
throw new IllegalArgumentException("No ParseClassName annotation provided on " + subclass);
521-
}
522-
if (subclass.getDeclaredConstructors().length > 0) {
523-
try {
524-
if (!isAccessible(subclass.getDeclaredConstructor())) {
525-
throw new IllegalArgumentException("Default constructor for " + subclass
526-
+ " is not accessible.");
527-
}
528-
} catch (NoSuchMethodException e) {
529-
throw new IllegalArgumentException("No default constructor provided for " + subclass);
530-
}
531-
}
532-
Class<? extends ParseObject> oldValue = objectTypes.get(className);
533-
if (oldValue != null && subclass.isAssignableFrom(oldValue)) {
534-
// The old class was already more descendant than the new subclass type. No-op.
535-
return;
536-
}
537-
objectTypes.put(className, subclass);
538-
if (oldValue != null && !subclass.equals(oldValue)) {
539-
if (className.equals(getClassName(ParseUser.class))) {
540-
ParseUser.getCurrentUserController().clearFromMemory();
541-
} else if (className.equals(getClassName(ParseInstallation.class))) {
542-
ParseInstallation.getCurrentInstallationController().clearFromMemory();
543-
}
544-
}
490+
getSubclassingController().registerSubclass(subclass);
545491
}
546492

547493
/* package for tests */ static void unregisterSubclass(Class<? extends ParseObject> subclass) {
548-
unregisterSubclass(getClassName(subclass));
549-
}
550-
551-
/* package for tests */ static void unregisterSubclass(String className) {
552-
objectTypes.remove(className);
494+
getSubclassingController().unregisterSubclass(subclass);
553495
}
554496

555497
/**
@@ -3516,26 +3458,6 @@ public boolean hasSameId(ParseObject other) {
35163458
}
35173459
}
35183460

3519-
/**
3520-
* Gets the class name based on the {@link ParseClassName} annotation associated with a class.
3521-
*
3522-
* @param clazz
3523-
* The class to inspect.
3524-
* @return The name of the Parse class, if one is provided. Otherwise, {@code null}.
3525-
*/
3526-
static String getClassName(Class<? extends ParseObject> clazz) {
3527-
String name = classNames.get(clazz);
3528-
if (name == null) {
3529-
ParseClassName info = clazz.getAnnotation(ParseClassName.class);
3530-
if (info == null) {
3531-
return null;
3532-
}
3533-
name = info.value();
3534-
classNames.put(clazz, name);
3535-
}
3536-
return name;
3537-
}
3538-
35393461
/**
35403462
* Called when a non-pointer is being created to allow additional initialization to occur.
35413463
*/
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/*
2+
* Copyright (c) 2015-present, Parse, LLC.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*/
9+
package com.parse;
10+
11+
import java.lang.reflect.Constructor;
12+
import java.lang.reflect.Modifier;
13+
import java.util.HashMap;
14+
import java.util.Map;
15+
16+
/* package */ class ParseObjectSubclassingController {
17+
private final Object mutex = new Object();
18+
private final Map<String, Constructor<? extends ParseObject>> registeredSubclasses = new HashMap<>();
19+
20+
/* package */ String getClassName(Class<? extends ParseObject> clazz) {
21+
ParseClassName info = clazz.getAnnotation(ParseClassName.class);
22+
if (info == null) {
23+
throw new IllegalArgumentException("No ParseClassName annotation provided on " + clazz);
24+
}
25+
return info.value();
26+
}
27+
28+
/* package */ boolean isSubclassValid(String className, Class<? extends ParseObject> clazz) {
29+
Constructor<? extends ParseObject> constructor = null;
30+
31+
synchronized (mutex) {
32+
constructor = registeredSubclasses.get(className);
33+
}
34+
35+
return constructor == null
36+
? clazz == ParseObject.class
37+
: constructor.getDeclaringClass() == clazz;
38+
}
39+
40+
/* package */ void registerSubclass(Class<? extends ParseObject> clazz) {
41+
if (!ParseObject.class.isAssignableFrom(clazz)) {
42+
throw new IllegalArgumentException("Cannot register a type that is not a subclass of ParseObject");
43+
}
44+
45+
String className = getClassName(clazz);
46+
Constructor<? extends ParseObject> previousConstructor = null;
47+
48+
synchronized (mutex) {
49+
previousConstructor = registeredSubclasses.get(className);
50+
if (previousConstructor != null) {
51+
Class<? extends ParseObject> previousClass = previousConstructor.getDeclaringClass();
52+
if (clazz.isAssignableFrom(previousClass)) {
53+
// Previous subclass is more specific or equal to the current type, do nothing.
54+
return;
55+
} else if (previousClass.isAssignableFrom(clazz)) {
56+
// Previous subclass is parent of new child subclass, fallthrough and actually
57+
// register this class.
58+
/* Do nothing */
59+
} else {
60+
throw new IllegalArgumentException(
61+
"Tried to register both " + previousClass.getName() + " and " + clazz.getName() +
62+
" as the ParseObject subclass of " + className + ". " + "Cannot determine the right " +
63+
"class to use because neither inherits from the other."
64+
);
65+
}
66+
}
67+
68+
try {
69+
registeredSubclasses.put(className, getConstructor(clazz));
70+
} catch (NoSuchMethodException ex) {
71+
throw new IllegalArgumentException(
72+
"Cannot register a type that does not implement the default constructor!"
73+
);
74+
} catch (IllegalAccessException ex) {
75+
throw new IllegalArgumentException(
76+
"Default constructor for " + clazz + " is not accessible."
77+
);
78+
}
79+
}
80+
81+
if (previousConstructor != null) {
82+
// TODO: This is super tightly coupled. Let's remove it when automatic registration is in.
83+
// NOTE: Perform this outside of the mutex, to prevent any potential deadlocks.
84+
if (className.equals(getClassName(ParseUser.class))) {
85+
ParseUser.getCurrentUserController().clearFromMemory();
86+
} else if (className.equals(getClassName(ParseInstallation.class))) {
87+
ParseInstallation.getCurrentInstallationController().clearFromMemory();
88+
}
89+
}
90+
}
91+
92+
/* package */ void unregisterSubclass(Class<? extends ParseObject> clazz) {
93+
String className = getClassName(clazz);
94+
95+
synchronized (mutex) {
96+
registeredSubclasses.remove(className);
97+
}
98+
}
99+
100+
/* package */ ParseObject newInstance(String className) {
101+
Constructor<? extends ParseObject> constructor = null;
102+
103+
synchronized (mutex) {
104+
constructor = registeredSubclasses.get(className);
105+
}
106+
107+
try {
108+
return constructor != null
109+
? constructor.newInstance()
110+
: new ParseObject(className);
111+
} catch (RuntimeException e) {
112+
throw e;
113+
} catch (Exception e) {
114+
throw new RuntimeException("Failed to create instance of subclass.", e);
115+
}
116+
}
117+
118+
private static Constructor<? extends ParseObject> getConstructor(Class<? extends ParseObject> clazz) throws NoSuchMethodException, IllegalAccessException {
119+
Constructor<? extends ParseObject> constructor = clazz.getDeclaredConstructor();
120+
if (constructor == null) {
121+
throw new NoSuchMethodException();
122+
}
123+
int modifiers = constructor.getModifiers();
124+
if (Modifier.isPublic(modifiers) || (clazz.getPackage().getName().equals("com.parse") &&
125+
!(Modifier.isProtected(modifiers) || Modifier.isPrivate(modifiers)))) {
126+
return constructor;
127+
}
128+
throw new IllegalAccessException();
129+
}
130+
}

0 commit comments

Comments
 (0)