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

io.swagger.parser.SwaggerCompatConverter Maven / Gradle / Ivy

There is a newer version: 1.0.71
Show newest version
package io.swagger.parser;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.swagger.models.ArrayModel;
import io.swagger.models.AuthorizationScope;
import io.swagger.models.Contact;
import io.swagger.models.Info;
import io.swagger.models.License;
import io.swagger.models.Method;
import io.swagger.models.Model;
import io.swagger.models.ModelImpl;
import io.swagger.models.Operation;
import io.swagger.models.ParamType;
import io.swagger.models.PassAs;
import io.swagger.models.Path;
import io.swagger.models.RefModel;
import io.swagger.models.Response;
import io.swagger.models.Scheme;
import io.swagger.models.Swagger;
import io.swagger.models.SwaggerVersion;
import io.swagger.models.apideclaration.Api;
import io.swagger.models.apideclaration.ApiDeclaration;
import io.swagger.models.apideclaration.ExtendedTypedObject;
import io.swagger.models.apideclaration.Items;
import io.swagger.models.apideclaration.ModelProperty;
import io.swagger.models.apideclaration.ResponseMessage;
import io.swagger.models.auth.ApiKeyAuthDefinition;
import io.swagger.models.auth.AuthorizationValue;
import io.swagger.models.auth.BasicAuthDefinition;
import io.swagger.models.auth.In;
import io.swagger.models.auth.OAuth2Definition;
import io.swagger.models.parameters.BodyParameter;
import io.swagger.models.parameters.FormParameter;
import io.swagger.models.parameters.HeaderParameter;
import io.swagger.models.parameters.Parameter;
import io.swagger.models.parameters.PathParameter;
import io.swagger.models.parameters.QueryParameter;
import io.swagger.models.parameters.SerializableParameter;
import io.swagger.models.properties.ArrayProperty;
import io.swagger.models.properties.Property;
import io.swagger.models.properties.PropertyBuilder;
import io.swagger.models.properties.RefProperty;
import io.swagger.models.properties.StringProperty;
import io.swagger.models.properties.UntypedProperty;
import io.swagger.models.refs.RefFormat;
import io.swagger.models.resourcelisting.ApiInfo;
import io.swagger.models.resourcelisting.ApiKeyAuthorization;
import io.swagger.models.resourcelisting.ApiListingReference;
import io.swagger.models.resourcelisting.Authorization;
import io.swagger.models.resourcelisting.AuthorizationCodeGrant;
import io.swagger.models.resourcelisting.BasicAuthorization;
import io.swagger.models.resourcelisting.ImplicitGrant;
import io.swagger.models.resourcelisting.OAuth2Authorization;
import io.swagger.models.resourcelisting.ResourceListing;
import io.swagger.parser.util.ClasspathHelper;
import io.swagger.parser.util.RemoteUrl;
import io.swagger.parser.util.SwaggerDeserializationResult;
import io.swagger.report.MessageBuilder;
import io.swagger.transform.migrate.ApiDeclarationMigrator;
import io.swagger.transform.migrate.ResourceListingMigrator;
import io.swagger.util.Json;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.math.BigDecimal;
import java.net.URI;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

// legacy models

public class SwaggerCompatConverter implements SwaggerParserExtension {
    static Logger LOGGER = LoggerFactory.getLogger(SwaggerCompatConverter.class);

    @Override
    public SwaggerDeserializationResult readWithInfo(JsonNode node) {
        // not implemented
        return null;
    }

    @Override
    public SwaggerDeserializationResult readWithInfo(String location, List auths) {
        SwaggerDeserializationResult result = new SwaggerDeserializationResult();
        try {
            Swagger swagger = read(location, auths);
            if(swagger != null) {
                result.setSwagger(swagger);
                JsonNode jsonNode = Json.mapper().convertValue(swagger, JsonNode.class);
                return new Swagger20Parser().readWithInfo(jsonNode);
            }
        }
        catch (IOException e) {
            // TODO
        }
        return result;
    }

    @Override
    public Swagger read(JsonNode node) throws IOException {
        // TODO
        return null;
    }

    public Swagger read(String input) throws IOException {
        return read(input, null);
    }

