Skip to content

Commit 63deb9d

Browse files
Merge pull request #18 from blueelvis/develop
[PLUGIN-1753] Added support for Named Databases, fixed UI along with several other fixes
2 parents 0918fe3 + 1aa8ee0 commit 63deb9d

17 files changed

+588
-97
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,7 @@ DirectivesVisitor.java
5050
release.properties
5151

5252
# Remove dev directory.
53-
dev
53+
dev
54+
55+
# VSCode Files
56+
.vscode

src/main/java/io/cdap/plugin/gcp/firestore/common/FirestoreConfig.java

Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@
1010
import io.cdap.cdap.etl.api.FailureCollector;
1111
import io.cdap.plugin.common.Constants;
1212
import io.cdap.plugin.common.IdUtils;
13+
import io.cdap.plugin.gcp.firestore.util.FirestoreConstants;
1314

1415
import java.io.IOException;
16+
import java.util.UUID;
1517
import javax.annotation.Nullable;
1618

1719
/**
@@ -20,6 +22,7 @@
2022
public class FirestoreConfig extends PluginConfig {
2123
public static final String NAME_PROJECT = "project";
2224
public static final String NAME_SERVICE_ACCOUNT_TYPE = "serviceAccountType";
25+
public static final String NAME_DATABASE = "databaseName";
2326
public static final String NAME_SERVICE_ACCOUNT_FILE_PATH = "serviceFilePath";
2427
public static final String NAME_SERVICE_ACCOUNT_JSON = "serviceAccountJSON";
2528
public static final String AUTO_DETECT = "auto-detect";
@@ -37,25 +40,31 @@ public class FirestoreConfig extends PluginConfig {
3740
@Nullable
3841
protected String project;
3942

43+
@Name(NAME_DATABASE)
44+
@Description("Name of the Firestore Database. "
45+
+ "If not specified, it will use '(default)'.")
46+
@Macro
47+
@Nullable
48+
protected String databaseName;
49+
4050
@Name(NAME_SERVICE_ACCOUNT_TYPE)
4151
@Description("Service account type, file path where the service account is located or the JSON content of the " +
4252
"service account.")
4353
@Macro
44-
@Nullable
4554
protected String serviceAccountType;
4655

4756
@Name(NAME_SERVICE_ACCOUNT_FILE_PATH)
4857
@Description("Path on the local file system of the service account key used "
4958
+ "for authorization. Can be set to 'auto-detect' when running on a Dataproc cluster. "
5059
+ "When running on other clusters, the file must be present on every node in the cluster.")
51-
@Macro
5260
@Nullable
61+
@Macro
5362
protected String serviceFilePath;
5463

5564
@Name(NAME_SERVICE_ACCOUNT_JSON)
5665
@Description("Content of the service account file.")
57-
@Macro
5866
@Nullable
67+
@Macro
5968
protected String serviceAccountJson;
6069

6170
public String getProject() {
@@ -103,11 +112,13 @@ public String getServiceAccountType() {
103112
return serviceAccountType;
104113
}
105114

115+
@Nullable
106116
public Boolean isServiceAccountJson() {
107117
String serviceAccountType = getServiceAccountType();
108118
return Strings.isNullOrEmpty(serviceAccountType) ? null : serviceAccountType.equals(SERVICE_ACCOUNT_JSON);
109119
}
110120

121+
@Nullable
111122
public Boolean isServiceAccountFilePath() {
112123
String serviceAccountType = getServiceAccountType();
113124
return Strings.isNullOrEmpty(serviceAccountType) ? null : serviceAccountType.equals(SERVICE_ACCOUNT_FILE_PATH);
@@ -127,6 +138,16 @@ public String getServiceAccount() {
127138
*/
128139
public void validate(FailureCollector collector) {
129140
IdUtils.validateReferenceName(referenceName, collector);
141+
validateDatabaseName(collector);
142+
}
143+
144+
public String getDatabaseName() {
145+
if (containsMacro(NAME_DATABASE) && Strings.isNullOrEmpty(databaseName)) {
146+
return null;
147+
} else if (Strings.isNullOrEmpty(databaseName)) {
148+
return "(default)";
149+
}
150+
return databaseName;
130151
}
131152

