Skip to content

ParseACL implementation / Documentation Review #160

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Apr 22, 2019
Merged
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
347 changes: 230 additions & 117 deletions README.md
Original file line number Diff line number Diff line change
@@ -46,7 +46,27 @@ You can create custom objects by calling:
var dietPlan = ParseObject('DietPlan')
..set('Name', 'Ketogenic')
..set('Fat', 65);
await dietPlan.save()
```
Verify that the object has been successfully saved using
```dart
var response = await dietPlan.save();
if (response.success) {
dietPlan = response.result;
}
```
Types supported:
* String
* Double
* Int
* Boolean
* DateTime
* File
* Geopoint
* ParseObject/ParseUser (Pointer)
* Map
* List (all types supported)

You then have the ability to do the following with that object:
The features available are:-
* Get
@@ -110,6 +130,14 @@ Retrieve it, call
```dart
var response = await dietPlan.increment("count", 1);
```
or using with save function

```dart
dietPlan.setIncrement('count', 1);
dietPlan.setDecrement('count', 1);
var response = dietPlan.save()
```

## Array Operator in objects
@@ -122,48 +150,56 @@ var response = await dietPlan.addUnique("listKeywords", ["a", "a","d"]);
var response = await dietPlan.remove("listKeywords", ["a"]);
```
or using with save function

```dart
dietPlan.setAdd('listKeywords', ['a','a','d']);
dietPlan.setAddUnique('listKeywords', ['a','a','d']);
dietPlan.setRemove('listKeywords', ['a']);
var response = dietPlan.save()
```

## Queries
Once you have setup the project and initialised the instance, you can then retreive data from your server by calling:
```dart
var apiResponse = await ParseObject('ParseTableName').getAll();
if (apiResponse.success){
for (var testObject in apiResponse.result) {
print(ApplicationConstants.APP_NAME + ": " + testObject.toString());
}
}
if (apiResponse.success){
for (var testObject in apiResponse.result) {
print(ApplicationConstants.APP_NAME + ": " + testObject.toString());
}
}
```
Or you can get an object by its objectId:

```dart
var dietPlan = await DietPlan().getObject('R5EonpUDWy');
if (dietPlan.success) {
print(ApplicationConstants.keyAppName + ": " + (dietPlan.result as DietPlan).toString());
} else {
print(ApplicationConstants.keyAppName + ": " + dietPlan.exception.message);
}
if (dietPlan.success) {
print(ApplicationConstants.keyAppName + ": " + (dietPlan.result as DietPlan).toString());
} else {
print(ApplicationConstants.keyAppName + ": " + dietPlan.exception.message);
}
```

## Complex queries
You can create complex queries to really put your database to the test:

```dart
var queryBuilder = QueryBuilder<DietPlan>(DietPlan())
..startsWith(DietPlan.keyName, "Keto")
..greaterThan(DietPlan.keyFat, 64)
..lessThan(DietPlan.keyFat, 66)
..equals(DietPlan.keyCarbs, 5);
var response = await queryBuilder.query();
if (response.success) {
print(ApplicationConstants.keyAppName + ": " + ((response.result as List<dynamic>).first as DietPlan).toString());
} else {
print(ApplicationConstants.keyAppName + ": " + response.exception.message);
}
var queryBuilder = QueryBuilder<DietPlan>(DietPlan())
..startsWith(DietPlan.keyName, "Keto")
..greaterThan(DietPlan.keyFat, 64)
..lessThan(DietPlan.keyFat, 66)
..equals(DietPlan.keyCarbs, 5);
var response = await queryBuilder.query();
if (response.success) {
print(ApplicationConstants.keyAppName + ": " + ((response.result as List<dynamic>).first as DietPlan).toString());
} else {
print(ApplicationConstants.keyAppName + ": " + response.exception.message);
}
```

The features available are:-
@@ -197,15 +233,15 @@ For example, imagine you have Post class and a Comment class, where each Comment
You can find comments on posts with images by doing:

```dart
QueryBuilder<ParseObject> queryPost =
QueryBuilder<ParseObject>(ParseObject('Post'))
..whereValueExists('image', true);
QueryBuilder<ParseObject> queryPost =
QueryBuilder<ParseObject>(ParseObject('Post'))
..whereValueExists('image', true);
QueryBuilder<ParseObject> queryComment =
QueryBuilder<ParseObject>(ParseObject('Comment'))
..whereMatchesQuery('post', queryPost);
QueryBuilder<ParseObject> queryComment =
QueryBuilder<ParseObject>(ParseObject('Comment'))
..whereMatchesQuery('post', queryPost);
var apiResponse = await queryComment.query();
var apiResponse = await queryComment.query();
```

If you want to retrieve objects where a field contains an object that does not match another query, you can use the
@@ -214,28 +250,28 @@ Imagine you have Post class and a Comment class, where each Comment has a pointe
You can find comments on posts without images by doing:

```dart
QueryBuilder<ParseObject> queryPost =
QueryBuilder<ParseObject>(ParseObject('Post'))
..whereValueExists('image', true);
QueryBuilder<ParseObject> queryPost =
QueryBuilder<ParseObject>(ParseObject('Post'))
..whereValueExists('image', true);
QueryBuilder<ParseObject> queryComment =
QueryBuilder<ParseObject>(ParseObject('Comment'))
..whereDoesNotMatchQuery('post', queryPost);
QueryBuilder<ParseObject> queryComment =
QueryBuilder<ParseObject>(ParseObject('Comment'))
..whereDoesNotMatchQuery('post', queryPost);
var apiResponse = await queryComment.query();
var apiResponse = await queryComment.query();
```

## Counting Objects
If you only care about the number of games played by a particular player:

```dart
QueryBuilder<ParseObject> queryPlayers =
QueryBuilder<ParseObject>(ParseObject('GameScore'))
..whereEqualTo('playerName', 'Jonathan Walsh');
var apiResponse = await queryPlayers.count();
if (apiResponse.success && apiResponse.result != null) {
int countGames = apiResponse.count;
}
QueryBuilder<ParseObject> queryPlayers =
QueryBuilder<ParseObject>(ParseObject('GameScore'))
..whereEqualTo('playerName', 'Jonathan Walsh');
var apiResponse = await queryPlayers.count();
if (apiResponse.success && apiResponse.result != null) {
int countGames = apiResponse.count;
}
```

## Live Queries
@@ -249,32 +285,32 @@ The Parse Server configuration guide on the server is found here https://docs.pa

Initialize the Parse Live Query by entering the parameter liveQueryUrl in Parse().initialize:
```dart
Parse().initialize(
ApplicationConstants.keyApplicationId,
ApplicationConstants.keyParseServerUrl,
clientKey: ApplicationConstants.keyParseClientKey,
debug: true,
liveQueryUrl: ApplicationConstants.keyLiveQueryUrl,
autoSendSessionId: true);
Parse().initialize(
ApplicationConstants.keyApplicationId,
ApplicationConstants.keyParseServerUrl,
clientKey: ApplicationConstants.keyParseClientKey,
debug: true,
liveQueryUrl: ApplicationConstants.keyLiveQueryUrl,
autoSendSessionId: true);
```

Declare LiveQuery:
```dart
final LiveQuery liveQuery = LiveQuery();
final LiveQuery liveQuery = LiveQuery();
```

Set the QueryBuilder that will be monitored by LiveQuery:
```dart
QueryBuilder<ParseObject> query =
QueryBuilder<ParseObject>(ParseObject('TestAPI'))
..whereEqualTo('intNumber', 1);
QueryBuilder<ParseObject> query =
QueryBuilder<ParseObject>(ParseObject('TestAPI'))
..whereEqualTo('intNumber', 1);
```
__Create a subscription__
You’ll get the LiveQuery events through this subscription.
The first time you call subscribe, we’ll try to open the WebSocket connection to the LiveQuery server for you.

```dart
await liveQuery.subscribe(query);
await liveQuery.subscribe(query);
```