    @Override
    public Swagger read(String input, List auths) throws IOException {
        Swagger output = null;
        MessageBuilder migrationMessages = new MessageBuilder();
        SwaggerLegacyParser swaggerParser = new SwaggerLegacyParser();
        ResourceListing resourceListing = null;
        resourceListing = readResourceListing(input, migrationMessages, auths);

        List apis = new ArrayList();

        if (resourceListing != null) {
            List refs = resourceListing.getApis();
            boolean readAsSingleFile = false;
            if (refs != null) {
                for (ApiListingReference ref : refs) {
                    ApiDeclaration apiDeclaration = null;
                    JsonNode node = ref.getExtraFields();
                    JsonNode operations = node.get("operations");
                    if (operations != null) {
                        if (!readAsSingleFile) {
                            // this is a single-file swagger definition
                            apiDeclaration = readDeclaration(input, migrationMessages, auths);
                            readAsSingleFile = true; // avoid doing this again
                        }
                    } else {
                        String location = null;
                        if (input.startsWith("http")) {
                            // look up as url
                            String pathLocation = ref.getPath();
                            if (pathLocation.startsWith("http")) {
                                // use as absolute url
                                location = pathLocation;
                            } else {
                                if (pathLocation.startsWith("/")) {
                                    // handle 1.1 specs
                                    if(resourceListing.getSwaggerVersion().equals(SwaggerVersion.V1_1) &&
                                            resourceListing.getExtraFields().get("basePath") != null) {
                                        String basePath = resourceListing.getExtraFields().get("basePath").textValue();
                                        location = basePath + pathLocation;
                                    }
                                    else {
                                        location = input + pathLocation;
                                    }
                                } else {
                                    location = input + "/" + pathLocation;
                                }
                            }
                        } else {
                            // file system
                            File fileLocation = new File(input);
                            if (ref.getPath().startsWith("/")) {
                                location = fileLocation.getParent() + ref.getPath();
                            } else {
                                location = fileLocation.getParent() + File.separator + ref.getPath();
                            }
                        }
                        if (location.indexOf(".{format}") != -1) {
                            location = location.replaceAll("\\.\\{format\\}", ".json");
                        }
                        apiDeclaration = readDeclaration(location, migrationMessages, auths);
                    }
                    if (apiDeclaration != null) {
                        apis.add(apiDeclaration);
                    }
                }
            }
            output = convert(resourceListing, apis);
        }
        return output;
    }

    public ResourceListing readResourceListing(String input, MessageBuilder messages, List auths) {
        ResourceListing output = null;
        JsonNode jsonNode = null;
        try {
            if (input.startsWith("http")) {
                String json = RemoteUrl.urlToString(input, auths);
                jsonNode = Json.mapper().readTree(json);
            } else {
                final String fileScheme = "file:";
                java.nio.file.Path path;
                if (input.toLowerCase().startsWith(fileScheme)) {
                    path = java.nio.file.Paths.get(URI.create(input));
                } else {
                    path = java.nio.file.Paths.get(input);
                }
                String json;
                if (Files.exists(path)) {
                    json= FileUtils.readFileToString(path.toFile(), "UTF-8");
                } else {
                    json = ClasspathHelper.loadFileFromClasspath(input  );
                }

                jsonNode = Json.mapper().readTree(json);

            }
            if (jsonNode == null || jsonNode.get("swaggerVersion") == null) {
                return null;
            }
            ResourceListingMigrator migrator = new ResourceListingMigrator();
            JsonNode transformed = migrator.migrate(messages, jsonNode);
            output = Json.mapper().convertValue(transformed, ResourceListing.class);
        } catch (java.lang.IllegalArgumentException e) {
            return null;
        } catch (Exception e) {
            LOGGER.error("failed to read resource listing", e);
        }
        return output;
    }

    public Model convertModel(io.swagger.models.apideclaration.Model model) {
        ModelImpl output = new ModelImpl();
        output.setName(model.getId());
        output.setDescription(model.getDescription());
        output.setDiscriminator(model.getDiscriminator());
        if (model.getRequired() != null) {
            output.setRequired(model.getRequired());
        }
        for (String key : model.getProperties().keySet()) {
            Property prop = convertProperty(model.getProperties().get(key));
            if (prop != null) {
                output.addProperty(key, prop);
            }
        }
        return output;
    }