132153
public String getReferenceName() {
@@ -150,4 +171,69 @@ public boolean autoServiceAccountUnavailable() {
150171
}
151172
return false;
152173
}
174+
175+
/**
176+
* Validates the given database name to consists of characters allowed to represent a dataset.
177+
*/
178+
public void validateDatabaseName(FailureCollector collector) {
179+
if (containsMacro(FirestoreConfig.NAME_DATABASE)) {
180+
return;
181+
}
182+
183+
String databaseName = getDatabaseName();
184+
185+
// Check if the database name is empty or null.
186+
if (Strings.isNullOrEmpty(databaseName)) {
187+
collector.addFailure("Database Name must be specified.", null)
188+
.withConfigProperty(FirestoreConfig.NAME_DATABASE);
189+
}
190+
191+
// Check if database name contains the (default)
192+
if (!databaseName.equals(FirestoreConstants.DEFAULT_DATABASE_NAME)) {
193+
194+
// Ensure database name includes only letters, numbers, and hyphen (-)
195+
// characters.
196+
if (!databaseName.matches("^[a-zA-Z0-9-]+$")) {
197+
collector.addFailure("Database name can only include letters, numbers and hyphen characters.", null)
198+
.withConfigProperty(FirestoreConfig.NAME_DATABASE);
199+
}
200+
201+
// Ensure database name is in lower case.
202+
if (databaseName != databaseName.toLowerCase()) {
203+
collector.addFailure("Database name must be in lowercase.", null)
204+
.withConfigProperty(FirestoreConfig.NAME_DATABASE);
205+
}
206+
207+
// The first character must be a letter.
208+
if (!databaseName.matches("^[a-zA-Z].*")) {
209+
collector.addFailure("Database name's first character can only be an alphabet.", null)
210+
.withConfigProperty(FirestoreConfig.NAME_DATABASE);
211+
}
212+
213+
// The last character must be a letter or number.
214+
if (!databaseName.matches(".*[a-zA-Z0-9]$")) {
215+
collector.addFailure("Database name's last character can only be a letter or a number.", null)
216+
.withConfigProperty(FirestoreConfig.NAME_DATABASE);
217+
}
218+
219+
// Minimum of 4 characters.
220+
if (databaseName.length() < 4) {
221+
collector.addFailure("Database name should be at least 4 letters.", null)
222+
.withConfigProperty(FirestoreConfig.NAME_DATABASE);
223+
}
224+
225+
// Maximum of 63 characters.
226+
if (databaseName.length() > 63) {
227+
collector.addFailure("Database name cannot be more than 63 characters.", null)
228+
.withConfigProperty(FirestoreConfig.NAME_DATABASE);
229+
}
230+
231+
// Should not be a UUID.
232+
try {
233+
UUID.fromString(databaseName);
234+
collector.addFailure("Database name cannot contain a UUID.", null)
235+
.withConfigProperty(FirestoreConfig.NAME_DATABASE);
236+
} catch (IllegalArgumentException e) { }
237+
}
238+
}
153239
}

