All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sun.tools.xjc.Options Maven / Gradle / Ivy

Go to download

JAXB Binding Compiler. Contains source code needed for binding customization files into java sources. In other words: the *tool* to generate java classes for the given xml representation.

There is a newer version: 4.0.5
Show newest version
/*
 * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Distribution License v. 1.0, which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */

package com.sun.tools.xjc;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.Charset;
import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.StandardCharsets;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.sun.codemodel.CodeWriter;
import com.sun.codemodel.JPackage;
import com.sun.codemodel.JResourceFile;
import com.sun.codemodel.writer.FileCodeWriter;
import com.sun.codemodel.writer.PrologCodeWriter;
import com.sun.istack.tools.DefaultAuthenticator;
import com.sun.tools.xjc.api.ClassNameAllocator;
import com.sun.tools.xjc.api.SpecVersion;
import com.sun.tools.xjc.generator.bean.field.FieldRendererFactory;
import com.sun.tools.xjc.model.Model;
import com.sun.tools.xjc.reader.Util;

import org.glassfish.jaxb.core.api.impl.NameConverter;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;

/**
 * Global options.
 *
 * 

* This class stores invocation configuration for XJC. * The configuration in this class should be abstract enough so that * it could be parsed from both command-line or Ant. */ public class Options { /** * If "-debug" is specified. */ public boolean debugMode; /** * If the "-verbose" option is specified. */ public boolean verbose; /** * If the "-quiet" option is specified. */ public boolean quiet; /** * If the -readOnly option is specified. */ public boolean readOnly; /** * No file header comment (to be more friendly with diff.) */ public boolean noFileHeader; /** * When on, fixes getter/setter generation to match the Bean Introspection API */ public boolean enableIntrospection; /** * When on, generates content property for types with multiple xs:any derived elements (which is supposed to be correct behaviour) */ public boolean contentForWildcard; /** * Encoding to be used by generated java sources, null for platform default. */ public String encoding; /** * If true XML security features when parsing XML documents will be disabled. * The default value is false. *

* Boolean * * @since 2.2.6 */ public boolean disableXmlSecurity; /** * Check the source schemas with extra scrutiny. * The exact meaning depends on the schema language. */ public boolean strictCheck = true; /** * If -explicit-annotation option is specified. *

* This generates code that works around issues specific to 1.4 runtime. */ public boolean runtime14 = false; /** * If true, try to resolve name conflicts automatically by assigning mechanical numbers. */ public boolean automaticNameConflictResolution = false; /** * strictly follow the compatibility rules and reject schemas that * contain features from App. E.2, use vendor binding extensions */ public static final int STRICT = 1; /** * loosely follow the compatibility rules and allow the use of vendor * binding extensions */ public static final int EXTENSION = 2; /** * this switch determines how carefully the compiler will follow * the compatibility rules in the spec. Either {@code STRICT} * or {@code EXTENSION}. */ public int compatibilityMode = STRICT; private static final String JAVAX = "javax.xml.bind"; private static final String JAKARTA = "jakarta.xml.bind"; private static final String JAXB_CORE = "org.glassfish.jaxb.core"; private static final String BIND = "com.sun.xml.bind"; public final Map classNameReplacer = new HashMap<>(); public boolean isExtensionMode() { return compatibilityMode == EXTENSION; } private static final Logger logger = org.glassfish.jaxb.core.Utils.getClassLogger(); /** * Generates output for the specified version of the runtime. */ public SpecVersion target = SpecVersion.LATEST; public Options() { } /** * Target directory when producing files. *

* This field is not used when XJC is driven through the XJC API. * Plugins that need to generate extra files should do so by using * {@link JPackage#addResourceFile(JResourceFile)}. */ public File targetDir = new File("."); /** * On JDK 8 an odler stores {@code CatalogResolver}, but the field * type is made to {@link EntityResolver} so that XJC can be * used even if resolver.jar is not available in the classpath. */ public EntityResolver entityResolver = null; /** * Type of input schema language. One of the {@code SCHEMA_XXX} * constants. */ private Language schemaLanguage = null; /** * The -p option that should control the default Java package that * will contain the generated code. Null if unspecified. */ public String defaultPackage = null; /** * Similar to the -p option, but this one works with a lower priority, * and customizations overrides this. Used by JAX-RPC. */ public String defaultPackage2 = null; /** * Input schema files as a list of {@link InputSource}s. */ private final List grammars = new ArrayList<>(); private final List bindFiles = new ArrayList<>(); // Proxy setting. private String proxyHost = null; private String proxyPort = null; public String proxyAuth = null; /** * {@link Plugin}s that are enabled in this compilation. */ public final List activePlugins = new ArrayList<>(); /** * All discovered {@link Plugin}s. * This is lazily parsed, so that we can take '-cp' option into account. * * @see #getAllPlugins() */ private List allPlugins; /** * Set of URIs that plug-ins recognize as extension bindings. */ public final Set pluginURIs = new HashSet<>(); /** * This allocator has the final say on deciding the class name. */ public ClassNameAllocator classNameAllocator; /** * This switch controls whether or not xjc will generate package level annotations */ public boolean packageLevelAnnotations = true; /** * This {@link FieldRendererFactory} determines how the fields are generated. */ private FieldRendererFactory fieldRendererFactory = new FieldRendererFactory(); /** * Used to detect if two {@link Plugin}s try to overwrite {@link #fieldRendererFactory}. */ private Plugin fieldRendererFactoryOwner = null; /** * If this is non-null, we use this {@link NameConverter} over the one * given in the schema/binding. */ private NameConverter nameConverter = null; /** * Used to detect if two {@link Plugin}s try to overwrite {@link #nameConverter}. */ private Plugin nameConverterOwner = null; /** * Java module name in {@code module-info.java}. */ private String javaModule = null; /** * Gets the active {@link FieldRendererFactory} that shall be used to build {@link Model}. * * @return always non-null. */ public FieldRendererFactory getFieldRendererFactory() { return fieldRendererFactory; } /** * Sets the {@link FieldRendererFactory}. *

* This method is for plugins to call to set a custom {@link FieldRendererFactory}. * * @param frf The {@link FieldRendererFactory} to be installed. Must not be null. * @param owner Identifies the plugin that owns this {@link FieldRendererFactory}. * When two {@link Plugin}s try to call this method, this allows XJC * to report it as a user-friendly error message. * @throws BadCommandLineException If a conflit happens, this exception carries a user-friendly error * message, indicating a conflict. */ public void setFieldRendererFactory(FieldRendererFactory frf, Plugin owner) throws BadCommandLineException { // since this method is for plugins, make it bit more fool-proof than usual if (frf == null) throw new IllegalArgumentException(); if (fieldRendererFactoryOwner != null) { throw new BadCommandLineException( Messages.format(Messages.FIELD_RENDERER_CONFLICT, fieldRendererFactoryOwner.getOptionName(), owner.getOptionName())); } this.fieldRendererFactoryOwner = owner; this.fieldRendererFactory = frf; } /** * Gets the active {@link NameConverter} that shall be used to build {@link Model}. * * @return can be null, in which case it's up to the binding. */ public NameConverter getNameConverter() { return nameConverter; } /** * Sets the {@link NameConverter}. *

* This method is for plugins to call to set a custom {@link NameConverter}. * * @param nc The {@link NameConverter} to be installed. Must not be null. * @param owner Identifies the plugin that owns this {@link NameConverter}. * When two {@link Plugin}s try to call this method, this allows XJC * to report it as a user-friendly error message. * @throws BadCommandLineException If a conflit happens, this exception carries a user-friendly error * message, indicating a conflict. */ public void setNameConverter(NameConverter nc, Plugin owner) throws BadCommandLineException { // since this method is for plugins, make it bit more fool-proof than usual if (nc == null) throw new IllegalArgumentException(); if (nameConverter != null) { throw new BadCommandLineException( Messages.format(Messages.NAME_CONVERTER_CONFLICT, nameConverterOwner.getOptionName(), owner.getOptionName())); } this.nameConverterOwner = owner; this.nameConverter = nc; } /** * Gets all the {@link Plugin}s discovered so far. *

* A plugins are enumerated when this method is called for the first time, * by taking {@link #classpaths} into account. That means * "-cp plugin.jar" has to come before you specify options to enable it. * */ public List getAllPlugins() { if (allPlugins == null) { allPlugins = findServices(Plugin.class); } return allPlugins; } public Language getSchemaLanguage() { if (schemaLanguage == null) schemaLanguage = guessSchemaLanguage(); return schemaLanguage; } public void setSchemaLanguage(Language _schemaLanguage) { this.schemaLanguage = _schemaLanguage; } /** * Input schema files. * */ public InputSource[] getGrammars() { return grammars.toArray(new InputSource[0]); } /** * Adds a new input schema. * */ public void addGrammar(InputSource is) { grammars.add(absolutize(is)); } private InputSource fileToInputSource(File source) { try { String url = source.toURI().toURL().toExternalForm(); return new InputSource(Util.escapeSpace(url)); } catch (MalformedURLException e) { return new InputSource(source.getPath()); } } public void addGrammar(File source) { addGrammar(fileToInputSource(source)); } /** * Recursively scan directories and add all XSD files in it. * */ public void addGrammarRecursive(File dir) { addRecursive(dir, ".xsd", grammars); } private void addRecursive(File dir, String suffix, List result) { File[] files = dir.listFiles(); if (files == null) return; // work defensively for (File f : files) { if (f.isDirectory()) addRecursive(f, suffix, result); else if (f.getPath().endsWith(suffix)) result.add(absolutize(fileToInputSource(f))); } } private InputSource absolutize(InputSource is) { // absolutize all the system IDs in the input, so that we can map system IDs to DOM trees. try { URL baseURL = new File(".").getCanonicalFile().toURI().toURL(); is.setSystemId(new URL(baseURL, is.getSystemId()).toExternalForm()); } catch (IOException e) { logger.log(Level.FINE, "{0}, {1}", new Object[]{is.getSystemId(), e.getLocalizedMessage()}); } return is; } /** * Input external binding files. * */ public InputSource[] getBindFiles() { return bindFiles.toArray(new InputSource[0]); } /** * Adds a new binding file. * */ public void addBindFile(InputSource is) { bindFiles.add(absolutize(is)); } /** * Adds a new binding file. * */ public void addBindFile(File bindFile) { bindFiles.add(fileToInputSource(bindFile)); } /** * Recursively scan directories and add all ".xjb" files in it. * */ public void addBindFileRecursive(File dir) { addRecursive(dir, ".xjb", bindFiles); } public final List classpaths = new ArrayList<>(); /** * Gets a classLoader that can load classes specified via the * -classpath option. * */ public ClassLoader getUserClassLoader(ClassLoader parent) { if (classpaths.isEmpty()) return parent; return new URLClassLoader( classpaths.toArray(new URL[0]), parent); } /** * Gets Java module name option. * * @return Java module name option or {@code null} if this option was not set. */ public String getModuleName() { return javaModule; } /** * Parses an option {@code args[i]} and return * the number of tokens consumed. * * @return 0 if the argument is not understood. Returning 0 * will let the caller report an error. * @throws BadCommandLineException If the callee wants to provide a custom message for an error. */ public int parseArgument(String[] args, int i) throws BadCommandLineException { if (args[i].equals("-classpath") || args[i].equals("-cp")) { String a = requireArgument(args[i], args, ++i); for (String p : a.split(File.pathSeparator)) { File file = new File(p); try { classpaths.add(file.toURI().toURL()); } catch (MalformedURLException e) { throw new BadCommandLineException( Messages.format(Messages.NOT_A_VALID_FILENAME, file), e); } } return 2; } if (args[i].equals("-d")) { targetDir = new File(requireArgument("-d", args, ++i)); if (!targetDir.exists()) throw new BadCommandLineException( Messages.format(Messages.NON_EXISTENT_DIR, targetDir)); return 2; } if (args[i].equals("-readOnly")) { readOnly = true; return 1; } if (args[i].equals("-p")) { defaultPackage = requireArgument("-p", args, ++i); if (defaultPackage.length() == 0) { // user specified default package // there won't be any package to annotate, so disable them // automatically as a usability feature packageLevelAnnotations = false; } return 2; } if (args[i].equals("-m")) { javaModule = requireArgument("-m", args, ++i); return 2; } if (args[i].equals("-debug")) { debugMode = true; verbose = true; return 1; } if (args[i].equals("-nv")) { strictCheck = false; return 1; } if (args[i].equals("-npa")) { packageLevelAnnotations = false; return 1; } if (args[i].equals("-no-header")) { noFileHeader = true; return 1; } if (args[i].equals("-verbose")) { verbose = true; return 1; } if (args[i].equals("-quiet")) { quiet = true; return 1; } if (args[i].equals("-XexplicitAnnotation")) { runtime14 = true; return 1; } if (args[i].equals("-enableIntrospection")) { enableIntrospection = true; return 1; } if (args[i].equals("-disableXmlSecurity")) { disableXmlSecurity = true; return 1; } if (args[i].equals("-contentForWildcard")) { contentForWildcard = true; return 1; } if (args[i].equals("-XautoNameResolution")) { automaticNameConflictResolution = true; return 1; } if (args[i].equals("-b")) { addFile(requireArgument("-b", args, ++i), bindFiles, ".xjb"); return 2; } if (args[i].equals("-dtd")) { schemaLanguage = Language.DTD; return 1; } if (args[i].equals("-relaxng")) { schemaLanguage = Language.RELAXNG; return 1; } if (args[i].equals("-relaxng-compact")) { schemaLanguage = Language.RELAXNG_COMPACT; return 1; } if (args[i].equals("-xmlschema")) { schemaLanguage = Language.XMLSCHEMA; return 1; } if (args[i].equals("-wsdl")) { schemaLanguage = Language.WSDL; return 1; } if (args[i].equals("-extension")) { compatibilityMode = EXTENSION; return 1; } if (args[i].equals("-target")) { String token = requireArgument("-target", args, ++i); target = SpecVersion.parse(token); if (target == null) throw new BadCommandLineException(Messages.format(Messages.ILLEGAL_TARGET_VERSION, token)); addClassNameReplacers(target); return 2; } if (args[i].equals("-httpproxyfile")) { if (i == args.length - 1 || args[i + 1].startsWith("-")) { throw new BadCommandLineException( Messages.format(Messages.MISSING_PROXYFILE)); } File file = new File(args[++i]); if (!file.exists()) { throw new BadCommandLineException( Messages.format(Messages.NO_SUCH_FILE, file)); } try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) { parseProxy(in.readLine()); } catch (IOException e) { throw new BadCommandLineException( Messages.format(Messages.FAILED_TO_PARSE, file, e.getMessage()), e); } return 2; } if (args[i].equals("-httpproxy")) { if (i == args.length - 1 || args[i + 1].startsWith("-")) { throw new BadCommandLineException( Messages.format(Messages.MISSING_PROXY)); } parseProxy(args[++i]); return 2; } if (args[i].equals("-host")) { proxyHost = requireArgument("-host", args, ++i); return 2; } if (args[i].equals("-port")) { proxyPort = requireArgument("-port", args, ++i); return 2; } if (args[i].equals("-catalog")) { // use Sun's "XML Entity and URI Resolvers" by Norman Walsh // to resolve external entities. // https://xerces.apache.org/xml-commons/components/resolver/resolver-article.html File catalogFile = new File(requireArgument("-catalog", args, ++i)); try { addCatalog(catalogFile); } catch (IOException e) { throw new BadCommandLineException( Messages.format(Messages.FAILED_TO_PARSE, catalogFile, e.getMessage()), e); } return 2; } if (args[i].equals("-Xtest-class-name-allocator")) { classNameAllocator = new ClassNameAllocator() { @Override public String assignClassName(String packageName, String className) { System.out.printf("assignClassName(%s,%s)\n", packageName, className); return className + "_Type"; } }; return 1; } if (args[i].equals("-encoding")) { encoding = requireArgument("-encoding", args, ++i); try { if (!Charset.isSupported(encoding)) { throw new BadCommandLineException( Messages.format(Messages.UNSUPPORTED_ENCODING, encoding)); } } catch (IllegalCharsetNameException icne) { throw new BadCommandLineException( Messages.format(Messages.UNSUPPORTED_ENCODING, encoding)); } return 2; } // see if this is one of the extensions for (Plugin plugin : getAllPlugins()) { try { if (('-' + plugin.getOptionName()).equals(args[i])) { activePlugins.add(plugin); plugin.onActivated(this); pluginURIs.addAll(plugin.getCustomizationURIs()); // give the plugin a chance to parse arguments to this option. // this is new in 2.1, and due to the backward compatibility reason, // if plugin didn't understand it, we still return 1 to indicate // that this option is consumed. int r = plugin.parseArgument(this, args, i); if (r != 0) return r; else return 1; } int r = plugin.parseArgument(this, args, i); if (r != 0) return r; } catch (IOException e) { throw new BadCommandLineException(e.getMessage(), e); } } return 0; // unrecognized } private boolean addClassNameReplacers(SpecVersion target) { boolean isJavax = isJavax(); if (!isJavax && target.ordinal() < SpecVersion.V3_0.ordinal()) { logger.warning("Jakarta does not support version 2.x version "); classNameReplacer.put(JAKARTA, JAVAX); classNameReplacer.put(JAXB_CORE, BIND); } else if (isJavax && target.ordinal() >= SpecVersion.V3_0.ordinal()) { logger.warning("Javax does not support version 3.x version "); } return true; } private boolean isJavax() { try { Class.forName("javax.xml.bind.annotation.XmlType"); return true; } catch (ClassNotFoundException e) { return false; } } private void parseProxy(String text) throws BadCommandLineException { int i = text.lastIndexOf('@'); int j = text.lastIndexOf(':'); if (i > 0) { proxyAuth = text.substring(0, i); if (j > i) { proxyHost = text.substring(i + 1, j); proxyPort = text.substring(j + 1); } else { proxyHost = text.substring(i + 1); proxyPort = "80"; } } else { //no auth info if (j < 0) { //no port proxyHost = text; proxyPort = "80"; } else { proxyHost = text.substring(0, j); proxyPort = text.substring(j + 1); } } try { Integer.valueOf(proxyPort); } catch (NumberFormatException e) { throw new BadCommandLineException(Messages.format(Messages.ILLEGAL_PROXY, text)); } } /** * Obtains an operand and reports an error if it's not there. * */ public String requireArgument(String optionName, String[] args, int i) throws BadCommandLineException { if (i == args.length || args[i].startsWith("-")) { throw new BadCommandLineException( Messages.format(Messages.MISSING_OPERAND, optionName)); } return args[i]; } /** * Parses a token to a file (or a set of files) * and add them as {@link InputSource} to the specified list. * * @param suffix If the given token is a directory name, we do a recursive search * and find all files that have the given suffix. */ private void addFile(String name, List target, String suffix) throws BadCommandLineException { Object src; try { src = Util.getFileOrURL(name); } catch (IOException e) { throw new BadCommandLineException( Messages.format(Messages.NOT_A_FILE_NOR_URL, name)); } if (src instanceof URL) { target.add(absolutize(new InputSource(Util.escapeSpace(((URL) src).toExternalForm())))); } else { File fsrc = (File) src; if (fsrc.isDirectory()) { addRecursive(fsrc, suffix, target); } else { target.add(absolutize(fileToInputSource(fsrc))); } } } // Since javax.xml.catalog is unmodifiable we need to track catalog // URLs added and create new catalog each time addCatalog is called private final ArrayList catalogUrls = new ArrayList<>(); /** * Adds a new catalog file.Use created or existed resolver to parse new catalog file. * */ public void addCatalog(File catalogFile) throws IOException { URI newUri = catalogFile.toURI(); if (!catalogUrls.contains(newUri)) { catalogUrls.add(newUri); } entityResolver = CatalogUtil.getCatalog(entityResolver, catalogFile, catalogUrls); } /** * Parses arguments and fill fields of this object. * * @throws BadCommandLineException thrown when there's a problem in the command-line arguments */ public void parseArguments(String[] args) throws BadCommandLineException { for (int i = 0; i < args.length; i++) { if (args[i].length() == 0) throw new BadCommandLineException(); if (args[i].charAt(0) == '-') { int j = parseArgument(args, i); if (j == 0) throw new BadCommandLineException( Messages.format(Messages.UNRECOGNIZED_PARAMETER, args[i])); i += (j - 1); } else { if (args[i].endsWith(".jar")) scanEpisodeFile(new File(args[i])); else addFile(args[i], grammars, ".xsd"); } } // configure proxy if (proxyHost != null || proxyPort != null) { if (proxyHost != null && proxyPort != null) { System.setProperty("http.proxyHost", proxyHost); System.setProperty("http.proxyPort", proxyPort); System.setProperty("https.proxyHost", proxyHost); System.setProperty("https.proxyPort", proxyPort); } else if (proxyHost == null) { throw new BadCommandLineException( Messages.format(Messages.MISSING_PROXYHOST)); } else { throw new BadCommandLineException( Messages.format(Messages.MISSING_PROXYPORT)); } if (proxyAuth != null) { DefaultAuthenticator.getAuthenticator().setProxyAuth(proxyAuth); } } if (grammars.isEmpty()) throw new BadCommandLineException( Messages.format(Messages.MISSING_GRAMMAR)); if (schemaLanguage == null) schemaLanguage = guessSchemaLanguage(); // if(target==SpecVersion.V3_0 && !isExtensionMode()) // throw new BadCommandLineException( // "Currently 2.2 is still not finalized yet, so using it requires the -extension switch." + // "NOTE THAT 2.2 SPEC MAY CHANGE BEFORE IT BECOMES FINAL."); if (pluginLoadFailure != null) throw new BadCommandLineException( Messages.format(Messages.PLUGIN_LOAD_FAILURE, pluginLoadFailure)); } /** * Finds the {@code META-INF/sun-jaxb.episode} file to add as a binding customization. * */ public void scanEpisodeFile(File jar) throws BadCommandLineException { try { URLClassLoader ucl = new URLClassLoader(new URL[]{jar.toURI().toURL()}); Enumeration resources = ucl.findResources("META-INF/sun-jaxb.episode"); while (resources.hasMoreElements()) { URL url = resources.nextElement(); addBindFile(new InputSource(url.toExternalForm())); } } catch (IOException e) { throw new BadCommandLineException( Messages.format(Messages.FAILED_TO_LOAD, jar, e.getMessage()), e); } } /** * Guesses the schema language. * */ public Language guessSchemaLanguage() { // otherwise, use the file extension. // not a good solution, but very easy. if ((grammars != null) && (grammars.size() > 0)) { String name = grammars.get(0).getSystemId().toLowerCase(); if (name.endsWith(".rng")) return Language.RELAXNG; if (name.endsWith(".rnc")) return Language.RELAXNG_COMPACT; if (name.endsWith(".dtd")) return Language.DTD; if (name.endsWith(".wsdl")) return Language.WSDL; } // by default, assume XML Schema return Language.XMLSCHEMA; } /** * Creates a configured CodeWriter that produces files into the specified directory. * */ public CodeWriter createCodeWriter() throws IOException { return createCodeWriter(new FileCodeWriter(targetDir, readOnly, encoding)); } /** * Creates a configured CodeWriter that produces files into the specified directory. * */ public CodeWriter createCodeWriter(CodeWriter core) { if (noFileHeader) return core; return new PrologCodeWriter(core, getPrologComment()); } /** * Gets the string suitable to be used as the prolog comment baked into artifacts.This is the string like "This file was generated by the JAXB RI on YYYY/mm/dd..." * */ public String getPrologComment() { // generate format syntax: 'at'





© 2015 - 2024 Weber Informatics LLC | Privacy Policy