    public Property convertProperty(ModelProperty property) {
        Property output = null;
        output = propertyFromTypedObject(property);
        output.setDescription(property.getDescription());
        return output;
    }

    public Parameter convertParameter(io.swagger.models.apideclaration.Parameter param) {
        Parameter output = null;
        List _enum = param.getEnumValues();

        if (ParamType.PATH.equals(param.getParamType())) {
            PathParameter p = new PathParameter();
            p.setDefaultValue(param.getDefaultValue());
            p.setEnum(_enum);
            output = p;
        } else if (ParamType.QUERY.equals(param.getParamType())) {
            QueryParameter p = new QueryParameter();
            p.setDefaultValue(param.getDefaultValue());
            p.setEnum(_enum);
            output = p;
        } else if (ParamType.HEADER.equals(param.getParamType())) {
            HeaderParameter p = new HeaderParameter();
            p.setDefaultValue(param.getDefaultValue());
            p.setEnum(_enum);
            output = p;
        } else if (ParamType.BODY.equals(param.getParamType())) {
            BodyParameter p = new BodyParameter();
            output = p;
        } else if (ParamType.FORM.equals(param.getParamType())) {
            FormParameter p = new FormParameter();
            p.setDefaultValue(param.getDefaultValue());
            p.setEnum(_enum);
            output = p;
        }

        output.setName(param.getName());
        output.setDescription(param.getDescription());
        if (param.getRequired() != null) {
            output.setRequired(param.getRequired());
        }

        Property property = null;
        String type = param.getType() == null ? null : param.getType().toString();
        String format = param.getFormat() == null ? null : param.getFormat().toString();

        if (null == type) {
            LOGGER.warn("Empty type in Param: " + param);
        }

        if (output instanceof BodyParameter) {
            BodyParameter bp = (BodyParameter) output;
            bp.setSchema(modelFromExtendedTypedObject(param));
        } else if (output instanceof SerializableParameter) {
            SerializableParameter sp = (SerializableParameter) output;
            Property p = null;
            if (param.getAllowMultiple() != null && param.getAllowMultiple() == true) {
                ArrayProperty arrayProperty = new ArrayProperty();
                Property innerType = PropertyBuilder.build(type, format, null);
                arrayProperty.setItems(innerType);
                p = arrayProperty;
            } else {
                p = propertyFromTypedObject(param);
                if (p == null) {
                    LOGGER.warn(String.format(
                            "WARNING! No property detected for parameter '%s' (%s)! Falling back to string!",
                            param.getName(), param.getParamType()));
                    p = new StringProperty();
                }
            }
            if (p instanceof ArrayProperty) {
                ArrayProperty ap = (ArrayProperty) p;
                sp.setType("array");
                sp.setCollectionFormat("csv");
                sp.setItems(ap.getItems());
            } else {
                sp.setType(p.getType());
                sp.setFormat(p.getFormat());
            }
        }
        // all path parameters are required
        if (output instanceof PathParameter) {
            ((PathParameter) output).setRequired(true);
        }
        return output;
    }

    public Model modelFromExtendedTypedObject(ExtendedTypedObject obj) {
        String type = obj.getType() == null ? null : obj.getType().toString();
        String format = obj.getFormat() == null ? null : obj.getFormat().toString();

        Model output = null;
        if (obj.getRef() != null) {
            output = new RefModel().asDefault(obj.getRef());
        } else {
            if ("array".equals(type)) {
                ArrayModel am = new ArrayModel();
                Items items = obj.getItems();
                type = items.getType() == null ? null : items.getType().toString();
                format = items.getFormat() == null ? null : items.getFormat().toString();

                Property innerType = PropertyBuilder.build(type, format, null);
                if (innerType != null) {
                    am.setItems(innerType);
                }
                if (items.getRef() != null) {
                    am.setItems(new RefProperty(items.getRef(),RefFormat.INTERNAL));
                } else {
                    am.setItems(new RefProperty(type,RefFormat.INTERNAL));
                }
                output = am;
            } else {
                Property input = PropertyBuilder.build(type, format, null);
                if (input != null) {
                	output = PropertyBuilder.toModel(input);
                } else if (!"void".equals(type)) {
                    //use ref model
                    output = new RefModel().asDefault(type);
                }
            }
        }
        return output;
    }