src/main/java/io/cdap/plugin/gcp/firestore/sink/FirestoreOutputFormatProvider.java

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,21 +38,31 @@ public class FirestoreOutputFormatProvider implements OutputFormatProvider {
3838
* for {@link FirestoreRecordWriter}.
3939
*
4040
* @param project Firestore project
41-
* @param serviceAccountPath Firestore service account path
41+
* @param databaseName Name of the Firestore Database
42+
* @param serviceAccountFilePath Path to the JSON File containing the service account credentials
43+
* @param serviceAccountJson JSON content of the service account credentials
44+
* @param serviceAccountType The type of the Service account if it is stored in a filePath or JSON
4245
* @param collection Firestore collection name
4346
* @param shouldUseAutoGeneratedId should use auto generated document id
4447
* @param batchSize batch size
4548
*/
46-
public FirestoreOutputFormatProvider(String project, @Nullable String serviceAccountPath,
47-
String collection, String shouldUseAutoGeneratedId, String batchSize) {
49+
public FirestoreOutputFormatProvider(String project, String databaseName, @Nullable String serviceAccountFilePath,
50+
@Nullable String serviceAccountJson, String serviceAccountType,
51+
String collection, String shouldUseAutoGeneratedId,
52+
String batchSize) {
4853
ImmutableMap.Builder<String, String> builder = new ImmutableMap.Builder<String, String>()
4954
.put(FirestoreConfig.NAME_PROJECT, project)
50-
.put(FirestoreConstants.PROPERTY_COLLECTION, collection)
55+
.put(FirestoreConfig.NAME_DATABASE, databaseName)
56+
.put(FirestoreConfig.NAME_SERVICE_ACCOUNT_TYPE, serviceAccountType)
57+
.put(FirestoreConstants.PROPERTY_COLLECTION, Strings.isNullOrEmpty(collection) ? "" : collection)
5158
.put(FirestoreSinkConstants.PROPERTY_ID_TYPE, shouldUseAutoGeneratedId)
5259
.put(FirestoreSinkConstants.PROPERTY_BATCH_SIZE, batchSize);
5360

54-
if (!Strings.isNullOrEmpty(serviceAccountPath)) {
55-
builder.put(FirestoreConfig.NAME_SERVICE_ACCOUNT_FILE_PATH, serviceAccountPath);
61+
if (!Strings.isNullOrEmpty(serviceAccountFilePath)) {
62+
builder.put(FirestoreConfig.NAME_SERVICE_ACCOUNT_FILE_PATH, serviceAccountFilePath);
63+
}
64+
if (!Strings.isNullOrEmpty(serviceAccountJson)) {
65+
builder.put(FirestoreConfig.NAME_SERVICE_ACCOUNT_JSON, serviceAccountJson);
5666
}
5767
this.configMap = builder.build();
5868
}

src/main/java/io/cdap/plugin/gcp/firestore/sink/FirestoreRecordWriter.java

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import com.google.cloud.firestore.WriteResult;
2525
import com.google.common.base.Strings;
2626
import io.cdap.plugin.gcp.firestore.common.FirestoreConfig;
27+
import io.cdap.plugin.gcp.firestore.exception.FirestoreInitializationException;
2728
import io.cdap.plugin.gcp.firestore.sink.util.FirestoreSinkConstants;
2829
import io.cdap.plugin.gcp.firestore.util.FirestoreConstants;
2930
import io.cdap.plugin.gcp.firestore.util.FirestoreUtil;
@@ -58,16 +59,38 @@ public class FirestoreRecordWriter extends RecordWriter<NullWritable, Map<String
5859
*/
5960
public FirestoreRecordWriter(TaskAttemptContext taskAttemptContext) {
6061
Configuration config = taskAttemptContext.getConfiguration();
62+
6163
String projectId = config.get(FirestoreConfig.NAME_PROJECT);
64+
String databaseId = config.get(FirestoreConfig.NAME_DATABASE);
65+
66+
// Get Service Account
67+
Boolean isServiceAccountFilePath = false;
6268
String serviceAccountFilePath = config.get(FirestoreConfig.NAME_SERVICE_ACCOUNT_FILE_PATH);
69+
70+
// Get Service Account Type whether JSON or FilePath
71+
String serviceAccountType = config.get(FirestoreConfig.NAME_SERVICE_ACCOUNT_TYPE);
72+
73+
String serviceAccount = "";
74+
if (serviceAccountType.equalsIgnoreCase(FirestoreConfig.SERVICE_ACCOUNT_FILE_PATH)) {
75+
serviceAccount = config.get(FirestoreConfig.NAME_SERVICE_ACCOUNT_FILE_PATH);
76+
isServiceAccountFilePath = true;
77+
} else if (serviceAccountType.equalsIgnoreCase(FirestoreConfig.SERVICE_ACCOUNT_JSON)) {
78+
serviceAccount = config.get(FirestoreConfig.NAME_SERVICE_ACCOUNT_JSON);
79+
isServiceAccountFilePath = false;
80+
} else {
81+
throw new FirestoreInitializationException("Service account type can only be either a File Path or JSON.");
82+
}
83+
6384
String collection = Strings.nullToEmpty(config.get(FirestoreConstants.PROPERTY_COLLECTION)).trim();
6485
this.batchSize = config.getInt(FirestoreSinkConstants.PROPERTY_BATCH_SIZE, 25);
6586
this.useAutogeneratedId = config.getBoolean(FirestoreSinkConstants.PROPERTY_ID_TYPE, false);
66-
LOG.debug("Initialize RecordWriter(projectId={}, collection={}, serviceFilePath={}, batchSize={}, " +
67-
"useAutogeneratedId={})", projectId, collection, serviceAccountFilePath, batchSize,
68-
useAutogeneratedId);
6987

70-
this.db = FirestoreUtil.getFirestore(serviceAccountFilePath, projectId);
88+
LOG.debug("Initialize RecordWriter(projectId={}, databaseId={}, collection={}, " +
89+
"isServiceAccountFilePath={}, serviceFilePath={}, " +
90+
"batchSize={}, useAutogeneratedId={}", projectId, databaseId, collection, isServiceAccountFilePath,
91+
serviceAccountFilePath, batchSize, useAutogeneratedId);
92+
93+
this.db = FirestoreUtil.getFirestore(serviceAccount, isServiceAccountFilePath, projectId, databaseId);
7194
this.collectionRef = db.collection(collection);
7295
this.batch = db.batch();
7396
this.totalCount = 0;

src/main/java/io/cdap/plugin/gcp/firestore/sink/FirestoreSink.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,17 @@ public void prepareRun(BatchSinkContext batchSinkContext) throws Exception {
7575
collector.getOrThrowException();
7676

7777
String project = config.getProject();
78-
String serviceAccountFile = config.getServiceAccountFilePath();
78+
String databaseName = config.getDatabaseName();
79+
String serviceAccountFilePath = config.getServiceAccountFilePath();
80+
String serviceAccountJson = config.getServiceAccountJson();
81+
String serviceAccountType = config.getServiceAccountType();
7982
String collection = config.getCollection();
8083
String shouldAutoGenerateId = Boolean.toString(config.shouldUseAutoGeneratedId());
8184
String batchSize = Integer.toString(config.getBatchSize());
8285

8386
batchSinkContext.addOutput(Output.of(config.getReferenceName(),
84-
new FirestoreOutputFormatProvider(project, serviceAccountFile, collection, shouldAutoGenerateId,
85-
batchSize)));
87+
new FirestoreOutputFormatProvider(project, databaseName, serviceAccountFilePath, serviceAccountJson,
88+
serviceAccountType, collection, shouldAutoGenerateId, batchSize)));
8689

8790
LineageRecorder lineageRecorder = new LineageRecorder(batchSinkContext, config.getReferenceName());
8891
lineageRecorder.createExternalDataset(inputSchema);

src/main/java/io/cdap/plugin/gcp/firestore/sink/FirestoreSinkConfig.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,17 +75,19 @@ public FirestoreSinkConfig() {
7575
* @param referenceName the reference name
7676
* @param project the project id
7777
* @param serviceFilePath the service file path
78+
* @param databaseName the name of the database
7879
* @param collection the collection
7980
* @param idType the id type
8081
* @param idAlias the id alias
8182
* @param batchSize the batch size
8283
*/
8384
@VisibleForTesting
84-
public FirestoreSinkConfig(String referenceName, String project, String serviceFilePath,
85+
public FirestoreSinkConfig(String referenceName, String project, String serviceFilePath, String databaseName,
8586
String collection, String idType, String idAlias, int batchSize) {
8687
this.referenceName = referenceName;
8788
this.project = project;
8889
this.serviceFilePath = serviceFilePath;
90+
this.databaseName = databaseName;
8991
this.collection = collection;
9092
this.idType = idType;
9193
this.idAlias = idAlias;
@@ -148,7 +150,6 @@ public boolean shouldUseAutoGeneratedId() {
148150
*/
149151
public void validate(@Nullable Schema schema, FailureCollector collector) {
150152
super.validate(collector);
151-
152153
validateBatchSize(collector);
153154
validateFirestoreConnection(collector);
154155

@@ -164,13 +165,15 @@ void validateFirestoreConnection(FailureCollector collector) {
164165
return;
165166
}
166167
try {
167-
Firestore db = FirestoreUtil.getFirestore(getServiceAccountFilePath(), getProject());
168+
Firestore db = FirestoreUtil.getFirestore(getServiceAccount(), isServiceAccountFilePath(),
169+
getProject(), getDatabaseName());
168170
db.close();
169171
} catch (Exception e) {
170172
collector.addFailure(e.getMessage(), "Ensure properties like project, service account " +
171-
"file path are correct.")
173+
"file path, database name are correct.")
172174
.withConfigProperty(NAME_SERVICE_ACCOUNT_FILE_PATH)
173175
.withConfigProperty(NAME_PROJECT)
176+
.withConfigProperty(NAME_DATABASE)
174177
.withStacktrace(e.getStackTrace());
175178
}
176179
}
@@ -185,7 +188,7 @@ private void validateSchema(Schema schema, FailureCollector collector) {
185188
}
186189

187190
/**
188-
* Validates given field schema to be complaint with Firestore types.
191+
* Validates given field schema to be compliant with Firestore types.
189192
* Will throw {@link IllegalArgumentException} if schema contains unsupported type.
190193
*
191194
* @param fieldName field name

src/main/java/io/cdap/plugin/gcp/firestore/source/FirestoreInputFormatProvider.java

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,10 @@ public class FirestoreInputFormatProvider implements InputFormatProvider {
3939
/**
4040
* Constructor for FirestoreInputFormatProvider object.
4141
* @param project the project of Firestore DB
42-
* @param serviceAccountPath the service account path of Firestore DB
42+
* @param databaseName Name of the Firestore database
43+
* @param serviceAccountFilePath the service account path of Firestore DB
44+
* @param serviceAccountJson JSON content of the service account credentials
45+
* @param serviceAccountType The type of the Service account if it is stored in a filePath or JSON
4346
* @param collection the collection
4447
* @param mode there are two modes (basic and advanced)
4548
* @param pullDocuments the list of documents to pull
@@ -48,18 +51,24 @@ public class FirestoreInputFormatProvider implements InputFormatProvider {
4851
* @param fields the fields of collection
4952
*/
5053
public FirestoreInputFormatProvider(
51-
String project, @Nullable String serviceAccountPath, String collection, String mode,
54+
String project, String databaseName, @Nullable String serviceAccountFilePath, @Nullable String serviceAccountJson,
55+
String serviceAccountType, String collection, String mode,
5256
String pullDocuments, String skipDocuments, String filters, List<String> fields) {
5357
ImmutableMap.Builder<String, String> builder = new ImmutableMap.Builder<String, String>()
5458
.put(FirestoreConfig.NAME_PROJECT, project)
59+
.put(FirestoreConfig.NAME_DATABASE, databaseName)
60+
.put(FirestoreConfig.NAME_SERVICE_ACCOUNT_TYPE, serviceAccountType)
5561
.put(FirestoreConstants.PROPERTY_COLLECTION, Strings.isNullOrEmpty(collection) ? "" : collection)
5662
.put(FirestoreSourceConstants.PROPERTY_QUERY_MODE, mode)
5763
.put(FirestoreSourceConstants.PROPERTY_PULL_DOCUMENTS, Strings.isNullOrEmpty(pullDocuments) ? "" : pullDocuments)
5864
.put(FirestoreSourceConstants.PROPERTY_SKIP_DOCUMENTS, Strings.isNullOrEmpty(skipDocuments) ? "" : skipDocuments)
5965
.put(FirestoreSourceConstants.PROPERTY_CUSTOM_QUERY, Strings.isNullOrEmpty(filters) ? "" : filters)
6066
.put(FirestoreSourceConstants.PROPERTY_SCHEMA, Joiner.on(",").join(fields));
61-
if (Objects.nonNull(serviceAccountPath)) {
62-
builder.put(FirestoreConfig.NAME_SERVICE_ACCOUNT_FILE_PATH, serviceAccountPath);
67+
if (Objects.nonNull(serviceAccountFilePath)) {
68+
builder.put(FirestoreConfig.NAME_SERVICE_ACCOUNT_FILE_PATH, serviceAccountFilePath);
69+
}
70+
if (Objects.nonNull(serviceAccountJson)) {
71+
builder.put(FirestoreConfig.NAME_SERVICE_ACCOUNT_JSON, serviceAccountJson);
6372
}
6473
this.configMap = builder.build();
6574
}

0 commit comments

Comments
 (0)