001package gudusoft.gsqlparser.dlineage.dataflow.model;
002
003import gudusoft.gsqlparser.EDbVendor;
004import gudusoft.gsqlparser.ESetOperatorType;
005import gudusoft.gsqlparser.TCustomSqlStatement;
006import gudusoft.gsqlparser.TSourceToken;
007import gudusoft.gsqlparser.dlineage.util.DlineageUtil;
008import gudusoft.gsqlparser.dlineage.util.Pair3;
009import gudusoft.gsqlparser.nodes.TCTE;
010import gudusoft.gsqlparser.nodes.TFunctionCall;
011import gudusoft.gsqlparser.nodes.TJoinList;
012import gudusoft.gsqlparser.pp.utils.SourceTokenSearcher;
013import gudusoft.gsqlparser.sqlenv.TSQLEnv;
014import gudusoft.gsqlparser.stmt.*;
015import gudusoft.gsqlparser.stmt.hive.THiveLoad;
016import gudusoft.gsqlparser.stmt.mssql.TCreateExternalDataSourceStmt;
017import gudusoft.gsqlparser.stmt.mssql.TMssqlCreateFunction;
018import gudusoft.gsqlparser.stmt.mssql.TMssqlDeclare;
019import gudusoft.gsqlparser.stmt.mssql.TMssqlExecute;
020import gudusoft.gsqlparser.stmt.snowflake.TCreateStageStmt;
021import gudusoft.gsqlparser.stmt.snowflake.TCreateStreamStmt;
022import gudusoft.gsqlparser.stmt.snowflake.TSnowflakeCopyIntoStmt;
023import gudusoft.gsqlparser.stmt.teradata.TTeradataCreateProcedure;
024import gudusoft.gsqlparser.util.Logger;
025import gudusoft.gsqlparser.util.LoggerFactory;
026import gudusoft.gsqlparser.util.SQLUtil;
027
028import java.io.ByteArrayInputStream;
029import java.io.IOException;
030import java.util.ArrayDeque;
031import java.util.ArrayList;
032import java.util.Deque;
033import java.util.List;
034import java.util.Properties;
035
036public class Process {
037    private static final Logger logger = LoggerFactory.getLogger(Process.class);
038    private long id;
039    protected String server;
040    protected String schema;
041    protected String database;
042    private String name;
043    private String procedureName;
044    private Long procedureId;
045    private String queryHashId;
046    private String type;
047    private String customType;
048    private TCustomSqlStatement gspObject;
049    private Pair3<Long, Long, String> startPosition;
050    private Pair3<Long, Long, String> endPosition;
051    private List<Object> targetColumns = new ArrayList<Object>();
052    private List<Transform> transforms = new ArrayList<Transform>();
053
054    public Process(TCustomSqlStatement gspObject) {
055
056        if (gspObject == null) {
057            throw new IllegalArgumentException("Process arguments can't be null.");
058        }
059
060        id = ++ModelBindingManager.get().TABLE_COLUMN_ID;
061
062        this.gspObject = gspObject;
063
064        TSourceToken startToken = gspObject.getStartToken();
065        TSourceToken endToken = gspObject.getEndToken();
066        if (startToken != null) {
067            this.startPosition = new Pair3<Long, Long, String>(startToken.lineNo, startToken.columnNo,
068                    ModelBindingManager.getGlobalHash());
069        }
070
071        if (endToken != null) {
072            this.endPosition = new Pair3<Long, Long, String>(endToken.lineNo,
073                    endToken.columnNo + SQLUtil.endTrim(endToken.getAstext()).length(), ModelBindingManager.getGlobalHash());
074        }
075
076        this.schema = ModelBindingManager.getGlobalSchema();
077        this.database = ModelBindingManager.getGlobalDatabase();
078        this.queryHashId = SQLUtil.stringToMD5(gspObject.asCanonical().replaceAll("\r?\n", "\r\n"));
079
080        EDbVendor vendor = ModelBindingManager.getGlobalOption().getVendor();
081        boolean supportCatalog = TSQLEnv.supportCatalog(vendor);
082        boolean supportSchema = TSQLEnv.supportSchema(vendor);
083
084        fillSchemaInfo();
085
086        if (!supportCatalog) {
087            this.database = null;
088        } else if (this.database == null && !TSQLEnv.DEFAULT_DB_NAME.equals(getDefaultDatabase())) {
089            this.database = getDefaultDatabase();
090        }
091
092        if (!supportSchema) {
093            this.schema = null;
094        } else if (this.schema == null && !TSQLEnv.DEFAULT_SCHEMA_NAME.equals(getDefaultSchema())) {
095            this.schema = getDefaultSchema();
096        }
097
098        String procedureParent = getProcedureParentName(gspObject);
099        if (procedureParent != null) {
100            procedureName = procedureParent;
101            Procedure procedure = ModelBindingManager.get()
102                    .getProcedureByName(DlineageUtil.getIdentifierNormalTableName(
103                            DlineageUtil.getProcedureNameWithArgs(getParentProcedure(gspObject))));
104            if (procedure == null) {
105                procedure = ModelBindingManager.get().getProcedureByName(DlineageUtil.getIdentifierNormalTableName(
106                        DlineageUtil.getProcedureNameWithArgNum(getParentProcedure(gspObject))));
107            }
108            if (procedure != null) {
109                this.procedureId = procedure.getId();
110            }
111        } else {
112            procedureName = "batchQueries";
113        }
114
115        TCustomSqlStatement stmt = DlineageUtil.getTopStmt(ModelBindingManager.getGlobalStmtStack().peek());
116        String sqlComment = null;
117        try {
118            sqlComment = stmt.getCommentBeforeNode();
119        } catch (Exception e) {
120        }
121        if (!SQLUtil.isEmpty(sqlComment) && sqlComment.indexOf("process") != -1) {
122            Properties properties = new Properties();
123            try {
124                properties.load(
125                        new ByteArrayInputStream(sqlComment.replace("--", "").trim().replace(",", "\n").getBytes()));
126                if (properties.containsKey("process_label")) {
127                    this.customType = properties.getProperty("process_label");
128                } else if (properties.containsKey("process")) {
129                    this.customType = properties.getProperty("process");
130                }
131            } catch (IOException e) {
132                logger.error("load sql comment properties failed.", e);
133            }
134        }
135
136        if (gspObject instanceof TCreateTableSqlStatement) {
137            this.type = "Create Table";
138        } else if (gspObject instanceof TCreateViewSqlStatement) {
139            this.type = "Create View";
140        } else if (gspObject instanceof TUpdateSqlStatement) {
141            this.type = "Update";
142        } else if (gspObject instanceof TMergeSqlStatement) {
143            this.type = "Merge";
144        } else if (gspObject instanceof TInsertSqlStatement) {
145            this.type = "Insert";
146        } else if (gspObject instanceof TSelectSqlStatement
147                && ((TSelectSqlStatement) gspObject).getIntoClause() != null) {
148            this.type = "Select Into";
149        } else if (gspObject instanceof TCreateTriggerStmt) {
150            this.type = "Create Trigger";
151        } else if (gspObject instanceof TCreateFunctionStmt) {
152            this.type = "Create Function";
153        } else if (gspObject instanceof TMssqlCreateFunction) {
154            this.type = "Create Function";
155        } else if (gspObject instanceof TCreateProcedureStmt) {
156            this.type = "Create Procedure";
157        } else if (gspObject instanceof THiveLoad) {
158            this.type = "Hive Load";
159        } else if (gspObject instanceof TSnowflakeCopyIntoStmt) {
160            this.type = "Copy Into";
161        } else if (gspObject instanceof TAlterTableStatement) {
162            this.type = "Alter Table";
163        } else if (gspObject instanceof TRenameStmt) {
164            this.type = "Rename Table";
165        } else if (gspObject instanceof TMssqlDeclare) {
166            this.type = "Mssql Declare";
167        } else if (gspObject instanceof TCreateStageStmt) {
168            this.type = "Create Stage";
169        } else if (gspObject instanceof TCreateSynonymStmt) {
170            this.type = "Create Synonym";
171        } else if (gspObject instanceof TCreateExternalDataSourceStmt) {
172            this.type = "Create Datasource";
173        } else if (gspObject instanceof TCreateStreamStmt) {
174            this.type = "Create Stream";
175        } else if (gspObject instanceof TCreateDatabaseSqlStatement) {
176            this.type = "Create Database";
177        } else if (gspObject instanceof TCreateSchemaSqlStatement) {
178            this.type = "Create Schema";
179        } else if (gspObject instanceof TMssqlExecute && ((TMssqlExecute)gspObject).getModuleName().toString().equalsIgnoreCase("sp_rename")) {
180            this.type = "Rename Table";
181        } else if (gspObject instanceof TCallStatement){
182            this.type = "Function Call";
183        } else {
184            this.type = gspObject.getClass().getSimpleName();
185        }
186
187        if (this.server == null && !TSQLEnv.DEFAULT_SERVER_NAME.equals(getDefaultServer())) {
188            this.server = getDefaultServer();
189        }
190
191        if (stmt.getStatements() != null) {
192            for (int i = 0; i < stmt.getStatements().size(); i++) {
193                TCustomSqlStatement subquery = stmt.getStatements().get(i);
194                if (subquery instanceof TSelectSqlStatement) {
195                    TSelectSqlStatement select = (TSelectSqlStatement) subquery;
196                    appendTransform(select, ESetOperatorType.none);
197                }
198            }
199        }
200    }
201
202    public Process(TFunctionCall gspObject) {
203
204        if (gspObject == null) {
205            throw new IllegalArgumentException("Process arguments can't be null.");
206        }
207
208        id = ++ModelBindingManager.get().TABLE_COLUMN_ID;
209
210
211        TSourceToken startToken = gspObject.getStartToken();
212        TSourceToken endToken = gspObject.getEndToken();
213        if (startToken != null) {
214            this.startPosition = new Pair3<Long, Long, String>(startToken.lineNo, startToken.columnNo,
215                    ModelBindingManager.getGlobalHash());
216        }
217
218        if (endToken != null) {
219            this.endPosition = new Pair3<Long, Long, String>(endToken.lineNo,
220                    endToken.columnNo + SQLUtil.endTrim(endToken.getAstext()).length(), ModelBindingManager.getGlobalHash());
221        }
222
223        TCustomSqlStatement stmt = DlineageUtil.getTopStmt(ModelBindingManager.getGlobalStmtStack().peek());
224        this.gspObject = stmt;
225
226        this.schema = ModelBindingManager.getGlobalSchema();
227        this.database = ModelBindingManager.getGlobalDatabase();
228
229        if (stmt != null) {
230            this.queryHashId = SQLUtil.stringToMD5(stmt.asCanonical().replaceAll("\r?\n", "\r\n"));
231        }
232
233        EDbVendor vendor = ModelBindingManager.getGlobalOption().getVendor();
234        boolean supportCatalog = TSQLEnv.supportCatalog(vendor);
235        boolean supportSchema = TSQLEnv.supportSchema(vendor);
236
237        fillSchemaInfo();
238
239        if (!supportCatalog) {
240            this.database = null;
241        } else if (this.database == null && !TSQLEnv.DEFAULT_DB_NAME.equals(getDefaultDatabase())) {
242            this.database = getDefaultDatabase();
243        }
244
245        if (!supportSchema) {
246            this.schema = null;
247        } else if (this.schema == null && !TSQLEnv.DEFAULT_SCHEMA_NAME.equals(getDefaultSchema())) {
248            this.schema = getDefaultSchema();
249        }
250
251        String procedureParent = getProcedureParentName(stmt);
252        if (procedureParent != null) {
253            procedureName = procedureParent;
254            Procedure procedure = ModelBindingManager.get()
255                    .getProcedureByName(DlineageUtil.getIdentifierNormalTableName(
256                            DlineageUtil.getProcedureNameWithArgs(getParentProcedure(stmt))));
257            if (procedure == null) {
258                procedure = ModelBindingManager.get().getProcedureByName(DlineageUtil.getIdentifierNormalTableName(
259                        DlineageUtil.getProcedureNameWithArgNum(getParentProcedure(stmt))));
260            }
261            if (procedure != null) {
262                this.procedureId = procedure.getId();
263            }
264        } else {
265            procedureName = "batchQueries";
266        }
267
268        String sqlComment = null;
269        try {
270            sqlComment = stmt.getCommentBeforeNode();
271        } catch (Exception e) {
272        }
273        if (!SQLUtil.isEmpty(sqlComment) && sqlComment.indexOf("process") != -1) {
274            Properties properties = new Properties();
275            try {
276                properties.load(
277                        new ByteArrayInputStream(sqlComment.replace("--", "").trim().replace(",", "\n").getBytes()));
278                if (properties.containsKey("process_label")) {
279                    this.customType = properties.getProperty("process_label");
280                } else if (properties.containsKey("process")) {
281                    this.customType = properties.getProperty("process");
282                }
283            } catch (IOException e) {
284                logger.error("load sql comment properties failed.", e);
285            }
286        }
287
288        this.type = "Function Call";
289
290        if (this.server == null && !TSQLEnv.DEFAULT_SERVER_NAME.equals(getDefaultServer())) {
291            this.server = getDefaultServer();
292        }
293    }
294
295    private void appendTransform(TSelectSqlStatement select, ESetOperatorType operatorType) {
296        // Iterative traversal of the UNION tree to avoid StackOverflow with deeply nested unions.
297        Deque<Object[]> stack = new ArrayDeque<>();
298        stack.push(new Object[]{select, operatorType});
299
300        while (!stack.isEmpty()) {
301            Object[] entry = stack.pop();
302            TSelectSqlStatement current = (TSelectSqlStatement) entry[0];
303            ESetOperatorType currentOp = (ESetOperatorType) entry[1];
304
305            if (currentOp != ESetOperatorType.none) {
306                Transform transform = new Transform();
307                transform.setType(currentOp.name());
308                transform.setCodeString(current.toString());
309                transform.setStartToken(current.getStartToken());
310                transform.setEndToken(current.getEndToken());
311                transforms.add(transform);
312            } else {
313                if (current.getCteList() != null && current.getCteList().size() > 0) {
314                    for (int i = 0; i < current.getCteList().size(); i++) {
315                        TCTE cte = current.getCteList().getCTE(i);
316                        Transform transform = new Transform();
317                        transform.setType(Transform.CTE);
318                        transform.setCodeString(cte.toString());
319                        transform.setStartToken(cte.getStartToken());
320                        transform.setEndToken(cte.getEndToken());
321                        transforms.add(transform);
322                    }
323                }
324                if (current.getSetOperatorType() != ESetOperatorType.none) {
325                    // Push right first so left is processed first (stack is LIFO)
326                    if (current.getRightStmt() != null) {
327                        ESetOperatorType rightOp = current.getRightStmt().getSetOperatorType() != ESetOperatorType.none
328                                ? ESetOperatorType.none : current.getSetOperatorType();
329                        stack.push(new Object[]{current.getRightStmt(), rightOp});
330                    }
331                    if (current.getLeftStmt() != null) {
332                        ESetOperatorType leftOp = current.getLeftStmt().getSetOperatorType() != ESetOperatorType.none
333                                ? ESetOperatorType.none : current.getSetOperatorType();
334                        stack.push(new Object[]{current.getLeftStmt(), leftOp});
335                    }
336                } else {
337                    if (current.getJoins() != null && current.getJoins().size() > 0) {
338                        TJoinList joins = current.getJoins();
339                        TSourceToken startToken = joins.getStartToken();
340                        TSourceToken fromToken = SourceTokenSearcher.backforwardSearch(startToken, 10, "from");
341                        if (fromToken == null) {
342                            if (startToken.getAstext().equalsIgnoreCase("from")) {
343                                fromToken = startToken;
344                            } else {
345                                continue;
346                            }
347                        }
348                        StringBuilder builder = new StringBuilder();
349                        if (joins.getEndToken().posinlist < fromToken.container.size()
350                                && joins.getEndToken().posinlist >= fromToken.posinlist) {
351                            for (int j = fromToken.posinlist; j <= joins.getEndToken().posinlist; j++) {
352                                builder.append(fromToken.container.get(j));
353                            }
354                        } else {
355                            System.err.println("Handle statement transform error, statemet is:");
356                            System.err.println(current.toString());
357                        }
358                        Transform transform = new Transform();
359                        transform.setType(Transform.FROM);
360                        transform.setCodeString(builder.toString());
361                        transform.setStartToken(fromToken);
362                        transform.setEndToken(joins.getEndToken());
363                        transforms.add(transform);
364                    }
365                }
366            }
367        }
368    }
369
370    private TSelectSqlStatement getFirstSubquery(TSelectSqlStatement select) {
371        // Iterative left-chain walk to avoid StackOverflow with deeply nested unions.
372        TSelectSqlStatement current = select;
373        while (current.getSetOperatorType() != ESetOperatorType.none) {
374            current = current.getLeftStmt();
375        }
376        return current;
377    }
378
379    private TSelectSqlStatement getLastSubquery(TSelectSqlStatement select) {
380        // Iterative right-chain walk to avoid StackOverflow with deeply nested unions.
381        TSelectSqlStatement current = select;
382        while (current.getSetOperatorType() != ESetOperatorType.none) {
383            current = current.getRightStmt();
384        }
385        return current;
386    }
387
388    private void fillSchemaInfo() {
389        TCustomSqlStatement stmt = DlineageUtil.getTopStmt(ModelBindingManager.getGlobalStmtStack().peek());
390        String sqlComment = null;
391        try {
392            sqlComment = stmt.getCommentBeforeNode();
393        } catch (Exception e) {
394        }
395        if (!SQLUtil.isEmpty(sqlComment) && (sqlComment.indexOf("db") != -1 || sqlComment.indexOf("schema") != -1)) {
396            Properties properties = new Properties();
397            try {
398                properties.load(
399                        new ByteArrayInputStream(sqlComment.replace("--", "").trim().replace(",", "\n").getBytes()));
400                if (SQLUtil.isEmpty(this.server) && properties.containsKey("db-instance")) {
401                    this.server = properties.getProperty("db-instance");
402                }
403                if (SQLUtil.isEmpty(this.database) && properties.containsKey("db")) {
404                    this.database = properties.getProperty("db");
405                    if (this.database.indexOf(".") != -1) {
406                        String delimitedChar = TSQLEnv.delimitedChar(ModelBindingManager.getGlobalOption().getVendor());
407                        this.database = delimitedChar + SQLUtil.trimColumnStringQuote(this.database) + delimitedChar;
408                    }
409                }
410                if (SQLUtil.isEmpty(this.schema) && properties.containsKey("schema")) {
411                    this.schema = properties.getProperty("schema");
412                    if (this.schema.indexOf(".") != -1) {
413                        String delimitedChar = TSQLEnv.delimitedChar(ModelBindingManager.getGlobalOption().getVendor());
414                        this.schema = delimitedChar + SQLUtil.trimColumnStringQuote(this.schema) + delimitedChar;
415                    }
416                }
417            } catch (IOException e) {
418                logger.error("load sql comment properties failed.", e);
419            }
420        }
421    }
422
423    protected String getDefaultServer() {
424        String defaultServer = null;
425        if (ModelBindingManager.getGlobalSQLEnv() != null) {
426            defaultServer = ModelBindingManager.getGlobalSQLEnv().getDefaultServerName();
427        }
428        if (!SQLUtil.isEmpty(defaultServer))
429            return defaultServer;
430        return TSQLEnv.DEFAULT_SERVER_NAME;
431    }
432
433    protected String getDefaultSchema() {
434        String defaultSchema = null;
435        if (ModelBindingManager.getGlobalSQLEnv() != null) {
436            defaultSchema = ModelBindingManager.getGlobalSQLEnv().getDefaultSchemaName();
437        }
438        if (!SQLUtil.isEmpty(defaultSchema))
439            return defaultSchema;
440        return TSQLEnv.DEFAULT_SCHEMA_NAME;
441    }
442
443    protected String getDefaultDatabase() {
444        return ModelBindingManager.getEffectiveDefaultDatabase();
445    }
446
447    private String getProcedureParentName(TCustomSqlStatement stmt) {
448        if (stmt instanceof TStoredProcedureSqlStatement) {
449                        if (((TStoredProcedureSqlStatement) stmt).getStoredProcedureName() != null) {
450                                return ((TStoredProcedureSqlStatement) stmt).getStoredProcedureName().toString();
451                        }
452                }
453
454        if (stmt == null) {
455                return null;
456        }
457        
458        if (stmt instanceof TTeradataCreateProcedure) {
459            if (((TTeradataCreateProcedure) stmt).getProcedureName() != null) {
460                return ((TTeradataCreateProcedure) stmt).getProcedureName().toString();
461            }
462        }
463        
464        if (stmt instanceof TStoredProcedureSqlStatement) {
465            if (((TStoredProcedureSqlStatement) stmt).getStoredProcedureName() != null) {
466                return ((TStoredProcedureSqlStatement) stmt).getStoredProcedureName().toString();
467            }
468
469            if (stmt instanceof TCommonBlock) {
470                if (((TCommonBlock) stmt).getBlockBody().getParentObjectName() instanceof TStoredProcedureSqlStatement) {
471                    stmt = (TStoredProcedureSqlStatement) ((TCommonBlock) stmt).getBlockBody().getParentObjectName();
472                }
473                else {
474                        return getProcedureParentName(stmt.getParentStmt());
475                }
476            }
477            
478            return null;
479        }
480        
481                stmt = stmt.getParentStmt();
482        if (stmt == null) {
483            if(ModelBindingManager.getGlobalProcedure()!=null) {
484                return getProcedureParentName(ModelBindingManager.getGlobalProcedure());
485            }
486            else {
487                return null;
488            }
489        }
490        
491
492        if (stmt instanceof TCommonBlock) {
493            if (((TCommonBlock) stmt).getBlockBody().getParentObjectName() instanceof TStoredProcedureSqlStatement) {
494                stmt = (TStoredProcedureSqlStatement) ((TCommonBlock) stmt).getBlockBody().getParentObjectName();
495            }
496        }
497
498        return getProcedureParentName(stmt);
499    }
500
501    private TStoredProcedureSqlStatement getParentProcedure(TCustomSqlStatement stmt) {
502        if (stmt == null) {
503                return null;
504        }
505        
506        if (stmt instanceof TStoredProcedureSqlStatement) {
507            if (((TStoredProcedureSqlStatement) stmt).getStoredProcedureName() != null) {
508                return ((TStoredProcedureSqlStatement) stmt);
509            }
510            
511            if (stmt instanceof TCommonBlock) {
512                if (((TCommonBlock) stmt).getBlockBody().getParentObjectName() instanceof TStoredProcedureSqlStatement) {
513                    return (TStoredProcedureSqlStatement) ((TCommonBlock) stmt).getBlockBody().getParentObjectName();
514                }
515                else {
516                        return getParentProcedure(stmt.getParentStmt());
517                }
518            }
519            
520            return null;
521        }
522        
523            stmt = stmt.getParentStmt();
524        if (stmt == null) {
525            if(ModelBindingManager.getGlobalProcedure()!=null) {
526                return ModelBindingManager.getGlobalProcedure();
527            }
528            else {
529                return null;
530            }
531        }
532        
533        if (stmt instanceof TCommonBlock) {
534            if (((TCommonBlock) stmt).getBlockBody().getParentObjectName() instanceof TStoredProcedureSqlStatement) {
535                stmt = (TStoredProcedureSqlStatement) ((TCommonBlock) stmt).getBlockBody().getParentObjectName();
536            }
537        }
538
539        return getParentProcedure(stmt);
540    }
541
542    public Pair3<Long, Long, String> getStartPosition() {
543        return startPosition;
544    }
545
546    public Pair3<Long, Long, String> getEndPosition() {
547        return endPosition;
548    }
549
550    public TCustomSqlStatement getGspObject() {
551        return gspObject;
552    }
553
554    public long getId() {
555        return id;
556    }
557
558    public String getSchema() {
559        return schema;
560    }
561
562    public String getDatabase() {
563        return database;
564    }
565
566    public String getName() {
567        return name;
568    }
569
570    public String getType() {
571        return type;
572    }
573
574    public void setName(String name) {
575        this.name = name;
576    }
577
578    public void setType(String type) {
579        this.type = type;
580    }
581
582    public String getProcedureName() {
583        return procedureName;
584    }
585
586    public void setProcedureName(String procedureName) {
587        this.procedureName = procedureName;
588    }
589
590    public String getQueryHashId() {
591        return queryHashId;
592    }
593
594    public void setQueryHashId(String queryHashId) {
595        this.queryHashId = queryHashId;
596    }
597
598    public void appendColumn(Object tableColumn) {
599        if (tableColumn != null && !targetColumns.contains(tableColumn)) {
600            targetColumns.add(tableColumn);
601        }
602    }
603
604    public List<Object> getTargetColumns() {
605        return targetColumns;
606    }
607
608    public String getCustomType() {
609        return customType;
610    }
611
612    public Long getProcedureId() {
613        return procedureId;
614    }
615
616    public String getServer() {
617        return server;
618    }
619
620    public List<Transform> getTransforms() {
621        return transforms;
622    }
623}