    public Property propertyFromTypedObject(ExtendedTypedObject obj) {
        String type = obj.getType() == null ? null : obj.getType().toString();
        String format = obj.getFormat() == null ? null : obj.getFormat().toString();

        Property output = null;
        if ("array".equals(type)) {
            ArrayProperty am = new ArrayProperty();
            Items items = obj.getItems();
            if (items == null) {
                LOGGER.error("Error! Missing array type for property!  Assuming `object` -- please fix your spec");
                items = new Items();
                items.setType("object");
            }
            type = items.getType() == null ? null : items.getType().toString();
            format = items.getFormat() == null ? null : items.getFormat().toString();

            Map args = new HashMap<>();
            if(items.getExtraFields().get("enum") != null && items.getExtraFields().get("enum").isArray()) {
                ArrayNode an = (ArrayNode)items.getExtraFields().get("enum");

                List enumValues = new ArrayList<>();
                for(JsonNode jn : an) {
                    enumValues.add(jn.textValue());
                }
                args.put(PropertyBuilder.PropertyId.ENUM, enumValues);
            }
            Property innerType = PropertyBuilder.build(type, format, args);
            if (innerType != null && !(innerType instanceof UntypedProperty)) {
                am.setItems(innerType);
            } else if (items.getRef() != null) {
                am.setItems(new RefProperty(items.getRef(),RefFormat.INTERNAL));
            } else {
                am.setItems(new RefProperty(type,RefFormat.INTERNAL));
            }
            output = am;
        } else {
            Map args = new HashMap();
            if (obj.getEnumValues() != null && obj.getEnumValues().size() > 0) {
                args.put(PropertyBuilder.PropertyId.ENUM, obj.getEnumValues());
            }
            if (obj.getMinimum() != null) {
                args.put(PropertyBuilder.PropertyId.MINIMUM, new BigDecimal(obj.getMinimum()));
            }
            if (obj.getMaximum() != null) {
                args.put(PropertyBuilder.PropertyId.MAXIMUM, new BigDecimal(obj.getMaximum()));
            }

            Property i = PropertyBuilder.build(type, format, args);
            if (i != null && !(i instanceof UntypedProperty)) {
                output = i;
            } else {
                if (obj.getRef() != null) {
                    output = new RefProperty(obj.getRef(),RefFormat.INTERNAL);
                } else if (type != null && !type.equals("void")) {
                    output = new RefProperty(type,RefFormat.INTERNAL);
                }
            }
        }

        return output;
    }

    public Operation convertOperation(String tag, io.swagger.models.apideclaration.Operation operation,
                                      ApiDeclaration apiDeclaration) {
        Method method;

        if (operation.getMethod() == null) {
            JsonNode node = (JsonNode) operation.getExtraFields().get("httpMethod");
            method = Method.forValue(node.asText());
            operation.setMethod(method);
        }

        Operation output = new Operation()
                .summary(operation.getSummary())
                .description(operation.getNotes())
                .operationId(operation.getNickname());

        if (tag != null) {
            output.tag(tag);
        }

        for (io.swagger.models.apideclaration.Parameter parameter : operation.getParameters()) {
            output.parameter(convertParameter(parameter));
        }

        if (operation.getConsumes() != null && !operation.getConsumes().isEmpty()) {
            for (String consumes : operation.getConsumes()) {
                output.consumes(consumes);
            }
        } else if (apiDeclaration.getConsumes() != null) {
            for (String consumes : apiDeclaration.getConsumes()) {
                output.consumes(consumes);
            }
        }
        if (operation.getProduces() != null && !operation.getProduces().isEmpty()) {
            for (String produces : operation.getProduces()) {
                output.produces(produces);
            }
        } else if (apiDeclaration.getProduces() != null) {
            for (String produces : apiDeclaration.getProduces()) {
                output.produces(produces);
            }
        }

        for (ResponseMessage message : operation.getResponseMessages()) {
            Response response = new Response().description(message.getMessage());

            Model responseModel = null;
            if (message.getResponseModel() != null) {
                response.setResponseSchema(new RefModel().asDefault(message.getResponseModel()));
            }
            output.response(message.getCode(), response);
        }

        // default response type
        Model responseProperty = modelFromExtendedTypedObject(operation);
        Response response = new Response()
                .description("success")
                .responseSchema(responseProperty);
        if (output.getResponses() == null) {
            output.defaultResponse(response);
        } else if (responseProperty != null) {
            output.response(200, response);
        }

        Map> auths = operation.getAuthorizations();

        for (String securityName : auths.keySet()) {
            List scopes = auths.get(securityName);
            List updatedScopes = new ArrayList();
            for (AuthorizationScope s : scopes) {
                updatedScopes.add(s.getScope());
            }
            output.addSecurity(securityName, updatedScopes);
        }

        return output;
    }