__Event Handling__
@@ -284,78 +320,78 @@ __Create event__
When a new ParseObject is created and it fulfills the QueryBuilder you subscribe, you’ll get this event.
The object is the ParseObject which was created.
```dart
liveQuery.on(LiveQueryEvent.create, (value) {
print('*** CREATE ***: ${DateTime.now().toString()}\n $value ');
print((value as ParseObject).objectId);
print((value as ParseObject).updatedAt);
print((value as ParseObject).createdAt);
print((value as ParseObject).get('objectId'));
print((value as ParseObject).get('updatedAt'));
print((value as ParseObject).get('createdAt'));
});
liveQuery.on(LiveQueryEvent.create, (value) {
print('*** CREATE ***: ${DateTime.now().toString()}\n $value ');
print((value as ParseObject).objectId);
print((value as ParseObject).updatedAt);
print((value as ParseObject).createdAt);
print((value as ParseObject).get('objectId'));
print((value as ParseObject).get('updatedAt'));
print((value as ParseObject).get('createdAt'));
});
```

__Update event__
When an existing ParseObject which fulfills the QueryBuilder you subscribe is updated (The ParseObject fulfills the
QueryBuilder before and after changes), you’ll get this event.
The object is the ParseObject which was updated. Its content is the latest value of the ParseObject.
```dart
liveQuery.on(LiveQueryEvent.update, (value) {
print('*** UPDATE ***: ${DateTime.now().toString()}\n $value ');
print((value as ParseObject).objectId);
print((value as ParseObject).updatedAt);
print((value as ParseObject).createdAt);
print((value as ParseObject).get('objectId'));
print((value as ParseObject).get('updatedAt'));
print((value as ParseObject).get('createdAt'));
});
liveQuery.on(LiveQueryEvent.update, (value) {
print('*** UPDATE ***: ${DateTime.now().toString()}\n $value ');
print((value as ParseObject).objectId);
print((value as ParseObject).updatedAt);
print((value as ParseObject).createdAt);
print((value as ParseObject).get('objectId'));
print((value as ParseObject).get('updatedAt'));
print((value as ParseObject).get('createdAt'));
});
```

__Enter event__
When an existing ParseObject’s old value does not fulfill the QueryBuilder but its new value fulfills the QueryBuilder,
you’ll get this event. The object is the ParseObject which enters the QueryBuilder.
Its content is the latest value of the ParseObject.
```dart
liveQuery.on(LiveQueryEvent.enter, (value) {
print('*** ENTER ***: ${DateTime.now().toString()}\n $value ');
print((value as ParseObject).objectId);
print((value as ParseObject).updatedAt);
print((value as ParseObject).createdAt);
print((value as ParseObject).get('objectId'));
print((value as ParseObject).get('updatedAt'));
print((value as ParseObject).get('createdAt'));
});
liveQuery.on(LiveQueryEvent.enter, (value) {
print('*** ENTER ***: ${DateTime.now().toString()}\n $value ');
print((value as ParseObject).objectId);
print((value as ParseObject).updatedAt);
print((value as ParseObject).createdAt);
print((value as ParseObject).get('objectId'));
print((value as ParseObject).get('updatedAt'));
print((value as ParseObject).get('createdAt'));
});
```

__Leave event__
When an existing ParseObject’s old value fulfills the QueryBuilder but its new value doesn’t fulfill the QueryBuilder,
you’ll get this event. The object is the ParseObject which leaves the QueryBuilder.
Its content is the latest value of the ParseObject.
```dart
liveQuery.on(LiveQueryEvent.leave, (value) {
print('*** LEAVE ***: ${DateTime.now().toString()}\n $value ');
print((value as ParseObject).objectId);
print((value as ParseObject).updatedAt);
print((value as ParseObject).createdAt);
print((value as ParseObject).get('objectId'));
print((value as ParseObject).get('updatedAt'));
print((value as ParseObject).get('createdAt'));
});
liveQuery.on(LiveQueryEvent.leave, (value) {
print('*** LEAVE ***: ${DateTime.now().toString()}\n $value ');
print((value as ParseObject).objectId);
print((value as ParseObject).updatedAt);
print((value as ParseObject).createdAt);
print((value as ParseObject).get('objectId'));
print((value as ParseObject).get('updatedAt'));
print((value as ParseObject).get('createdAt'));
});
```

