blob: 5de773c3fe8479f66ddc26fef2e0de55e586bca9 [file] [log] [blame]
/*
* 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.
*/
package org.apache.sis.xml;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Files;
import java.nio.file.DirectoryStream;
import java.nio.file.DirectoryIteratorException;
import java.lang.reflect.Method;
import jakarta.xml.bind.annotation.XmlSchema;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlRootElement;
import org.opengis.geoapi.SchemaException;
import org.apache.sis.xml.privy.LegacyNamespaces;
/**
* Creates a file in the {@value TransformingReader#FILENAME} format.
* {@code RenameListGenerator} can be executed if ISO 19115 standards have changed.
* The format is described in the {@code readme.html} page in source code directory.
* Output format contains namespaces first, then classes, then properties. Example:
*
* <pre class="text">
* http://standards.iso.org/iso/19115/-3/cit/1.0
* CI_Address
* administrativeArea
* city
* CI_Citation
* citedResponsibleParty</pre>
*
* This class can be used as a starting point for generating a new file from scratch.
* It should not be used for updating the existing file (unless a lot of things have changed)
* because some of {@value TransformingReader#FILENAME} content have been edited by hand.
* In particular:
*
* <ul>
* <li>Current implementation lists all classes, including classes that should
* not be listed because they did not existed in previous standard.</li>
* <li>Current implementation repeats properties inherited from parent classes.
* It does not use the "<var>Child</var> : <var>Parent</var>" syntax.</li>
* </ul>
*
* For generating a new file:
*
* {@snippet lang="java" :
* public static void main(String[] args) throws Exception {
* RenameListGenerator gen = new RenameListGenerator(Path.of("/home/user/project/build/classes"));
* gen.add(Path.of("org/apache/sis/metadata/iso"));
* try (final BufferedWriter out = Files.newBufferedWriter(Path.of("MyOutputFile.lst"))) {
* gen.print(out);
* }
* }
* }
*/
public final class RenameListGenerator {
/**
* Properties in those namespaces do not have older namespaces to map from.
*/
private static final Set<String> LEGACY_NAMESPACES = Set.of(
LegacyNamespaces.GMD,
LegacyNamespaces.GMI,
LegacyNamespaces.SRV);
/**
* The {@value} string used in JAXB annotations for default names or namespaces.
*/
private static final String DEFAULT = "##default";
/**
* Root directory from which to search for classes.
*/
private final Path classRootDirectory;
/**
* The content to write. Keys in the first (outer) map are namespaces. Keys in the enclosed maps
* are class names. Keys in the enclosed set are property names.
*/
private final Map<String, Map<String, Set<String>>> content;
/**
* Creates a new {@value TransformingReader#FILENAME} generator for classes under the given directory.
* The given directory shall be the root of {@code "*.class"} files.
*
* @param classRootDirectory the root of compiled class files.
*/
public RenameListGenerator(final Path classRootDirectory) {
this.classRootDirectory = classRootDirectory;
content = new TreeMap<>();
}
/**
* Gets the namespaces, types and properties for all class files in the given directory and sub-directories.
* Those information are memorized for future listing with {@link #print(Appendable)}.
*
* @param directory the directory to scan for classes, relative to class root directory.
* @throws IOException if an error occurred while reading files or schemas.
* @throws ClassNotFoundException if an error occurred while loading a {@code "*.class"} file.
* @throws SchemaException if two properties have the same name in the same class and namespace.
*/
public void add(final Path directory) throws IOException, ClassNotFoundException, SchemaException {
try (DirectoryStream<Path> stream = Files.newDirectoryStream(classRootDirectory.resolve(directory))) {
for (Path path : stream) {
final String filename = path.getFileName().toString();
if (!filename.startsWith(".")) {
if (Files.isDirectory(path)) {
add(path);
} else if (filename.endsWith(".class")) {
path = classRootDirectory.relativize(path);
String classname = path.toString();
classname = classname.substring(0, classname.length() - 6).replace('/', '.');
add(Class.forName(classname));
}
}
}
} catch (DirectoryIteratorException e) {
throw e.getCause();
}
}
/**
* Gets the namespaces, types and properties for the given class.
* Properties defined in super-classes will be copied as if they were declared in-line.
* Those information are memorized for future listing with {@link #print(Appendable)}.
*
* @throws SchemaException if two properties have the same name in the same class and namespace.
*/
private void add(Class<?> classe) throws SchemaException {
XmlRootElement root = classe.getDeclaredAnnotation(XmlRootElement.class);
if (root != null) {
/*
* Add the following entry:
*
* http://a.namespace
* PX_AClass
* …
*
* Then list all properties below "PX_AClass". Note that the namespace may change because properties
* may be declared in different namespaces, but the class name stay the same. If the same properties
* are inherited by many classes, they will be repeated in each subclass.
*/
final String topLevelTypeName = root.name();
String classNS = namespace(classe, root.namespace());
for (;; classNS = namespace(classe, root.namespace())) {
for (final Method method : classe.getDeclaredMethods()) {
if (!method.isBridge()) {
final XmlElement xe = method.getDeclaredAnnotation(XmlElement.class);
if (xe != null) {
String namespace = xe.namespace();
if (namespace.equals(DEFAULT)) {
namespace = classNS;
}
add(namespace, topLevelTypeName, xe.name());
}
}
}
classe = classe.getSuperclass();
root = classe.getDeclaredAnnotation(XmlRootElement.class);
if (root == null) break;
}
} else {
/*
* In Apache SIS implementation, classes without JAXB annotation except on a single method are
* code lists or enumerations. Those classes have exactly one method annotated with @XmlElement,
* and that method actually gives a type, not a property (because of the way OGC/ISO wrap every
* properties in a type).
*/
XmlElement singleton = null;
for (final Method method : classe.getDeclaredMethods()) {
final XmlElement xe = method.getDeclaredAnnotation(XmlElement.class);
if (xe != null) {
if (singleton != null) return;
singleton = xe;
}
}
}
}
/**
* Returns the namespace declared on {@link XmlSchema} annotation.
* May be the namespace inherited from the package.
*/
private static String namespace(final Class<?> classe, String classNS) {
if (classNS.equals(DEFAULT)) {
classNS = classe.getPackage().getDeclaredAnnotation(XmlSchema.class).namespace();
}
return classNS;
}
/**
* Adds a property in the given class in the given namespace.
*/
private void add(final String namespace, final String typeName, final String property) throws SchemaException {
if (!LEGACY_NAMESPACES.contains(namespace)) {
if (!content.computeIfAbsent(namespace, (k) -> new TreeMap<>())
.computeIfAbsent(typeName, (k) -> new TreeSet<>())
.add(property))
{
if (typeName.equals("Integer")) return; // Exception because of GO_Integer and GO_Integer64.
throw new SchemaException(String.format("Duplicated property %s.%s in:%n%s", typeName, property, namespace));
}
}
}
/**
* Prints the {@value TransformingReader#FILENAME} file.
*
* @param out where to print the content.
* @throws IOException if an error occurred while printing the content.
*/
public void print(final Appendable out) throws IOException {
for (final Map.Entry<String, Map<String, Set<String>>> e : content.entrySet()) {
out.append(e.getKey()).append('\n'); // Namespace
for (final Map.Entry<String, Set<String>> c : e.getValue().entrySet()) {
out.append(' ').append(c.getKey()).append('\n'); // Class
for (final String p : c.getValue()) {
out.append(" ").append(p).append('\n'); // Property
}
}
}
}
}