    public ApiDeclaration readDeclaration(String input, MessageBuilder messages, List auths) {
        ApiDeclaration output = null;
        try {
            JsonNode jsonNode = null;
            if (input.startsWith("http")) {
                String json = RemoteUrl.urlToString(input, auths);
                jsonNode = Json.mapper().readTree(json);
            } else {
                final String fileScheme = "file:";
                java.nio.file.Path path;
                if (input.toLowerCase().startsWith(fileScheme)) {
                    path = java.nio.file.Paths.get(URI.create(input));
                } else {
                    path = java.nio.file.Paths.get(input);
                }
                String json;
                if (Files.exists(path)) {
                     json= FileUtils.readFileToString(path.toFile(), "UTF-8");
                } else {
                    json = ClasspathHelper.loadFileFromClasspath(input  );
                }

                jsonNode = Json.mapper().readTree(json);
            }

            // this should be moved to a json patch
            if (jsonNode.isObject()) {
                ((ObjectNode) jsonNode).remove("authorizations");
            }

            ApiDeclarationMigrator migrator = new ApiDeclarationMigrator();
            JsonNode transformed = migrator.migrate(messages, jsonNode);
            output = Json.mapper().convertValue(transformed, ApiDeclaration.class);
        } catch (java.lang.IllegalArgumentException e) {
            return null;
        } catch (Exception e) {
            LOGGER.error("failed to read api declaration", e);
        }
        return output;
    }