__Delete event__
When an existing ParseObject which fulfills the QueryBuilder is deleted, you’ll get this event.
The object is the ParseObject which is deleted
```dart
liveQuery.on(LiveQueryEvent.delete, (value) {
print('*** DELETE ***: ${DateTime.now().toString()}\n $value ');
print((value as ParseObject).objectId);
print((value as ParseObject).updatedAt);
print((value as ParseObject).createdAt);
print((value as ParseObject).get('objectId'));
print((value as ParseObject).get('updatedAt'));
print((value as ParseObject).get('createdAt'));
});
liveQuery.on(LiveQueryEvent.delete, (value) {
print('*** DELETE ***: ${DateTime.now().toString()}\n $value ');
print((value as ParseObject).objectId);
print((value as ParseObject).updatedAt);
print((value as ParseObject).createdAt);
print((value as ParseObject).get('objectId'));
print((value as ParseObject).get('updatedAt'));
print((value as ParseObject).get('createdAt'));
});
```

__Unsubscribe__
@@ -364,7 +400,7 @@ After that, you won’t get any events from the subscription object and will clo
LiveQuery server.

```dart
await liveQuery.unSubscribe();
await liveQuery.unSubscribe();
```

## Users
@@ -380,11 +416,18 @@ Then have the user sign up:
var response = await user.signUp();
if (response.success) user = response.result;
```
You can also logout and login with the user:
You can also login with the user:
```dart
var response = await user.login();
if (response.success) user = response.result;
```
You can also logout with the user:
```dart
var response = await user.logout();
if (response.success) {
print('User logout');
}
```
Also, once logged in you can manage sessions tokens. This feature can be called after Parse().init() on startup to check for a logged in user.
```dart
user = ParseUser.currentUser();
@@ -397,8 +440,64 @@ Other user features are:-
* Destroy user
* Queries

## Security for Objects - ParseACL
For any object, you can specify which users are allowed to read the object, and which users are allowed to modify an object.
To support this type of security, each object has an access control list, implemented by the __ParseACL__ class.

If ParseACL is not specified (with the exception of the ParseUser class) all objects are set to Public for read and write.
The simplest way to use a ParseACL is to specify that an object may only be read or written by a single user.
To create such an object, there must first be a logged in ParseUser. Then, new ParseACL(user) generates a ParseACL that
limits access to that user. An object’s ACL is updated when the object is saved, like any other property.

```dart
ParseUser user = await ParseUser.currentUser() as ParseUser;
ParseACL parseACL = ParseACL(owner: user);
ParseObject parseObject = ParseObject("TestAPI");
...
parseObject.setACL(parseACL);
var apiResponse = await parseObject.save();
```
Permissions can also be granted on a per-user basis. You can add permissions individually to a ParseACL using
__setReadAccess__ and __setWriteAccess__
```dart
ParseUser user = await ParseUser.currentUser() as ParseUser;
ParseACL parseACL = ParseACL();
//grant total access to current user
parseACL.setReadAccess(userId: user.objectId, allowed: true);
parseACL.setWriteAccess(userId: user.objectId, allowed: true);
//grant read access to userId: 'TjRuDjuSAO'
parseACL.setReadAccess(userId: 'TjRuDjuSAO', allowed: true);
parseACL.setWriteAccess(userId: 'TjRuDjuSAO', allowed: false);
ParseObject parseObject = ParseObject("TestAPI");
...
parseObject.setACL(parseACL);
var apiResponse = await parseObject.save();
```
You can also grant permissions to all users at once using setPublicReadAccess and setPublicWriteAccess.
```dart
ParseACL parseACL = ParseACL();
parseACL.setPublicReadAccess(allowed: true);
parseACL.setPublicWriteAccess(allowed: true);
ParseObject parseObject = ParseObject("TestAPI");
...
parseObject.setACL(parseACL);
var apiResponse = await parseObject.save();
```
Operations that are forbidden, such as deleting an object that you do not have write access to, result in a
ParseError with code 101: 'ObjectNotFound'.
For security purposes, this prevents clients from distinguishing which object ids exist but are secured, versus which
object ids do not exist at all.

You can retrieve the ACL list of an object using:
```dart
ParseACL parseACL = parseObject.getACL();
```

## Config
The SDK now supports Parse Config. A map of all configs can be grabbed from the server by calling :
The SDK supports Parse Config. A map of all configs can be grabbed from the server by calling :
```dart
var response = await ParseConfig().getConfigs();
```
@@ -408,27 +507,41 @@ and to add a config:
ParseConfig().addConfig('TestConfig', 'testing');
```

## Cloud Functions
The SDK supports call Cloud Functions.

Executes a cloud function that returns a ParseObject type
```dart
final ParseCloudFunction function = ParseCloudFunction('hello');
final ParseResponse result =
await function.executeObjectFunction<ParseObject>();
if (result.success) {
if (result.result is ParseObject) {
final ParseObject parseObject = result.result;
print(parseObject.className);
}
}
```

Executes a cloud function with parameters
```dart
final ParseCloudFunction function = ParseCloudFunction('hello');
final Map<String, String> params = <String, String>{'plan': 'paid'};
function.execute(parameters: params);
```

## Other Features of this library
Main:
* Installation
* GeoPoints
* Files
* Installation (View the example application)
* GeoPoints (View the example application)
* Files (View the example application)
* Persistent storage
* Debug Mode - Logging API calls
* Manage Session ID's tokens

User:
* Create
* Login
* Logout
* CurrentUser
* RequestPasswordReset
* VerificationEmailRequest
* AllUsers
* Save
* Destroy
* Queries
* Anonymous
* Anonymous (View the example application)
* 3rd Party Authentication

Objects:
2 changes: 2 additions & 0 deletions lib/parse_server_sdk.dart
Original file line number Diff line number Diff line change
@@ -62,6 +62,8 @@ part 'src/objects/parse_session.dart';

part 'src/objects/parse_user.dart';

part 'src/objects/parse_acl.dart';

part 'src/utils/parse_decoder.dart';

part 'src/utils/parse_encoder.dart';
150 changes: 150 additions & 0 deletions lib/src/objects/parse_acl.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
part of flutter_parse_sdk;

/// [ParseACL] is used to control which users can access or modify a particular object
/// [ParseObject] can have its own [ParceACL]
/// You can grant read and write permissions separately to specific users
/// or you can grant permissions to "the public" so that, for example, any user could read a particular object but
/// only a particular set of users could write to that object
class ParseACL {
///Creates an ACL where only the provided user has access.
///[owner] The only user that can read or write objects governed by this ACL.
ParseACL({ParseUser owner}) {
if (owner != null) {
setReadAccess(userId: owner.objectId, allowed: true);
setWriteAccess(userId: owner.objectId, allowed: true);
}
}

final String _publicKEY = '*';
final Map<String, _ACLPermissions> _permissionsById = {};

/// Helper for setting stuff
void _setPermissionsIfNonEmpty(
{@required String userId, bool readPermission, bool writePermission}) {
if (!(readPermission || writePermission)) {
_permissionsById.remove(userId);
} else {
_permissionsById[userId] =
_ACLPermissions(readPermission, writePermission);
}
}

///Get whether the public is allowed to read this object.
bool getPublicReadAccess() {
return getReadAccess(userId: _publicKEY);
}

///Set whether the public is allowed to read this object.
void setPublicReadAccess({@required bool allowed}) {
setReadAccess(userId: _publicKEY, allowed: allowed);
}

/// Set whether the public is allowed to write this object.
bool getPublicWriteAccess() {
return getWriteAccess(userId: _publicKEY);
}

///Set whether the public is allowed to write this object.
void setPublicWriteAccess({@required bool allowed}) {
setWriteAccess(userId: _publicKEY, allowed: allowed);
}

///Set whether the given user id is allowed to read this object.
void setReadAccess({@required String userId, bool allowed}) {
if (userId == null) {
throw 'cannot setReadAccess for null userId';
}
final bool writePermission = getWriteAccess(userId: userId);
_setPermissionsIfNonEmpty(
userId: userId,
readPermission: allowed,
writePermission: writePermission);
}

/// Get whether the given user id is *explicitly* allowed to read this object. Even if this returns
/// [false], the user may still be able to access it if getPublicReadAccess returns
/// [true] or a role that the user belongs to has read access.
bool getReadAccess({@required String userId}) {
if (userId == null) {
throw 'cannot getReadAccess for null userId';
}
final _ACLPermissions _permissions = _permissionsById[userId];
return _permissions != null && _permissions.getReadPermission();
}

///Set whether the given user id is allowed to write this object.
void setWriteAccess({@required String userId, bool allowed}) {
if (userId == null) {
throw 'cannot setWriteAccess for null userId';
}
final bool readPermission = getReadAccess(userId: userId);
_setPermissionsIfNonEmpty(
userId: userId,
readPermission: readPermission,
writePermission: allowed);
}

///Get whether the given user id is *explicitly* allowed to write this object. Even if this
///returns [false], the user may still be able to write it if getPublicWriteAccess returns
///[true] or a role that the user belongs to has write access.
bool getWriteAccess({@required String userId}) {
if (userId == null) {
throw 'cannot getWriteAccess for null userId';
}
final _ACLPermissions _permissions = _permissionsById[userId];
return _permissions != null && _permissions.getReadPermission();
}

Map<String, dynamic> toJson() {
final Map<String, dynamic> map = <String, dynamic>{};
_permissionsById.forEach((String user, _ACLPermissions permission) {
map[user] = permission.toJson();
});
print(map);
return map;
}

@override
String toString() => json.encode(toJson());

ParseACL fromJson(Map<String, dynamic> map) {
final ParseACL parseACL = ParseACL();

map.forEach((String userId, dynamic permission) {
if (permission['read'] != null) {
parseACL.setReadAccess(userId: userId, allowed: permission['read']);
}
if (permission['write'] != null) {
parseACL.setWriteAccess(userId: userId, allowed: permission['write']);
}
});
return parseACL;
}
}

class _ACLPermissions {
_ACLPermissions(this._readPermission, this._writePermission);
final String _keyReadPermission = 'read';
final String _keyWritePermission = 'write';
bool _readPermission = false;
bool _writePermission = false;

bool getReadPermission() {
return _readPermission;
}

bool getWritePermission() {
return _writePermission;
}

Map<String, dynamic> toJson() {
final Map<String, dynamic> map = <String, dynamic>{};
if (_readPermission) {
map[_keyReadPermission] = true;
}
if (_writePermission) {
map[_keyWritePermission] = true;
}
return map;
}
}
18 changes: 17 additions & 1 deletion lib/src/objects/parse_base.dart
Original file line number Diff line number Diff line change
@@ -65,7 +65,7 @@ abstract class ParseBase {
map.remove(keyVarCreatedAt);
map.remove(keyVarUpdatedAt);
map.remove(keyVarClassName);
map.remove(keyVarAcl);
//map.remove(keyVarAcl);
map.remove(keyParamSessionToken);
}

@@ -97,6 +97,8 @@ abstract class ParseBase {
} else {
set<DateTime>(keyVarUpdatedAt, value);
}
} else if (key == keyVarAcl) {
getObjectData()[keyVarAcl] = ParseACL().fromJson(value);
} else {
getObjectData()[key] = parseDecode(value);
}
@@ -142,6 +144,20 @@ abstract class ParseBase {
}
}

///Set the [ParseACL] governing this object.
void setACL<ParseACL>(ParseACL acl) {
getObjectData()[keyVarAcl] = acl;
}

///Access the [ParseACL] governing this object.
ParseACL getACL() {
if (getObjectData().containsKey(keyVarAcl)) {
return getObjectData()[keyVarAcl];
} else {
return ParseACL();
}
}

/// Gets type [T] from objectData
///
/// Returns null or [defaultValue] if provided. To get an int, call
4 changes: 2 additions & 2 deletions lib/src/objects/parse_object.dart
Original file line number Diff line number Diff line change
@@ -65,7 +65,7 @@ class ParseObject extends ParseBase implements ParseCloneable {
Future<ParseResponse> create() async {
try {
final Uri url = getSanitisedUri(_client, '$_path');
final String body = json.encode(toJson(forApiRQ: true);
final String body = json.encode(toJson(forApiRQ: true));
final Response result = await _client.post(url, body: body);

//Set the objectId on the object after it is created.
@@ -74,7 +74,7 @@ class ParseObject extends ParseBase implements ParseCloneable {
final Map<String, dynamic> map = json.decode(result.body);
objectId = map['objectId'].toString();
}

return handleResponse<ParseObject>(
this, result, ParseApiRQ.create, _debug, className);
} on Exception catch (e) {