diff --git a/.editorconfig b/.editorconfig
index da154efb8a0..a2a93880be0 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -25,6 +25,21 @@ indent_style = space
indent_size = 2
trim_trailing_whitespace=true
+ij_continuation_indent_size = 4
+ij_java_wrap_comments = true
+ij_any_indent_case_from_switch = false
+
+[*.{avsc,avpr,avdl}]
+indent_style = space
+indent_size = 2
+trim_trailing_whitespace=true
+
+ij_continuation_indent_size = 4
+ij_json_space_after_colon = true
+ij_json_space_before_colon = true
+ij_json_spaces_within_brackets = true
+ij_any_array_initializer_wrap = off
+
[*.{ps1}]
indent_style = space
indent_size = 4
diff --git a/lang/java/archetypes/avro-service-archetype/src/main/pom/pom.xml b/lang/java/archetypes/avro-service-archetype/src/main/pom/pom.xml
index 69063eb2919..399bbea940b 100644
--- a/lang/java/archetypes/avro-service-archetype/src/main/pom/pom.xml
+++ b/lang/java/archetypes/avro-service-archetype/src/main/pom/pom.xml
@@ -33,6 +33,9 @@
Simple Avro Ordering Service
+ ${maven.compiler.source}
+ ${maven.compiler.target}
+ ${project.build.sourceEncoding}${project.version}${maven.compiler.source}${maven.compiler.target}
diff --git a/lang/java/avro/pom.xml b/lang/java/avro/pom.xml
index 541a73df8be..b96673d1851 100644
--- a/lang/java/avro/pom.xml
+++ b/lang/java/avro/pom.xml
@@ -24,7 +24,7 @@
avro-parentorg.apache.avro1.12.0-SNAPSHOT
- ../
+ ../pom.xmlavro
diff --git a/lang/java/compiler/pom.xml b/lang/java/compiler/pom.xml
index fe7fbe404bf..2019318ee97 100644
--- a/lang/java/compiler/pom.xml
+++ b/lang/java/compiler/pom.xml
@@ -183,37 +183,8 @@
-
-
-
- org.eclipse.m2e
- lifecycle-mapping
- 1.0.0
-
-
-
-
-
- org.codehaus.mojo
- exec-maven-plugin
- [1.0,)
-
- exec
-
-
-
-
-
-
-
-
-
-
-
-
-
${project.groupId}
@@ -242,4 +213,42 @@
+
+
+ m2e
+
+ m2e.version
+
+
+
+
+
+ org.eclipse.m2e
+ lifecycle-mapping
+ 1.0.0
+
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ [1.0,)
+
+ exec
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lang/java/compiler/src/main/java/org/apache/avro/compiler/specific/SpecificCompiler.java b/lang/java/compiler/src/main/java/org/apache/avro/compiler/specific/SpecificCompiler.java
index fdffaad32c1..f6f842035fa 100644
--- a/lang/java/compiler/src/main/java/org/apache/avro/compiler/specific/SpecificCompiler.java
+++ b/lang/java/compiler/src/main/java/org/apache/avro/compiler/specific/SpecificCompiler.java
@@ -178,8 +178,14 @@ public SpecificCompiler(Protocol protocol) {
}
public SpecificCompiler(Schema schema) {
+ this(Collections.singleton(schema));
+ }
+
+ public SpecificCompiler(Collection schemas) {
this();
- enqueue(schema);
+ for (Schema schema : schemas) {
+ enqueue(schema);
+ }
this.protocol = null;
}
diff --git a/lang/java/compiler/src/main/javacc/org/apache/avro/compiler/idl/idl.jj b/lang/java/compiler/src/main/javacc/org/apache/avro/compiler/idl/idl.jj
index 3ee12fc385b..117764497e3 100644
--- a/lang/java/compiler/src/main/javacc/org/apache/avro/compiler/idl/idl.jj
+++ b/lang/java/compiler/src/main/javacc/org/apache/avro/compiler/idl/idl.jj
@@ -88,7 +88,10 @@ import org.apache.commons.text.StringEscapeUtils;
*
* Note: each instance is not thread-safe, but multiple separate
* instances are safely independent.
+ *
+ * @deprecated Use the new org.apache.avro.idl.IdlReader from avro-idl instead.
*/
+@Deprecated
public class Idl implements Closeable {
static JsonNodeFactory FACTORY = JsonNodeFactory.instance;
private static final String OPTIONAL_NULLABLE_TYPE_PROPERTY = "org.apache.avro.compiler.idl.Idl.NullableType.optional";
diff --git a/lang/java/grpc/pom.xml b/lang/java/grpc/pom.xml
index f04dd468fcf..d895a1ba3a1 100644
--- a/lang/java/grpc/pom.xml
+++ b/lang/java/grpc/pom.xml
@@ -24,7 +24,7 @@
org.apache.avroavro-parent1.12.0-SNAPSHOT
- ../
+ ../pom.xmlavro-grpc
diff --git a/lang/java/grpc/src/test/avro/TestService.avdl b/lang/java/grpc/src/test/avro/TestService.avdl
index 9a4629a8f5c..6c5f6a038b8 100644
--- a/lang/java/grpc/src/test/avro/TestService.avdl
+++ b/lang/java/grpc/src/test/avro/TestService.avdl
@@ -1,4 +1,4 @@
-/**
+/*
* 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
diff --git a/lang/java/idl/pom.xml b/lang/java/idl/pom.xml
new file mode 100644
index 00000000000..6363ec150a5
--- /dev/null
+++ b/lang/java/idl/pom.xml
@@ -0,0 +1,172 @@
+
+
+
+ 4.0.0
+
+
+ avro-parent
+ org.apache.avro
+ 1.12.0-SNAPSHOT
+ ../pom.xml
+
+
+ avro-idl
+
+ Apache Avro IDL
+ bundle
+ https://avro.apache.org
+ Compilers for Avro IDL and Avro Specific Java API
+
+
+ ${project.parent.parent.basedir}
+
+ !org.apache.avro.idl*,
+ org.apache.avro*;version="${project.version}",
+ org.apache.commons.text*,
+ *
+
+ org.apache.avro.idl*;version="${project.version}"
+ 4.9.3
+
+
+
+
+
+ src/main/resources
+
+
+
+
+ src/test/resources
+
+
+ src/test/idl
+
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+
+
+
+ org.apache.avro.idl
+
+
+
+
+
+ prepare-test-jar
+ generate-test-resources
+
+ test-jar
+
+
+ test-resource
+ src/test/idl/putOnClassPath
+ putOnClassPath
+ ${project.build.testOutputDirectory}
+
+
+
+
+
+ org.antlr
+ antlr4-maven-plugin
+ ${antlr.version}
+
+
+ antlr
+
+ antlr4
+
+
+
+
+ ${project.basedir}/../../../share/idl_grammar
+ ${project.basedir}/../../../share/idl_grammar/imports
+ true
+ false
+
+
+
+
+
+
+
+ ${project.groupId}
+ avro
+ ${project.version}
+
+
+ org.antlr
+ antlr4-runtime
+ ${antlr.version}
+
+
+ org.apache.commons
+ commons-text
+ ${commons-text.version}
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+
+
+
+ m2e
+
+ m2e.version
+
+
+
+
+
+ org.eclipse.m2e
+ lifecycle-mapping
+ 1.0.0
+
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ [1.0,)
+
+ exec
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lang/java/idl/src/main/java/org/apache/avro/idl/IdlFile.java b/lang/java/idl/src/main/java/org/apache/avro/idl/IdlFile.java
new file mode 100644
index 00000000000..56627b5821b
--- /dev/null
+++ b/lang/java/idl/src/main/java/org/apache/avro/idl/IdlFile.java
@@ -0,0 +1,120 @@
+/*
+ * 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
+ *
+ * https://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.
+ */
+package org.apache.avro.idl;
+
+import org.apache.avro.Protocol;
+import org.apache.avro.Schema;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * A parsed IdlFile. Provides access to the named schemas in the IDL file and
+ * the protocol containing the schemas.
+ */
+public class IdlFile {
+ private final Protocol protocol;
+ private final String namespace;
+ private final Map namedSchemas;
+ private final List warnings;
+
+ IdlFile(Protocol protocol, List warnings) {
+ this(protocol.getNamespace(), protocol.getTypes(), protocol, warnings);
+ }
+
+ private IdlFile(String namespace, Iterable schemas, Protocol protocol, List warnings) {
+ this.namespace = namespace;
+ this.namedSchemas = new LinkedHashMap<>();
+ for (Schema namedSchema : schemas) {
+ this.namedSchemas.put(namedSchema.getFullName(), namedSchema);
+ }
+ this.protocol = protocol;
+ this.warnings = Collections.unmodifiableList(new ArrayList<>(warnings));
+ }
+
+ /**
+ * The protocol defined by the IDL file.
+ */
+ public Protocol getProtocol() {
+ return protocol;
+ }
+
+ public List getWarnings() {
+ return warnings;
+ }
+
+ public List getWarnings(String importFile) {
+ return warnings.stream()
+ .map(warning -> importFile + ' ' + Character.toLowerCase(warning.charAt(0)) + warning.substring(1))
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * The default namespace to resolve schema names against.
+ */
+ public String getNamespace() {
+ return namespace;
+ }
+
+ /**
+ * The named schemas defined by the IDL file, mapped by their full name.
+ */
+ public Map getNamedSchemas() {
+ return Collections.unmodifiableMap(namedSchemas);
+ }
+
+ /**
+ * Get a named schema defined by the IDL file, by name. The name can be a simple
+ * name in the default namespace of the IDL file (e.g., the namespace of the
+ * protocol), or a full name.
+ *
+ * @param name the full name of the schema, or a simple name
+ * @return the schema, or {@code null} if it does not exist
+ */
+ public Schema getNamedSchema(String name) {
+ Schema result = namedSchemas.get(name);
+ if (result != null) {
+ return result;
+ }
+ if (namespace != null && !name.contains(".")) {
+ result = namedSchemas.get(namespace + '.' + name);
+ }
+ return result;
+ }
+
+ // Visible for testing
+ String outputString() {
+ if (protocol != null) {
+ return protocol.toString();
+ }
+ if (namedSchemas.isEmpty()) {
+ return "[]";
+ } else {
+ StringBuilder buffer = new StringBuilder();
+ for (Schema schema : namedSchemas.values()) {
+ buffer.append(',').append(schema);
+ }
+ buffer.append(']').setCharAt(0, '[');
+ return buffer.toString();
+ }
+ }
+}
diff --git a/lang/java/idl/src/main/java/org/apache/avro/idl/IdlReader.java b/lang/java/idl/src/main/java/org/apache/avro/idl/IdlReader.java
new file mode 100644
index 00000000000..ec9f698819a
--- /dev/null
+++ b/lang/java/idl/src/main/java/org/apache/avro/idl/IdlReader.java
@@ -0,0 +1,1048 @@
+/*
+ * 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
+ *
+ * https://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.
+ */
+package org.apache.avro.idl;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.BooleanNode;
+import com.fasterxml.jackson.databind.node.DoubleNode;
+import com.fasterxml.jackson.databind.node.IntNode;
+import com.fasterxml.jackson.databind.node.LongNode;
+import com.fasterxml.jackson.databind.node.NullNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.databind.node.TextNode;
+import org.antlr.v4.runtime.BaseErrorListener;
+import org.antlr.v4.runtime.CharStream;
+import org.antlr.v4.runtime.CharStreams;
+import org.antlr.v4.runtime.CommonTokenStream;
+import org.antlr.v4.runtime.ParserRuleContext;
+import org.antlr.v4.runtime.RecognitionException;
+import org.antlr.v4.runtime.Recognizer;
+import org.antlr.v4.runtime.Token;
+import org.apache.avro.JsonProperties;
+import org.apache.avro.LogicalType;
+import org.apache.avro.LogicalTypes;
+import org.apache.avro.Protocol;
+import org.apache.avro.Schema;
+import org.apache.avro.SchemaParseException;
+import org.apache.avro.idl.IdlParser.ArrayTypeContext;
+import org.apache.avro.idl.IdlParser.EnumDeclarationContext;
+import org.apache.avro.idl.IdlParser.EnumSymbolContext;
+import org.apache.avro.idl.IdlParser.FieldDeclarationContext;
+import org.apache.avro.idl.IdlParser.FixedDeclarationContext;
+import org.apache.avro.idl.IdlParser.FormalParameterContext;
+import org.apache.avro.idl.IdlParser.FullTypeContext;
+import org.apache.avro.idl.IdlParser.IdentifierContext;
+import org.apache.avro.idl.IdlParser.IdlFileContext;
+import org.apache.avro.idl.IdlParser.ImportStatementContext;
+import org.apache.avro.idl.IdlParser.JsonArrayContext;
+import org.apache.avro.idl.IdlParser.JsonLiteralContext;
+import org.apache.avro.idl.IdlParser.JsonObjectContext;
+import org.apache.avro.idl.IdlParser.JsonPairContext;
+import org.apache.avro.idl.IdlParser.JsonValueContext;
+import org.apache.avro.idl.IdlParser.MapTypeContext;
+import org.apache.avro.idl.IdlParser.MessageDeclarationContext;
+import org.apache.avro.idl.IdlParser.NullableTypeContext;
+import org.apache.avro.idl.IdlParser.PrimitiveTypeContext;
+import org.apache.avro.idl.IdlParser.ProtocolDeclarationBodyContext;
+import org.apache.avro.idl.IdlParser.ProtocolDeclarationContext;
+import org.apache.avro.idl.IdlParser.RecordBodyContext;
+import org.apache.avro.idl.IdlParser.RecordDeclarationContext;
+import org.apache.avro.idl.IdlParser.ResultTypeContext;
+import org.apache.avro.idl.IdlParser.SchemaPropertyContext;
+import org.apache.avro.idl.IdlParser.UnionTypeContext;
+import org.apache.avro.idl.IdlParser.VariableDeclarationContext;
+import org.apache.avro.util.internal.Accessor;
+import org.apache.commons.text.StringEscapeUtils;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static java.util.Collections.singleton;
+import static java.util.Collections.unmodifiableMap;
+
+public class IdlReader {
+ /**
+ * Simple error listener. Throws a runtime exception because ANTLR does not give
+ * easy access to the (reasonably readable) error message elsewhere.
+ */
+ private static final BaseErrorListener SIMPLE_AVRO_ERROR_LISTENER = new BaseErrorListener() {
+ @Override
+ public void syntaxError(Recognizer, ?> recognizer, Object offendingSymbol, int line, int charPositionInLine,
+ String msg, RecognitionException e) {
+ throw new SchemaParseException("line " + line + ":" + charPositionInLine + " " + msg);
+ }
+ };
+ private static final String OPTIONAL_NULLABLE_TYPE_PROPERTY = "org.apache.avro.idl.Idl.NullableType.optional";
+ /**
+ * Pattern to match the common whitespace indents in a multi-line String.
+ * Doesn't match a single-line String, fully matches any multi-line String.
+ *
+ * To use: match on a {@link String#trim() trimmed} String, and then replace all
+ * newlines followed by the group "indent" with a newline.
+ */
+ private static final Pattern WS_INDENT = Pattern.compile("(?U).*\\R(?\\h*).*(?:\\R\\k.*)*");
+ /**
+ * Pattern to match the whitespace indents plus common stars (1 or 2) in a
+ * multi-line String. If a String fully matches, replace all occurrences of a
+ * newline followed by whitespace and then the group "stars" with a newline.
+ *
+ * Note: partial matches are invalid.
+ */
+ private static final Pattern STAR_INDENT = Pattern.compile("(?U)(?\\*{1,2}).*(?:\\R\\h*\\k.*)*");
+ /**
+ * Predicate to check for valid names. Should probably be delegated to the
+ * Schema class.
+ */
+ private static final Predicate VALID_NAME = Pattern.compile("[_\\p{L}][_\\p{L}\\d]*").asPredicate();
+ private static final Set INVALID_TYPE_NAMES = new HashSet<>(Arrays.asList("boolean", "int", "long", "float",
+ "double", "bytes", "string", "null", "date", "time_ms", "timestamp_ms", "localtimestamp_ms", "uuid"));
+ private static final String CLASSPATH_SCHEME = "classpath";
+
+ private final Set readLocations;
+ private final Map names;
+
+ public IdlReader() {
+ readLocations = new HashSet<>();
+ names = new LinkedHashMap<>();
+ }
+
+ public Map getTypes() {
+ return unmodifiableMap(names);
+ }
+
+ private Schema namedSchemaOrUnresolved(String fullName) {
+ Schema schema = names.get(fullName);
+ if (schema == null) {
+ schema = SchemaResolver.unresolvedSchema(fullName);
+ }
+ return schema;
+ }
+
+ private void setTypes(Map types) {
+ names.clear();
+ for (Schema schema : types.values()) {
+ addSchema(schema);
+ }
+ }
+
+ public void addTypes(Map types) {
+ for (Schema schema : types.values()) {
+ addSchema(schema);
+ }
+ }
+
+ private void addSchema(Schema schema) {
+ String fullName = schema.getFullName();
+ if (names.containsKey(fullName)) {
+ throw new SchemaParseException("Can't redefine: " + fullName);
+ }
+ names.put(fullName, schema);
+ }
+
+ public IdlFile parse(Path location) throws IOException {
+ return parse(location.toUri());
+ }
+
+ IdlFile parse(URI location) throws IOException {
+ try (InputStream stream = location.toURL().openStream()) {
+ readLocations.add(location);
+ URI inputDir = location;
+ if ("jar".equals(location.getScheme())) {
+ String jarUriAsString = location.toString();
+ String pathFromJarRoot = jarUriAsString.substring(jarUriAsString.indexOf("!/") + 2);
+ inputDir = URI.create(CLASSPATH_SCHEME + ":/" + pathFromJarRoot);
+ }
+ inputDir = inputDir.resolve(".");
+
+ return parse(inputDir, CharStreams.fromStream(stream, StandardCharsets.UTF_8));
+ }
+ }
+
+ /**
+ * Parse an IDL file from a stream. This method cannot handle imports.
+ */
+ public IdlFile parse(InputStream stream) throws IOException {
+ return parse(null, CharStreams.fromStream(stream, StandardCharsets.UTF_8));
+ }
+
+ private IdlFile parse(URI inputDir, CharStream charStream) {
+ IdlLexer lexer = new IdlLexer(charStream);
+ CommonTokenStream tokenStream = new CommonTokenStream(lexer);
+
+ IdlParserListener parseListener = new IdlParserListener(inputDir, tokenStream);
+
+ IdlParser parser = new IdlParser(tokenStream);
+ parser.removeErrorListeners();
+ parser.addErrorListener(SIMPLE_AVRO_ERROR_LISTENER);
+ parser.addParseListener(parseListener);
+ parser.setTrace(false);
+ parser.setBuildParseTree(false);
+
+ // Trigger parsing.
+ parser.idlFile();
+
+ return parseListener.getIdlFile();
+ }
+
+ /* Package private to facilitate testing */
+ static String stripIndents(String docComment) {
+ Matcher starMatcher = STAR_INDENT.matcher(docComment);
+ if (starMatcher.matches()) {
+ return docComment.replaceAll("(?U)(?:^|(\\R)\\h*)\\Q" + starMatcher.group("stars") + "\\E\\h?", "$1");
+ }
+
+ Matcher whitespaceMatcher = WS_INDENT.matcher(docComment);
+ if (whitespaceMatcher.matches()) {
+ return docComment.replaceAll("(?U)(\\R)" + whitespaceMatcher.group("indent"), "$1");
+ }
+
+ return docComment;
+ }
+
+ private static SchemaParseException error(String message, Token token) {
+ return error(message, token, null);
+ }
+
+ private static SchemaParseException error(String message, Token token, Throwable cause) {
+ SchemaParseException exception = new SchemaParseException(
+ message + ", at line " + token.getLine() + ", column " + token.getCharPositionInLine());
+ if (cause != null) {
+ exception.initCause(cause);
+ }
+ return exception;
+ }
+
+ private class IdlParserListener extends IdlBaseListener {
+ private final URI inputDir;
+ private final CommonTokenStream tokenStream;
+ private int hiddenTokensProcessedIndex;
+ private final List warnings;
+
+ private IdlFile result;
+ private Protocol protocol;
+ private final Deque namespaces;
+ private final List enumSymbols;
+ private String enumDefaultSymbol;
+ private Schema schema;
+ private String defaultVariableDocComment;
+ private final List fields;
+ private final Deque typeStack;
+ private final Deque jsonValues;
+ private final Deque propertiesStack;
+ private String messageDocComment;
+
+ public IdlParserListener(URI inputDir, CommonTokenStream tokenStream) {
+ this.inputDir = inputDir;
+ this.tokenStream = tokenStream;
+ hiddenTokensProcessedIndex = -1;
+ warnings = new ArrayList<>();
+
+ result = null;
+ protocol = null;
+ namespaces = new ArrayDeque<>();
+ enumSymbols = new ArrayList<>();
+ enumDefaultSymbol = null;
+ schema = null;
+ defaultVariableDocComment = null;
+ fields = new ArrayList<>();
+ typeStack = new ArrayDeque<>();
+ propertiesStack = new ArrayDeque<>();
+ jsonValues = new ArrayDeque<>();
+ messageDocComment = null;
+ }
+
+ public IdlFile getIdlFile() {
+ return result;
+ }
+
+ private String getDocComment(ParserRuleContext ctx) {
+ int newHiddenTokensProcessedIndex = ctx.start.getTokenIndex();
+ List docCommentTokens = tokenStream.getHiddenTokensToLeft(newHiddenTokensProcessedIndex, -1);
+ int searchEndIndex = newHiddenTokensProcessedIndex;
+
+ Token docCommentToken = null;
+ if (docCommentTokens != null) {
+ // There's at least one element
+ docCommentToken = docCommentTokens.get(docCommentTokens.size() - 1);
+ searchEndIndex = docCommentToken.getTokenIndex() - 1;
+ }
+
+ Set allHiddenTokens = singleton(IdlParser.DocComment);
+ if (searchEndIndex >= 0) {
+ List hiddenTokens = tokenStream.getTokens(hiddenTokensProcessedIndex + 1, searchEndIndex,
+ allHiddenTokens);
+ if (hiddenTokens != null) {
+ for (Token token : hiddenTokens) {
+ warnings.add(String.format(
+ "Line %d, char %d: Ignoring out-of-place documentation comment.%n"
+ + "Did you mean to use a multiline comment ( /* ... */ ) instead?",
+ token.getLine(), token.getCharPositionInLine() + 1));
+ }
+ }
+ }
+ hiddenTokensProcessedIndex = newHiddenTokensProcessedIndex;
+
+ if (docCommentToken == null) {
+ return null;
+ }
+ String comment = docCommentToken.getText();
+ String text = comment.substring(3, comment.length() - 2); // Strip /** & */
+ return stripIndents(text.trim());
+ }
+
+ private void pushNamespace(String namespace) {
+ namespaces.push(namespace == null ? "" : namespace);
+ }
+
+ private String currentNamespace() {
+ String namespace = namespaces.element();
+ return namespace.isEmpty() ? null : namespace;
+ }
+
+ private void popNamespace() {
+ namespaces.pop();
+ }
+
+ @Override
+ public void exitIdlFile(IdlFileContext ctx) {
+ IdlFile unresolved = new IdlFile(protocol, warnings);
+ result = SchemaResolver.resolve(unresolved, OPTIONAL_NULLABLE_TYPE_PROPERTY);
+ }
+
+ @Override
+ public void enterProtocolDeclaration(ProtocolDeclarationContext ctx) {
+ propertiesStack.push(new SchemaProperties(null, true, false, false));
+ }
+
+ @Override
+ public void enterProtocolDeclarationBody(ProtocolDeclarationBodyContext ctx) {
+ ProtocolDeclarationContext protocolCtx = (ProtocolDeclarationContext) ctx.parent;
+ SchemaProperties properties = propertiesStack.pop();
+ String protocolIdentifier = identifier(protocolCtx.name);
+ pushNamespace(namespace(protocolIdentifier, properties.namespace()));
+
+ String protocolName = name(protocolIdentifier);
+ String docComment = getDocComment(protocolCtx);
+ String protocolNamespace = currentNamespace();
+ protocol = properties.copyProperties(new Protocol(protocolName, docComment, protocolNamespace));
+ }
+
+ @Override
+ public void exitProtocolDeclaration(ProtocolDeclarationContext ctx) {
+ if (protocol != null)
+ protocol.setTypes(getTypes().values());
+ if (!namespaces.isEmpty())
+ popNamespace();
+ }
+
+ @Override
+ public void enterSchemaProperty(SchemaPropertyContext ctx) {
+ assert jsonValues.isEmpty();
+ }
+
+ @Override
+ public void exitSchemaProperty(SchemaPropertyContext ctx) {
+ String name = identifier(ctx.name);
+ JsonNode value = jsonValues.pop();
+ Token firstToken = ctx.value.start;
+
+ propertiesStack.element().addProperty(name, value, firstToken);
+ super.exitSchemaProperty(ctx);
+ }
+
+ @Override
+ public void exitImportStatement(ImportStatementContext importContext) {
+ String importFile = getString(importContext.location);
+ try {
+ URI importLocation = findImport(importFile);
+ if (!readLocations.add(importLocation)) {
+ // Already imported
+ return;
+ }
+ switch (importContext.importType.getType()) {
+ case IdlParser.IDL:
+ // Note that the parse(URI) method uses the same known schema collection
+ IdlFile idlFile = parse(importLocation);
+ if (protocol != null && idlFile.getProtocol() != null) {
+ protocol.getMessages().putAll(idlFile.getProtocol().getMessages());
+ }
+ warnings.addAll(idlFile.getWarnings(importFile));
+ break;
+ case IdlParser.Protocol:
+ try (InputStream stream = importLocation.toURL().openStream()) {
+ Protocol importProtocol = Protocol.parse(stream);
+ for (Schema s : importProtocol.getTypes()) {
+ addSchema(s);
+ }
+ if (protocol != null) {
+ protocol.getMessages().putAll(importProtocol.getMessages());
+ }
+ }
+ break;
+ case IdlParser.Schema:
+ try (InputStream stream = importLocation.toURL().openStream()) {
+ Schema.Parser parser = new Schema.Parser();
+ parser.addTypes(getTypes().values()); // inherit names
+ parser.parse(stream);
+ setTypes(parser.getTypes()); // update names
+ }
+ break;
+ }
+ } catch (IOException e) {
+ throw error("Error importing " + importFile + ": " + e, importContext.location, e);
+ }
+ }
+
+ /**
+ * Best effort guess at the import file location. For locations inside jar
+ * files, this may result in non-existing URLs.
+ */
+ private URI findImport(String importFile) throws IOException {
+ URI importLocation = inputDir.resolve(importFile);
+ String importLocationScheme = importLocation.getScheme();
+
+ if (CLASSPATH_SCHEME.equals(importLocationScheme)) {
+ String resourceName = importLocation.getSchemeSpecificPart().substring(1);
+ URI resourceLocation = findResource(resourceName);
+ if (resourceLocation != null) {
+ return resourceLocation;
+ }
+ }
+
+ if ("file".equals(importLocationScheme) && Files.exists(Paths.get(importLocation))) {
+ return importLocation;
+ }
+
+ // The importFile doesn't exist as file relative to the current file. Try to
+ // load it from the classpath.
+ URI resourceLocation = findResource(importFile);
+ if (resourceLocation != null) {
+ return resourceLocation;
+ }
+
+ // Cannot find the import.
+ throw new FileNotFoundException(importFile);
+ }
+
+ private URI findResource(String resourceName) {
+ ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
+ URL resourceLocation;
+ if (classLoader == null) {
+ resourceLocation = ClassLoader.getSystemResource(resourceName);
+ } else {
+ resourceLocation = classLoader.getResource(resourceName);
+ }
+ return resourceLocation == null ? null : URI.create(resourceLocation.toExternalForm());
+ }
+
+ @Override
+ public void enterFixedDeclaration(FixedDeclarationContext ctx) {
+ propertiesStack.push(new SchemaProperties(currentNamespace(), true, true, false));
+ }
+
+ @Override
+ public void exitFixedDeclaration(FixedDeclarationContext ctx) {
+ SchemaProperties properties = propertiesStack.pop();
+
+ String doc = getDocComment(ctx);
+ String identifier = identifier(ctx.name);
+ String name = name(identifier);
+ String space = namespace(identifier, properties.namespace());
+ int size = Integer.decode(ctx.size.getText());
+ Schema schema = Schema.createFixed(name, doc, space, size);
+ properties.copyAliases(schema::addAlias);
+ properties.copyProperties(schema);
+ addSchema(schema);
+ }
+
+ @Override
+ public void enterEnumDeclaration(EnumDeclarationContext ctx) {
+ assert enumSymbols.isEmpty();
+ assert enumDefaultSymbol == null;
+ propertiesStack.push(new SchemaProperties(currentNamespace(), true, true, false));
+ }
+
+ @Override
+ public void exitEnumDeclaration(EnumDeclarationContext ctx) {
+ String doc = getDocComment(ctx);
+ SchemaProperties properties = propertiesStack.pop();
+ String identifier = identifier(ctx.name);
+ String name = name(identifier);
+ String space = namespace(identifier, properties.namespace());
+
+ Schema schema = Schema.createEnum(name, doc, space, new ArrayList<>(enumSymbols), enumDefaultSymbol);
+ properties.copyAliases(schema::addAlias);
+ properties.copyProperties(schema);
+ enumSymbols.clear();
+ enumDefaultSymbol = null;
+
+ addSchema(schema);
+ }
+
+ @Override
+ public void enterEnumSymbol(EnumSymbolContext ctx) {
+ propertiesStack.push(new SchemaProperties(null, false, false, false));
+ }
+
+ @Override
+ public void exitEnumSymbol(EnumSymbolContext ctx) {
+ // TODO: implement doc comment & properties for enum symbols.
+ propertiesStack.pop();
+
+ enumSymbols.add(identifier(ctx.name));
+ }
+
+ @Override
+ public void exitEnumDefault(IdlParser.EnumDefaultContext ctx) {
+ enumDefaultSymbol = identifier(ctx.defaultSymbolName);
+ }
+
+ @Override
+ public void enterRecordDeclaration(RecordDeclarationContext ctx) {
+ assert schema == null;
+ assert fields.isEmpty();
+
+ propertiesStack.push(new SchemaProperties(currentNamespace(), true, true, false));
+ }
+
+ @Override
+ public void enterRecordBody(RecordBodyContext ctx) {
+ assert fields.isEmpty();
+
+ RecordDeclarationContext recordCtx = (RecordDeclarationContext) ctx.parent;
+
+ SchemaProperties properties = propertiesStack.pop();
+
+ String doc = getDocComment(recordCtx);
+ String identifier = identifier(recordCtx.name);
+ String name = name(identifier);
+ pushNamespace(namespace(identifier, properties.namespace()));
+ boolean isError = recordCtx.recordType.getType() == IdlParser.Error;
+ schema = Schema.createRecord(name, doc, currentNamespace(), isError);
+ properties.copyAliases(schema::addAlias);
+ properties.copyProperties(schema);
+ }
+
+ @Override
+ public void exitRecordDeclaration(RecordDeclarationContext ctx) {
+ schema.setFields(fields);
+ fields.clear();
+ addSchema(schema);
+ schema = null;
+
+ popNamespace();
+ }
+
+ @Override
+ public void enterFieldDeclaration(FieldDeclarationContext ctx) {
+ assert typeStack.isEmpty();
+ defaultVariableDocComment = getDocComment(ctx);
+ }
+
+ @Override
+ public void exitFieldDeclaration(FieldDeclarationContext ctx) {
+ typeStack.pop();
+ defaultVariableDocComment = null;
+ }
+
+ @Override
+ public void enterVariableDeclaration(VariableDeclarationContext ctx) {
+ assert jsonValues.isEmpty();
+ propertiesStack.push(new SchemaProperties(currentNamespace(), false, true, true));
+ }
+
+ @Override
+ public void exitVariableDeclaration(VariableDeclarationContext ctx) {
+ String doc = Optional.ofNullable(getDocComment(ctx)).orElse(defaultVariableDocComment);
+ String fieldName = identifier(ctx.fieldName);
+
+ JsonNode defaultValue = jsonValues.poll();
+ Schema type = typeStack.element();
+ JsonNode fieldDefault = fixDefaultValue(defaultValue, type);
+ Schema fieldType = fixOptionalSchema(type, fieldDefault);
+
+ SchemaProperties properties = propertiesStack.pop();
+
+ boolean validate = SchemaResolver.isFullyResolvedSchema(fieldType);
+ Schema.Field field = Accessor.createField(fieldName, fieldType, doc, fieldDefault, validate, properties.order());
+ properties.copyAliases(field::addAlias);
+ properties.copyProperties(field);
+ fields.add(field);
+ }
+
+ /**
+ * When parsing JSON, the parser generates a LongNode or IntNode based on the
+ * size of the number it encounters. But this may not be expected based on the
+ * schema. This method fixes that.
+ *
+ * @param defaultValue the parsed default value
+ * @param fieldType the field schema
+ * @return the default value, now matching the schema
+ */
+ private JsonNode fixDefaultValue(JsonNode defaultValue, Schema fieldType) {
+ if (!(defaultValue instanceof IntNode)) {
+ return defaultValue;
+ }
+
+ if (fieldType.getType() == Schema.Type.UNION) {
+ for (Schema unionedType : fieldType.getTypes()) {
+ if (unionedType.getType() == Schema.Type.INT) {
+ break;
+ } else if (unionedType.getType() == Schema.Type.LONG) {
+ return new LongNode(defaultValue.longValue());
+ }
+ }
+ return defaultValue;
+ }
+
+ if (fieldType.getType() == Schema.Type.LONG) {
+ return new LongNode(defaultValue.longValue());
+ }
+
+ return defaultValue;
+ }
+
+ /**
+ * For "optional schemas" (recognized by the marker property the NullableType
+ * production adds), ensure the null schema is in the right place.
+ *
+ * @param schema a schema
+ * @param defaultValue the intended default value
+ * @return the schema, or an optional schema with null in the right place
+ */
+ private Schema fixOptionalSchema(Schema schema, JsonNode defaultValue) {
+ Object optionalType = schema.getObjectProp(OPTIONAL_NULLABLE_TYPE_PROPERTY);
+ if (optionalType != null) {
+ // The schema is a union schema with 2 types: "null" and a non-"null" schema
+ Schema nullSchema = schema.getTypes().get(0);
+ Schema nonNullSchema = schema.getTypes().get(1);
+ boolean nonNullDefault = defaultValue != null && !defaultValue.isNull();
+
+ // Note: the resolving visitor we'll use later drops the marker property.
+ if (nonNullDefault) {
+ return Schema.createUnion(nonNullSchema, nullSchema);
+ }
+ }
+ return schema;
+ }
+
+ @Override
+ public void enterMessageDeclaration(MessageDeclarationContext ctx) {
+ assert typeStack.isEmpty();
+ assert fields.isEmpty();
+ assert messageDocComment == null;
+ propertiesStack.push(new SchemaProperties(currentNamespace(), false, false, false));
+ messageDocComment = getDocComment(ctx);
+ }
+
+ @Override
+ public void exitMessageDeclaration(MessageDeclarationContext ctx) {
+ Schema resultType = typeStack.pop();
+ Map properties = propertiesStack.pop().properties;
+ String name = identifier(ctx.name);
+
+ Schema request = Schema.createRecord(null, null, null, false, fields);
+ fields.clear();
+
+ Protocol.Message message;
+ if (ctx.oneway != null) {
+ if (resultType.getType() == Schema.Type.NULL) {
+ message = protocol.createMessage(name, messageDocComment, properties, request);
+ } else {
+ throw error("One-way message'" + name + "' must return void", ctx.returnType.start);
+ }
+ } else {
+ List errorSchemas = new ArrayList<>();
+ errorSchemas.add(Protocol.SYSTEM_ERROR);
+ for (IdentifierContext errorContext : ctx.errors) {
+ errorSchemas.add(namedSchemaOrUnresolved(fullName(currentNamespace(), identifier(errorContext))));
+ }
+ message = protocol.createMessage(name, messageDocComment, properties, request, resultType,
+ Schema.createUnion(errorSchemas));
+ }
+ messageDocComment = null;
+ protocol.getMessages().put(message.getName(), message);
+ }
+
+ @Override
+ public void enterFormalParameter(FormalParameterContext ctx) {
+ assert typeStack.size() == 1; // The message return type is on the stack; nothing else.
+ defaultVariableDocComment = getDocComment(ctx);
+ }
+
+ @Override
+ public void exitFormalParameter(FormalParameterContext ctx) {
+ typeStack.pop();
+ defaultVariableDocComment = null;
+ }
+
+ @Override
+ public void exitResultType(ResultTypeContext ctx) {
+ if (typeStack.isEmpty()) {
+ // if there's no type, we've parsed 'void': use the null type
+ typeStack.push(Schema.create(Schema.Type.NULL));
+ }
+ }
+
+ @Override
+ public void enterFullType(FullTypeContext ctx) {
+ propertiesStack.push(new SchemaProperties(currentNamespace(), false, false, false));
+ }
+
+ @Override
+ public void exitFullType(FullTypeContext ctx) {
+ SchemaProperties properties = propertiesStack.pop();
+
+ Schema type = typeStack.element();
+ if (type.getObjectProp(OPTIONAL_NULLABLE_TYPE_PROPERTY) != null) {
+ // Optional type: put the properties on the non-null content
+ properties.copyProperties(type.getTypes().get(1));
+ } else {
+ properties.copyProperties(type);
+ }
+ }
+
+ @Override
+ public void exitNullableType(NullableTypeContext ctx) {
+ Schema type;
+ if (ctx.referenceName == null) {
+ type = typeStack.pop();
+ } else {
+ // propertiesStack is empty within resultType->plainType->nullableType, and
+ // holds our properties otherwise
+ if (propertiesStack.isEmpty() || propertiesStack.peek().hasProperties()) {
+ throw error("Type references may not be annotated", ctx.getParent().getStart());
+ }
+ type = namedSchemaOrUnresolved(fullName(currentNamespace(), identifier(ctx.referenceName)));
+ }
+ if (ctx.optional != null) {
+ type = Schema.createUnion(Schema.create(Schema.Type.NULL), type);
+ // Add a marker property to the union (it will be removed when creating fields)
+ type.addProp(OPTIONAL_NULLABLE_TYPE_PROPERTY, BooleanNode.TRUE);
+ }
+ typeStack.push(type);
+ }
+
+ @Override
+ public void exitPrimitiveType(PrimitiveTypeContext ctx) {
+ switch (ctx.typeName.getType()) {
+ case IdlParser.Boolean:
+ typeStack.push(Schema.create(Schema.Type.BOOLEAN));
+ break;
+ case IdlParser.Int:
+ typeStack.push(Schema.create(Schema.Type.INT));
+ break;
+ case IdlParser.Long:
+ typeStack.push(Schema.create(Schema.Type.LONG));
+ break;
+ case IdlParser.Float:
+ typeStack.push(Schema.create(Schema.Type.FLOAT));
+ break;
+ case IdlParser.Double:
+ typeStack.push(Schema.create(Schema.Type.DOUBLE));
+ break;
+ case IdlParser.Bytes:
+ typeStack.push(Schema.create(Schema.Type.BYTES));
+ break;
+ case IdlParser.String:
+ typeStack.push(Schema.create(Schema.Type.STRING));
+ break;
+ case IdlParser.Null:
+ typeStack.push(Schema.create(Schema.Type.NULL));
+ break;
+ case IdlParser.Date:
+ typeStack.push(LogicalTypes.date().addToSchema(Schema.create(Schema.Type.INT)));
+ break;
+ case IdlParser.Time:
+ typeStack.push(LogicalTypes.timeMillis().addToSchema(Schema.create(Schema.Type.INT)));
+ break;
+ case IdlParser.Timestamp:
+ typeStack.push(LogicalTypes.timestampMillis().addToSchema(Schema.create(Schema.Type.LONG)));
+ break;
+ case IdlParser.LocalTimestamp:
+ typeStack.push(LogicalTypes.localTimestampMillis().addToSchema(Schema.create(Schema.Type.LONG)));
+ break;
+ case IdlParser.UUID:
+ typeStack.push(LogicalTypes.uuid().addToSchema(Schema.create(Schema.Type.STRING)));
+ break;
+ default: // Only option left: decimal
+ int precision = Integer.decode(ctx.precision.getText());
+ int scale = ctx.scale == null ? 0 : Integer.decode(ctx.scale.getText());
+ typeStack.push(LogicalTypes.decimal(precision, scale).addToSchema(Schema.create(Schema.Type.BYTES)));
+ break;
+ }
+ }
+
+ @Override
+ public void exitArrayType(ArrayTypeContext ctx) {
+ typeStack.push(Schema.createArray(typeStack.pop()));
+ }
+
+ @Override
+ public void exitMapType(MapTypeContext ctx) {
+ typeStack.push(Schema.createMap(typeStack.pop()));
+ }
+
+ @Override
+ public void enterUnionType(UnionTypeContext ctx) {
+ // push an empty marker union; we'll replace it with the real union upon exit
+ typeStack.push(Schema.createUnion());
+ }
+
+ @Override
+ public void exitUnionType(UnionTypeContext ctx) {
+ List types = new ArrayList<>();
+ Schema type;
+ while ((type = typeStack.pop()).getType() != Schema.Type.UNION) {
+ types.add(type);
+ }
+ Collections.reverse(types); // Popping the stack works in reverse order
+ // type is an empty marker union; ignore (drop) it
+ typeStack.push(Schema.createUnion(types));
+ }
+
+ @Override
+ public void exitJsonValue(JsonValueContext ctx) {
+ if (ctx.parent instanceof JsonArrayContext) {
+ JsonNode value = jsonValues.pop();
+ assert jsonValues.peek() instanceof ArrayNode;
+ ((ArrayNode) jsonValues.element()).add(value);
+ }
+ }
+
+ @Override
+ public void exitJsonLiteral(JsonLiteralContext ctx) {
+ Token literal = ctx.literal;
+ switch (literal.getType()) {
+ case IdlParser.Null:
+ jsonValues.push(NullNode.getInstance());
+ break;
+ case IdlParser.BTrue:
+ jsonValues.push(BooleanNode.TRUE);
+ break;
+ case IdlParser.BFalse:
+ jsonValues.push(BooleanNode.FALSE);
+ break;
+ case IdlParser.IntegerLiteral:
+ String number = literal.getText().replace("_", "");
+ char lastChar = number.charAt(number.length() - 1);
+ boolean coerceToLong = false;
+ if (lastChar == 'l' || lastChar == 'L') {
+ coerceToLong = true;
+ number = number.substring(0, number.length() - 1);
+ }
+ long longNumber = Long.decode(number);
+ int intNumber = (int) longNumber; // Narrowing cast: if too large a number, the two are different
+ jsonValues.push(coerceToLong || intNumber != longNumber ? new LongNode(longNumber) : new IntNode(intNumber));
+ break;
+ case IdlParser.FloatingPointLiteral:
+ jsonValues.push(new DoubleNode(Double.parseDouble(literal.getText())));
+ break;
+ default: // StringLiteral:
+ jsonValues.push(new TextNode(getString(literal)));
+ break;
+ }
+ }
+
+ @Override
+ public void enterJsonArray(JsonArrayContext ctx) {
+ jsonValues.push(new ArrayNode(null));
+ }
+
+ @Override
+ public void enterJsonObject(JsonObjectContext ctx) {
+ jsonValues.push(new ObjectNode(null));
+ }
+
+ @Override
+ public void exitJsonPair(JsonPairContext ctx) {
+ String name = getString(ctx.name);
+ JsonNode value = jsonValues.pop();
+ assert jsonValues.peek() instanceof ObjectNode;
+ ((ObjectNode) jsonValues.element()).set(name, value);
+ }
+
+ private String identifier(IdentifierContext ctx) {
+ return ctx.word.getText().replace("`", "");
+ }
+
+ private String name(String identifier) {
+ int dotPos = identifier.lastIndexOf('.');
+ String name = identifier.substring(dotPos + 1);
+ return validateName(name, true);
+ }
+
+ private String namespace(String identifier, String namespace) {
+ int dotPos = identifier.lastIndexOf('.');
+ String ns = dotPos < 0 ? namespace : identifier.substring(0, dotPos);
+ if (ns == null) {
+ return null;
+ }
+ for (int s = 0, e = ns.indexOf('.'); e > 0; s = e + 1, e = ns.indexOf('.', s)) {
+ validateName(ns.substring(s, e), false);
+ }
+ return ns;
+ }
+
+ private String validateName(String name, boolean isTypeName) {
+ if (name == null) {
+ throw new SchemaParseException("Null name");
+ } else if (!VALID_NAME.test(name)) {
+ throw new SchemaParseException("Illegal name: " + name);
+ }
+ if (isTypeName && INVALID_TYPE_NAMES.contains(name)) {
+ throw new SchemaParseException("Illegal name: " + name);
+ }
+ return name;
+ }
+
+ private String fullName(String namespace, String typeName) {
+ int dotPos = typeName.lastIndexOf('.');
+ if (dotPos > -1) {
+ return typeName;
+ }
+ return namespace != null ? namespace + "." + typeName : typeName;
+ }
+
+ private String getString(Token stringToken) {
+ String stringLiteral = stringToken.getText();
+ String betweenQuotes = stringLiteral.substring(1, stringLiteral.length() - 1);
+ return StringEscapeUtils.unescapeJava(betweenQuotes);
+ }
+ }
+
+ private static class SchemaProperties {
+ String contextNamespace;
+ boolean withNamespace;
+ String namespace;
+ boolean withAliases;
+ List aliases;
+ boolean withOrder;
+ Schema.Field.Order order;
+ Map properties;
+
+ public SchemaProperties(String contextNamespace, boolean withNamespace, boolean withAliases, boolean withOrder) {
+ this.contextNamespace = contextNamespace;
+ this.withNamespace = withNamespace;
+ this.withAliases = withAliases;
+ this.aliases = Collections.emptyList();
+ this.withOrder = withOrder;
+ this.order = Schema.Field.Order.ASCENDING;
+ this.properties = new LinkedHashMap<>();
+ }
+
+ public void addProperty(String name, JsonNode value, Token firstValueToken) {
+ if (withNamespace && "namespace".equals(name)) {
+ if (value.isTextual()) {
+ namespace = value.textValue();
+ } else {
+ throw error("@namespace(...) must contain a String value", firstValueToken);
+ }
+ } else if (withAliases && "aliases".equals(name)) {
+ if (value.isArray()) {
+ List result = new ArrayList<>();
+ Iterator elements = value.elements();
+ elements.forEachRemaining(element -> {
+ if (element.isTextual()) {
+ result.add(element.textValue());
+ } else {
+ throw error("@aliases(...) must contain an array of String values", firstValueToken);
+ }
+ });
+ aliases = result;
+ } else {
+ throw error("@aliases(...) must contain an array of String values", firstValueToken);
+ }
+ } else if (withOrder && "order".equals(name)) {
+ if (value.isTextual()) {
+ String orderValue = value.textValue().toUpperCase(Locale.ROOT);
+ switch (orderValue) {
+ case "ASCENDING":
+ order = Schema.Field.Order.ASCENDING;
+ break;
+ case "DESCENDING":
+ order = Schema.Field.Order.DESCENDING;
+ break;
+ case "IGNORE":
+ order = Schema.Field.Order.IGNORE;
+ break;
+ default:
+ throw error("@order(...) must contain \"ASCENDING\", \"DESCENDING\" or \"IGNORE\"", firstValueToken);
+ }
+ } else {
+ throw error("@order(...) must contain a String value", firstValueToken);
+ }
+ } else {
+ properties.put(name, value);
+ }
+ }
+
+ public String namespace() {
+ return namespace == null ? contextNamespace : namespace;
+ }
+
+ public Schema.Field.Order order() {
+ return order;
+ }
+
+ public void copyAliases(Consumer addAlias) {
+ aliases.forEach(addAlias);
+ }
+
+ public T copyProperties(T jsonProperties) {
+ properties.forEach(jsonProperties::addProp);
+ if (jsonProperties instanceof Schema) {
+ Schema schema = (Schema) jsonProperties;
+ LogicalType logicalType = LogicalTypes.fromSchemaIgnoreInvalid(schema);
+ if (logicalType != null) {
+ logicalType.addToSchema(schema);
+ }
+ }
+ return jsonProperties;
+ }
+
+ public boolean hasProperties() {
+ return !properties.isEmpty();
+ }
+ }
+}
diff --git a/lang/java/idl/src/main/java/org/apache/avro/idl/IsResolvedSchemaVisitor.java b/lang/java/idl/src/main/java/org/apache/avro/idl/IsResolvedSchemaVisitor.java
new file mode 100644
index 00000000000..12fd5dbff21
--- /dev/null
+++ b/lang/java/idl/src/main/java/org/apache/avro/idl/IsResolvedSchemaVisitor.java
@@ -0,0 +1,60 @@
+/*
+ * 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
+ *
+ * https://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.
+ */
+package org.apache.avro.idl;
+
+import org.apache.avro.Schema;
+
+/**
+ * This visitor checks if the current schema is fully resolved.
+ */
+public final class IsResolvedSchemaVisitor implements SchemaVisitor {
+ boolean hasUnresolvedParts;
+
+ IsResolvedSchemaVisitor() {
+ hasUnresolvedParts = false;
+ }
+
+ @Override
+ public SchemaVisitorAction visitTerminal(Schema terminal) {
+ hasUnresolvedParts = SchemaResolver.isUnresolvedSchema(terminal);
+ return hasUnresolvedParts ? SchemaVisitorAction.TERMINATE : SchemaVisitorAction.CONTINUE;
+ }
+
+ @Override
+ public SchemaVisitorAction visitNonTerminal(Schema nonTerminal) {
+ hasUnresolvedParts = SchemaResolver.isUnresolvedSchema(nonTerminal);
+ if (hasUnresolvedParts) {
+ return SchemaVisitorAction.TERMINATE;
+ }
+ if (nonTerminal.getType() == Schema.Type.RECORD && !nonTerminal.hasFields()) {
+ // We're still initializing the type...
+ return SchemaVisitorAction.SKIP_SUBTREE;
+ }
+ return SchemaVisitorAction.CONTINUE;
+ }
+
+ @Override
+ public SchemaVisitorAction afterVisitNonTerminal(Schema nonTerminal) {
+ return SchemaVisitorAction.CONTINUE;
+ }
+
+ @Override
+ public Boolean get() {
+ return !hasUnresolvedParts;
+ }
+}
diff --git a/lang/java/idl/src/main/java/org/apache/avro/idl/ResolvingVisitor.java b/lang/java/idl/src/main/java/org/apache/avro/idl/ResolvingVisitor.java
new file mode 100644
index 00000000000..04e41f3403a
--- /dev/null
+++ b/lang/java/idl/src/main/java/org/apache/avro/idl/ResolvingVisitor.java
@@ -0,0 +1,192 @@
+/*
+ * 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
+ *
+ * https://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.
+ */
+package org.apache.avro.idl;
+
+import org.apache.avro.AvroTypeException;
+import org.apache.avro.Schema;
+import org.apache.avro.Schema.Field;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.EnumSet;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
+
+import static org.apache.avro.Schema.Type.ARRAY;
+import static org.apache.avro.Schema.Type.ENUM;
+import static org.apache.avro.Schema.Type.FIXED;
+import static org.apache.avro.Schema.Type.MAP;
+import static org.apache.avro.Schema.Type.RECORD;
+import static org.apache.avro.Schema.Type.UNION;
+
+/**
+ * This visitor creates clone of the visited Schemata, minus the specified
+ * schema properties, and resolves all unresolved schemas.
+ */
+public final class ResolvingVisitor implements SchemaVisitor {
+ private static final Set CONTAINER_SCHEMA_TYPES = EnumSet.of(RECORD, ARRAY, MAP, UNION);
+ private static final Set NAMED_SCHEMA_TYPES = EnumSet.of(RECORD, ENUM, FIXED);
+
+ private final Function symbolTable;
+ private final Set schemaPropertiesToRemove;
+ private final IdentityHashMap replace;
+
+ private final Schema root;
+
+ public ResolvingVisitor(final Schema root, final Function symbolTable,
+ String... schemaPropertiesToRemove) {
+ this(root, symbolTable, new HashSet<>(Arrays.asList(schemaPropertiesToRemove)));
+ }
+
+ public ResolvingVisitor(final Schema root, final Function symbolTable,
+ Set schemaPropertiesToRemove) {
+ this.replace = new IdentityHashMap<>();
+ this.symbolTable = symbolTable;
+ this.schemaPropertiesToRemove = schemaPropertiesToRemove;
+
+ this.root = root;
+ }
+
+ public ResolvingVisitor withRoot(Schema root) {
+ return new ResolvingVisitor(root, symbolTable, schemaPropertiesToRemove);
+ }
+
+ @Override
+ public SchemaVisitorAction visitTerminal(final Schema terminal) {
+ Schema.Type type = terminal.getType();
+ Schema newSchema;
+ if (CONTAINER_SCHEMA_TYPES.contains(type)) {
+ if (!replace.containsKey(terminal)) {
+ throw new IllegalStateException("Schema " + terminal + " must be already processed");
+ }
+ return SchemaVisitorAction.CONTINUE;
+ } else if (type == ENUM) {
+ newSchema = Schema.createEnum(terminal.getName(), terminal.getDoc(), terminal.getNamespace(),
+ terminal.getEnumSymbols(), terminal.getEnumDefault());
+ } else if (type == FIXED) {
+ newSchema = Schema.createFixed(terminal.getName(), terminal.getDoc(), terminal.getNamespace(),
+ terminal.getFixedSize());
+ } else {
+ newSchema = Schema.create(type);
+ }
+ copyProperties(terminal, newSchema);
+ replace.put(terminal, newSchema);
+ return SchemaVisitorAction.CONTINUE;
+ }
+
+ public void copyProperties(final Schema first, final Schema second) {
+ // Logical type
+ Optional.ofNullable(first.getLogicalType()).ifPresent(logicalType -> logicalType.addToSchema(second));
+
+ // Aliases (if applicable)
+ if (NAMED_SCHEMA_TYPES.contains(first.getType())) {
+ first.getAliases().forEach(second::addAlias);
+ }
+
+ // Other properties
+ first.getObjectProps().forEach((name, value) -> {
+ if (!schemaPropertiesToRemove.contains(name)) {
+ second.addProp(name, value);
+ }
+ });
+ }
+
+ @Override
+ public SchemaVisitorAction visitNonTerminal(final Schema nt) {
+ Schema.Type type = nt.getType();
+ if (type == RECORD) {
+ if (SchemaResolver.isUnresolvedSchema(nt)) {
+ // unresolved schema will get a replacement that we already encountered,
+ // or we will attempt to resolve.
+ final String unresolvedSchemaName = SchemaResolver.getUnresolvedSchemaName(nt);
+ Schema resSchema = symbolTable.apply(unresolvedSchemaName);
+ if (resSchema == null) {
+ throw new AvroTypeException("Unable to resolve " + unresolvedSchemaName);
+ }
+ Schema replacement = replace.computeIfAbsent(resSchema, schema -> {
+ Schemas.visit(schema, this);
+ return replace.get(schema);
+ });
+ replace.put(nt, replacement);
+ } else {
+ // create a fieldless clone. Fields will be added in afterVisitNonTerminal.
+ Schema newSchema = Schema.createRecord(nt.getName(), nt.getDoc(), nt.getNamespace(), nt.isError());
+ copyProperties(nt, newSchema);
+ replace.put(nt, newSchema);
+ }
+ }
+ return SchemaVisitorAction.CONTINUE;
+ }
+
+ @Override
+ public SchemaVisitorAction afterVisitNonTerminal(final Schema nt) {
+ Schema.Type type = nt.getType();
+ Schema newSchema;
+ switch (type) {
+ case RECORD:
+ if (!SchemaResolver.isUnresolvedSchema(nt)) {
+ newSchema = replace.get(nt);
+ // Check if we've already handled the replacement schema with a
+ // reentrant call to visit(...) from within the visitor.
+ if (!newSchema.hasFields()) {
+ List fields = nt.getFields();
+ List newFields = new ArrayList<>(fields.size());
+ for (Schema.Field field : fields) {
+ newFields.add(new Field(field, replace.get(field.schema())));
+ }
+ newSchema.setFields(newFields);
+ }
+ }
+ return SchemaVisitorAction.CONTINUE;
+ case UNION:
+ List types = nt.getTypes();
+ List newTypes = new ArrayList<>(types.size());
+ for (Schema sch : types) {
+ newTypes.add(replace.get(sch));
+ }
+ newSchema = Schema.createUnion(newTypes);
+ break;
+ case ARRAY:
+ newSchema = Schema.createArray(replace.get(nt.getElementType()));
+ break;
+ case MAP:
+ newSchema = Schema.createMap(replace.get(nt.getValueType()));
+ break;
+ default:
+ throw new IllegalStateException("Illegal type " + type + ", schema " + nt);
+ }
+ copyProperties(nt, newSchema);
+ replace.put(nt, newSchema);
+ return SchemaVisitorAction.CONTINUE;
+ }
+
+ @Override
+ public Schema get() {
+ return replace.get(root);
+ }
+
+ @Override
+ public String toString() {
+ return "ResolvingVisitor{symbolTable=" + symbolTable + ", schemaPropertiesToRemove=" + schemaPropertiesToRemove
+ + ", replace=" + replace + '}';
+ }
+}
diff --git a/lang/java/idl/src/main/java/org/apache/avro/idl/SchemaResolver.java b/lang/java/idl/src/main/java/org/apache/avro/idl/SchemaResolver.java
new file mode 100644
index 00000000000..3130f5a2651
--- /dev/null
+++ b/lang/java/idl/src/main/java/org/apache/avro/idl/SchemaResolver.java
@@ -0,0 +1,135 @@
+/*
+ * 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
+ *
+ * https://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.
+ */
+package org.apache.avro.idl;
+
+import org.apache.avro.JsonProperties;
+import org.apache.avro.Protocol;
+import org.apache.avro.Schema;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * Utility class to resolve schemas that are unavailable at the point they are
+ * referenced in the IDL.
+ */
+final class SchemaResolver {
+
+ private SchemaResolver() {
+ }
+
+ private static final String UR_SCHEMA_ATTR = "org.apache.avro.idl.unresolved.name";
+
+ private static final String UR_SCHEMA_NAME = "UnresolvedSchema";
+
+ private static final String UR_SCHEMA_NS = "org.apache.avro.compiler";
+
+ /**
+ * Create a schema to represent an "unresolved" schema. (used to represent a
+ * schema whose definition does not exist, yet).
+ *
+ * @param name a schema name
+ * @return an unresolved schema for the given name
+ */
+ static Schema unresolvedSchema(final String name) {
+ Schema schema = Schema.createRecord(UR_SCHEMA_NAME, "unresolved schema", UR_SCHEMA_NS, false,
+ Collections.emptyList());
+ schema.addProp(UR_SCHEMA_ATTR, name);
+ return schema;
+ }
+
+ /**
+ * Is this an unresolved schema.
+ *
+ * @param schema a schema
+ * @return whether the schema is an unresolved schema
+ */
+ static boolean isUnresolvedSchema(final Schema schema) {
+ return (schema.getType() == Schema.Type.RECORD && schema.getProp(UR_SCHEMA_ATTR) != null
+ && UR_SCHEMA_NAME.equals(schema.getName()) && UR_SCHEMA_NS.equals(schema.getNamespace()));
+ }
+
+ /**
+ * Get the unresolved schema name.
+ *
+ * @param schema an unresolved schema
+ * @return the name of the unresolved schema
+ */
+ static String getUnresolvedSchemaName(final Schema schema) {
+ if (!isUnresolvedSchema(schema)) {
+ throw new IllegalArgumentException("Not a unresolved schema: " + schema);
+ }
+ return schema.getProp(UR_SCHEMA_ATTR);
+ }
+
+ /**
+ * Is this an unresolved schema?
+ */
+ static boolean isFullyResolvedSchema(final Schema schema) {
+ if (isUnresolvedSchema(schema)) {
+ return false;
+ } else {
+ return Schemas.visit(schema, new IsResolvedSchemaVisitor());
+ }
+ }
+
+ /**
+ * Clone all provided schemas while resolving all unreferenced schemas.
+ *
+ * @param idlFile a parsed IDL file
+ * @return a copy of idlFile with all schemas resolved
+ */
+ static IdlFile resolve(final IdlFile idlFile, String... schemaPropertiesToRemove) {
+ return new IdlFile(resolve(idlFile.getProtocol(), schemaPropertiesToRemove), idlFile.getWarnings());
+ }
+
+ /**
+ * Will clone the provided protocol while resolving all unreferenced schemas
+ *
+ * @param protocol a parsed protocol
+ * @return a copy of the protocol with all schemas resolved
+ */
+ static Protocol resolve(final Protocol protocol, String... schemaPropertiesToRemove) {
+ // Create an empty copy of the protocol
+ Protocol result = new Protocol(protocol.getName(), protocol.getDoc(), protocol.getNamespace());
+ protocol.getObjectProps().forEach(((JsonProperties) result)::addProp);
+
+ ResolvingVisitor visitor = new ResolvingVisitor(null, protocol::getType, schemaPropertiesToRemove);
+ Function resolver = schema -> Schemas.visit(schema, visitor.withRoot(schema));
+
+ // Resolve all schemata in the protocol.
+ result.setTypes(protocol.getTypes().stream().map(resolver).collect(Collectors.toList()));
+ Map resultMessages = result.getMessages();
+ protocol.getMessages().forEach((name, oldValue) -> {
+ Protocol.Message newValue;
+ if (oldValue.isOneWay()) {
+ newValue = result.createMessage(oldValue.getName(), oldValue.getDoc(), oldValue,
+ resolver.apply(oldValue.getRequest()));
+ } else {
+ Schema request = resolver.apply(oldValue.getRequest());
+ Schema response = resolver.apply(oldValue.getResponse());
+ Schema errors = resolver.apply(oldValue.getErrors());
+ newValue = result.createMessage(oldValue.getName(), oldValue.getDoc(), oldValue, request, response, errors);
+ }
+ resultMessages.put(name, newValue);
+ });
+ return result;
+ }
+}
diff --git a/lang/java/idl/src/main/java/org/apache/avro/idl/SchemaVisitor.java b/lang/java/idl/src/main/java/org/apache/avro/idl/SchemaVisitor.java
new file mode 100644
index 00000000000..0f9fcae5b68
--- /dev/null
+++ b/lang/java/idl/src/main/java/org/apache/avro/idl/SchemaVisitor.java
@@ -0,0 +1,47 @@
+/*
+ * 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
+ *
+ * https://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.
+ */
+package org.apache.avro.idl;
+
+import org.apache.avro.Schema;
+
+public interface SchemaVisitor {
+
+ /**
+ * Invoked for schemas that do not have "child" schemas (like string, int …) or
+ * for a previously encountered schema with children, which will be treated as a
+ * terminal. (to avoid circular recursion)
+ */
+ SchemaVisitorAction visitTerminal(Schema terminal);
+
+ /**
+ * Invoked for schema with children before proceeding to visit the children.
+ */
+ SchemaVisitorAction visitNonTerminal(Schema nonTerminal);
+
+ /**
+ * Invoked for schemas with children after its children have been visited.
+ */
+ SchemaVisitorAction afterVisitNonTerminal(Schema nonTerminal);
+
+ /**
+ * Invoked when visiting is complete.
+ *
+ * @return a value that will be returned by the visit method.
+ */
+ T get();
+}
diff --git a/lang/java/idl/src/main/java/org/apache/avro/idl/SchemaVisitorAction.java b/lang/java/idl/src/main/java/org/apache/avro/idl/SchemaVisitorAction.java
new file mode 100644
index 00000000000..6aed09b3d32
--- /dev/null
+++ b/lang/java/idl/src/main/java/org/apache/avro/idl/SchemaVisitorAction.java
@@ -0,0 +1,40 @@
+/*
+ * 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
+ *
+ * https://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.
+ */
+package org.apache.avro.idl;
+
+public enum SchemaVisitorAction {
+
+ /**
+ * continue visit.
+ */
+ CONTINUE,
+ /**
+ * terminate visit.
+ */
+ TERMINATE,
+ /**
+ * when returned from pre non terminal visit method the children of the non
+ * terminal are skipped. afterVisitNonTerminal for the current schema will not
+ * be invoked.
+ */
+ SKIP_SUBTREE,
+ /**
+ * Skip visiting the siblings of this schema.
+ */
+ SKIP_SIBLINGS
+}
diff --git a/lang/java/idl/src/main/java/org/apache/avro/idl/Schemas.java b/lang/java/idl/src/main/java/org/apache/avro/idl/Schemas.java
new file mode 100644
index 00000000000..da4b949d2bc
--- /dev/null
+++ b/lang/java/idl/src/main/java/org/apache/avro/idl/Schemas.java
@@ -0,0 +1,150 @@
+/*
+ * 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
+ *
+ * https://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.
+ */
+package org.apache.avro.idl;
+
+import org.apache.avro.Schema;
+import org.apache.avro.Schema.Field;
+
+import java.util.ArrayDeque;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.IdentityHashMap;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+/**
+ * Avro Schema utilities, to traverse...
+ */
+public final class Schemas {
+
+ private Schemas() {
+ }
+
+ /**
+ * Depth first visit.
+ */
+ public static T visit(final Schema start, final SchemaVisitor visitor) {
+ // Set of Visited Schemas
+ IdentityHashMap visited = new IdentityHashMap<>();
+ // Stack that contains the Schemas to process and afterVisitNonTerminal
+ // functions.
+ // Deque>>
+ // Using Either<...> has a cost we want to avoid...
+ Deque