    public Swagger convert(ResourceListing resourceListing, List apiDeclarations) {
        Info info = new Info();
        if (resourceListing.getInfo() != null) {
            ApiInfo apiInfo = resourceListing.getInfo();
            Contact contact = null;
            if (apiInfo.getContact() != null) {
                contact = new Contact()
                        .url(apiInfo.getContact());
            }
            License license = null;
            if (apiInfo.getLicense() != null) {
                license = new License()
                        .name(apiInfo.getLicense())
                        .url(apiInfo.getLicenseUrl());
            }
            info = new Info()
                    .description(apiInfo.getDescription())
                    .version(resourceListing.getApiVersion())
                    .title(apiInfo.getTitle())
                    .termsOfService(apiInfo.getTermsOfServiceUrl())
                    .contact(contact)
                    .license(license);
        } else if (resourceListing.getApiVersion() != null) {
            info = new Info()
                    .version(resourceListing.getApiVersion());
        }
        Map paths = new HashMap();
        Map definitions = new HashMap();
        String basePath = null;

        for (ApiDeclaration apiDeclaration : apiDeclarations) {
            String tag;
            if (apiDeclaration.getApiListingRef() != null) {
                String refPath = apiDeclaration.getApiListingRef().getPath();
                tag = refPath.substring(refPath.lastIndexOf("/") + 1);
            } else {
                tag = apiDeclaration.getResourcePath();
            }
            if (tag != null) {
                tag = tag.replaceAll("/", "");
            }
            if (basePath != null) {
                if (!basePath.equals(apiDeclaration.getBasePath()) && apiDeclaration.getBasePath() != null) {
                    LOGGER.warn("warning!  multiple basePath values not supported!");
                }
            } else {
                basePath = apiDeclaration.getBasePath();
            }

            List apis = apiDeclaration.getApis();
            for (Api api : apis) {
                String apiPath = api.getPath();
                String description = api.getDescription();
                List ops = api.getOperations();

                Path path = paths.get(apiPath);
                if (path == null) {
                    path = new Path();
                    paths.put(apiPath, path);
                }
                for (io.swagger.models.apideclaration.Operation op : ops) {
                    Operation operation = convertOperation(tag, op, apiDeclaration);

                    if (op.getMethod() != null) {
                        path.set(op.getMethod().toString().toLowerCase(), operation);
                    } else {
                        LOGGER.info("skipping operation with missing method:\n" + Json.pretty(op));
                    }
                }
            }

            // model definitions
            Map apiModels = apiDeclaration.getModels();
            for (String key : apiModels.keySet()) {
                Model model = convertModel(apiModels.get(key));
                definitions.put(key, model);
            }
        }

        String host = null;
        String scheme = "http";

        if (basePath != null) {
            String[] parts = basePath.split("://");
            if (parts.length == 2) {
                scheme = parts[0];
                int pos = parts[1].indexOf("/");
                if (pos != -1) {
                    host = parts[1].substring(0, pos);
                    basePath = parts[1].substring(pos);
                } else {
                    host = parts[1];
                    basePath = "/";
                }
            }
            if (!basePath.startsWith("/")) {
                basePath = "/" + basePath;
            }
        }


        Swagger swagger = new Swagger()
                .host(host)
                .scheme(Scheme.forValue(scheme))
                .basePath(basePath)
                .info(info)
                        // .securityRequirement(securityRequirement)
                .paths(paths)
                        // .securityDefinitions(name, securityScheme)
                .basePath(basePath);
        swagger.setDefinitions(definitions);
        // host is read from the api declarations


        Map authorizations = resourceListing.getAuthorizations();
        if (authorizations != null) {
            for (String authNickname : authorizations.keySet()) {
                Authorization auth = authorizations.get(authNickname);
                if (auth instanceof OAuth2Authorization) {
                    OAuth2Authorization o2 = (OAuth2Authorization) auth;
                    List scopes = o2.getScopes();

                    if (o2.getGrantTypes().getImplicit() != null) {
                        ImplicitGrant ig = o2.getGrantTypes().getImplicit();
                        OAuth2Definition oauth2 = new OAuth2Definition()
                                .implicit(ig.getLoginEndpoint().getUrl());
                        if (swagger.getSecurityDefinitions() != null && swagger.getSecurityDefinitions().keySet().contains(authNickname)) {
                            System.err.println("Warning!  Authorization nickname already in use!");
                        } else {
                            swagger.securityDefinition(authNickname, oauth2);
                        }
                        for (AuthorizationScope scope : scopes) {
                            oauth2.scope(scope.getScope(), scope.getDescription());
                        }
                    } else if (o2.getGrantTypes().getAuthorization_code() != null) {
                        AuthorizationCodeGrant ac = (AuthorizationCodeGrant) o2.getGrantTypes().getAuthorization_code();
                        OAuth2Definition oauth2 = new OAuth2Definition()
                                .accessCode(ac.getTokenRequestEndpoint().getUrl(), ac.getTokenEndpoint().getUrl());
                        if (swagger.getSecurityDefinitions() != null && swagger.getSecurityDefinitions().keySet().contains(authNickname)) {
                            System.err.println("Warning!  Authorization nickname already in use!");
                        } else {
                            swagger.securityDefinition(authNickname, oauth2);
                        }
                        for (AuthorizationScope scope : scopes) {
                            oauth2.scope(scope.getScope(), scope.getDescription());
                        }
                    }
                } else if (auth instanceof ApiKeyAuthorization) {
                    ApiKeyAuthorization o2 = (ApiKeyAuthorization) auth;
                    ApiKeyAuthDefinition def = new ApiKeyAuthDefinition();

                    PassAs passAs = o2.getPassAs();
                    if (PassAs.HEADER.equals(passAs)) {
                        def.in(In.HEADER);
                    } else {
                        def.in(In.QUERY);
                    }

                    def.setName(o2.getKeyname());

                    swagger.securityDefinition(authNickname, def);
                } else if (auth instanceof BasicAuthorization) {
                    BasicAuthDefinition def = new BasicAuthDefinition();

                    swagger.securityDefinition(authNickname, def);
                }
            }
        }
        return swagger;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy