| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| // Context parameters are typed as `any` because the generated grammar files |
| // require accepting arbitrary context types at runtime. |
| |
| import TranslateVisitor from './TranslateVisitor.js'; |
| |
| /** |
| * Converts a Gremlin traversal string into a Groovy source code representation. |
| * Mirrors the Java GroovyTranslateVisitor. |
| */ |
| export default class GroovyTranslateVisitor extends TranslateVisitor { |
| |
| constructor(graphTraversalSourceName: string = 'g') { |
| super(graphTraversalSourceName); |
| } |
| |
| visitIntegerLiteral(ctx: any): void { |
| const integerLiteral: string = ctx.getText().toLowerCase(); |
| const lastChar = integerLiteral[integerLiteral.length - 1]; |
| |
| if (!/[a-z]/.test(lastChar)) { |
| this.sb.push(integerLiteral); |
| return; |
| } |
| |
| const num = integerLiteral.slice(0, -1); |
| switch (lastChar) { |
| case 'b': |
| this.sb.push('(byte)'); |
| this.sb.push(num); |
| break; |
| case 's': |
| this.sb.push('(short)'); |
| this.sb.push(num); |
| break; |
| case 'n': |
| // Groovy BigInteger uses the 'g' suffix |
| this.sb.push(num); |
| this.sb.push('g'); |
| break; |
| default: |
| // i and l suffixes and anything else kept as-is |
| this.sb.push(integerLiteral); |
| } |
| } |
| |
| visitFloatLiteral(ctx: any): void { |
| if (ctx.infLiteral() != null) { this.visit(ctx.infLiteral()); return; } |
| if (ctx.nanLiteral() != null) { this.visit(ctx.nanLiteral()); return; } |
| |
| const floatLiteral: string = ctx.getText().toLowerCase(); |
| const lastChar = floatLiteral[floatLiteral.length - 1]; |
| |
| if (!/[a-z]/.test(lastChar)) { |
| this.sb.push(floatLiteral); |
| return; |
| } |
| |
| const num = floatLiteral.slice(0, -1); |
| switch (lastChar) { |
| case 'f': |
| case 'd': |
| // keep f/d suffix as-is |
| this.sb.push(floatLiteral); |
| break; |
| case 'm': |
| // Groovy BigDecimal: if no decimal point use new BigDecimal(NL), else just the number |
| if (!floatLiteral.includes('.')) { |
| this.sb.push('new BigDecimal('); |
| this.sb.push(num); |
| this.sb.push('L)'); |
| } else { |
| this.sb.push(num); |
| } |
| break; |
| default: |
| this.sb.push(floatLiteral); |
| } |
| } |
| |
| visitInfLiteral(ctx: any): void { |
| if (ctx.SignedInfLiteral() != null && ctx.SignedInfLiteral().getText() === '-Infinity') { |
| this.sb.push('Double.NEGATIVE_INFINITY'); |
| } else { |
| this.sb.push('Double.POSITIVE_INFINITY'); |
| } |
| } |
| |
| visitUuidLiteral(ctx: any): void { |
| if (ctx.stringLiteral() == null) { |
| this.sb.push('UUID.randomUUID()'); |
| return; |
| } |
| this.sb.push('UUID.fromString('); |
| this.sb.push(ctx.stringLiteral().getText()); |
| this.sb.push(')'); |
| } |
| |
| visitCharacterLiteral(ctx: any): void { |
| const text: string = ctx.getText(); |
| const withoutSuffix = text.substring(0, text.length - 1); |
| const inner = TranslateVisitor.removeFirstAndLastCharacters(withoutSuffix); |
| this.sb.push("'"); |
| this.sb.push(inner); |
| this.sb.push("' as char"); |
| } |
| |
| visitDurationLiteral(ctx: any): void { |
| const seconds = parseInt(ctx.integerLiteral(0).getText(), 10); |
| const nanos = parseInt(ctx.integerLiteral(1).getText(), 10); |
| const isPositive = ctx.booleanLiteral() === null || |
| ctx.booleanLiteral().getText() === 'true'; |
| this.sb.push(`Duration.ofSeconds(${seconds}, ${nanos})`); |
| if (!isPositive) { |
| this.sb.push('.negated()'); |
| } |
| } |
| |
| visitBinaryLiteral(ctx: any): void { |
| this.sb.push('ByteBuffer.wrap(Base64.getDecoder().decode('); |
| this.sb.push(ctx.stringLiteral().getText()); |
| this.sb.push('))'); |
| } |
| |
| visitNullLiteral(ctx: any): void { |
| if (ctx.parent?.constructor?.name === 'GenericMapNullableArgumentContext') { |
| this.sb.push('null as Map'); |
| } else { |
| this.sb.push(ctx.getText()); |
| } |
| } |
| |
| visitGenericSetLiteral(ctx: any): void { |
| this.sb.push('['); |
| const literals: any[] = ctx.genericLiteral(); |
| for (let i = 0; i < literals.length; i++) { |
| this.visit(literals[i]); |
| if (i < literals.length - 1) this.sb.push(', '); |
| } |
| this.sb.push('] as Set'); |
| } |
| |
| visitStringLiteral(ctx: any): void { |
| // Preserve original quote style and escape Groovy GString interpolation |
| this.sb.push(ctx.getText().replace(/\$/g, () => '\\$')); |
| } |
| |
| visitStringNullableLiteral(ctx: any): void { |
| if (ctx.getText() === 'null') { |
| this.sb.push('null'); |
| } else { |
| // Preserve original quote style and escape Groovy GString interpolation |
| this.sb.push(ctx.getText().replace(/\$/g, () => '\\$')); |
| } |
| } |
| |
| visitMapKey(ctx: any): void { |
| if (ctx.LPAREN() != null && ctx.RPAREN() != null) { |
| // Groovy map syntax: expression keys must be parenthesized — preserve the parens |
| this.sb.push('('); |
| this.visit(ctx.getChild(1)); |
| this.sb.push(')'); |
| } else { |
| this.visit(ctx.getChild(0)); |
| } |
| } |
| |
| visitNakedKey(ctx: any): void { |
| // Groovy map/config syntax uses unquoted identifier keys |
| this.sb.push(ctx.getText()); |
| } |
| |
| visitKeyword(ctx: any): void { |
| const keyword: string = ctx.getText(); |
| const parentName = ctx.parent?.constructor?.name ?? ''; |
| if (parentName === 'MapKeyContext' || parentName === 'ConfigurationContext') { |
| // Groovy uses unquoted keys |
| this.sb.push(keyword); |
| } else { |
| this.sb.push(keyword); |
| this.sb.push(' '); |
| } |
| } |
| |
| visitTraversalStrategy(ctx: any): void { |
| if (ctx.getChildCount() === 1) { |
| // No-arg strategy: output name as-is (no .instance()) |
| this.sb.push(ctx.getText()); |
| } else { |
| // Ensure 'new' keyword is present |
| if (ctx.getChild(0).getText() !== 'new') { |
| this.sb.push('new '); |
| } |
| this.visitChildren(ctx); |
| } |
| } |
| |
| visitTraversalSourceSpawnMethod_inject(ctx: any): void { |
| this.handleInject(ctx); |
| } |
| |
| visitTraversalMethod_inject(ctx: any): void { |
| this.handleInject(ctx); |
| } |
| |
| visitTraversalMethod_hasLabel_String_String(ctx: any): void { |
| if (ctx.getChildCount() > 4 && ctx.getChild(2).getText() === 'null') { |
| const varArgs = ctx.getChild(4); |
| this.sb.push(ctx.getChild(0).getText()); |
| this.sb.push('((String) null, '); |
| for (let i = 0; i < varArgs.getChildCount(); i += 2) { |
| if (varArgs.getChild(i).getText() === 'null') { |
| this.sb.push('(String) null'); |
| } else { |
| this.visit(varArgs.getChild(i)); |
| } |
| if (i < varArgs.getChildCount() - 1) { |
| this.sb.push(', '); |
| } |
| } |
| this.sb.push(')'); |
| return; |
| } |
| this.visitChildren(ctx); |
| } |
| |
| visitTraversalMethod_option_Merge_Map(ctx: any): void { |
| this.sb.push('option('); |
| this.visit(ctx.traversalMerge()); |
| this.sb.push(', '); |
| this.tryCastMapNullableArgument(ctx.genericMapNullableArgument()); |
| this.sb.push(')'); |
| } |
| |
| visitTraversalMethod_mergeV_Map(ctx: any): void { |
| this.sb.push('mergeV('); |
| this.tryCastMapNullableArgument(ctx.genericMapNullableArgument()); |
| this.sb.push(')'); |
| } |
| |
| visitTraversalSourceSpawnMethod_mergeV_Map(ctx: any): void { |
| this.sb.push('mergeV('); |
| this.tryCastMapNullableArgument(ctx.genericMapNullableArgument()); |
| this.sb.push(')'); |
| } |
| |
| visitTraversalMethod_mergeE_Map(ctx: any): void { |
| this.sb.push('mergeE('); |
| this.tryCastMapNullableArgument(ctx.genericMapNullableArgument()); |
| this.sb.push(')'); |
| } |
| |
| visitTraversalSourceSpawnMethod_mergeE_Map(ctx: any): void { |
| this.sb.push('mergeE('); |
| this.tryCastMapNullableArgument(ctx.genericMapNullableArgument()); |
| this.sb.push(')'); |
| } |
| |
| private tryCastMapNullableArgument(ctx: any): void { |
| const nullableLit = ctx?.genericMapNullableLiteral?.(); |
| if (nullableLit != null && nullableLit?.nullLiteral?.() != null) { |
| this.sb.push('(Map) null'); |
| } else { |
| this.visit(ctx); |
| } |
| } |
| |
| /** |
| * Special handling for inject with second `null` argument like g.inject(1, null). |
| * inject() is ambiguous in Groovy with jdk extension inject(Object, Closure). |
| */ |
| private handleInject(ctx: any): void { |
| if (ctx.getChildCount() > 3) { |
| const child2 = ctx.getChild(2); |
| if (child2?.constructor?.name === 'GenericLiteralVarargsContext') { |
| const varArgs = child2; |
| if (varArgs.getChildCount() === 1) { |
| const child0 = varArgs.getChild(0); |
| if (child0?.constructor?.name === 'GenericLiteralExprContext') { |
| const injectArgs = child0; |
| if (injectArgs.getChildCount() > 2 && injectArgs.getChild(2).getText() === 'null') { |
| this.sb.push(ctx.getChild(0).getText()); |
| this.sb.push('('); |
| for (let i = 0; i < injectArgs.getChildCount(); i += 2) { |
| if (i === 2) { |
| this.sb.push('(Object) null'); |
| } else { |
| this.visit(injectArgs.getChild(i)); |
| } |
| if (i < injectArgs.getChildCount() - 1) { |
| this.sb.push(', '); |
| } |
| } |
| this.sb.push(')'); |
| return; |
| } |
| } |
| } |
| } |
| } |
| this.visitChildren(ctx); |
| } |
| } |